feat: add a grid view
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package ca.ksamad.encore.data
|
||||
|
||||
/** How the albums library is laid out: a compact [List] or a two-column [Grid] of cover art. */
|
||||
enum class AlbumViewMode {
|
||||
List,
|
||||
Grid,
|
||||
;
|
||||
|
||||
companion object {
|
||||
/** Default when nothing is persisted yet. */
|
||||
val DEFAULT = List
|
||||
|
||||
/** Parse a persisted name back to a mode, tolerating unknown/legacy values. */
|
||||
fun fromName(name: String?): AlbumViewMode = entries.find { it.name == name } ?: DEFAULT
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,15 @@ class SettingsRepository(private val context: Context) {
|
||||
private object Keys {
|
||||
val HOST = stringPreferencesKey("host")
|
||||
val PORT = intPreferencesKey("port")
|
||||
val ALBUM_VIEW_MODE = stringPreferencesKey("album_view_mode")
|
||||
}
|
||||
|
||||
/** How the albums library is laid out; emits on every change, defaulted when unset. */
|
||||
val albumViewMode: Flow<AlbumViewMode> =
|
||||
context.dataStore.data.map { prefs -> AlbumViewMode.fromName(prefs[Keys.ALBUM_VIEW_MODE]) }
|
||||
|
||||
suspend fun setAlbumViewMode(mode: AlbumViewMode) {
|
||||
context.dataStore.edit { prefs -> prefs[Keys.ALBUM_VIEW_MODE] = mode.name }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ca.ksamad.encore.playback
|
||||
|
||||
import android.content.Context
|
||||
import ca.ksamad.encore.data.AlbumViewMode
|
||||
import ca.ksamad.encore.data.ConnectionSettings
|
||||
import ca.ksamad.encore.data.SettingsRepository
|
||||
import ca.ksamad.encore.mpd.MpdClient
|
||||
@@ -47,6 +48,14 @@ class MpdConnectionManager(context: Context) {
|
||||
val settings: StateFlow<ConnectionSettings> =
|
||||
settingsRepo.settings.stateIn(scope, SharingStarted.Eagerly, ConnectionSettings.DEFAULT)
|
||||
|
||||
/** Persisted albums-library layout preference. */
|
||||
val albumViewMode: StateFlow<AlbumViewMode> =
|
||||
settingsRepo.albumViewMode.stateIn(scope, SharingStarted.Eagerly, AlbumViewMode.DEFAULT)
|
||||
|
||||
fun setAlbumViewMode(mode: AlbumViewMode) {
|
||||
scope.launch { settingsRepo.setAlbumViewMode(mode) }
|
||||
}
|
||||
|
||||
// True until the startup auto-connect decision has been made.
|
||||
private val _bootstrapping = MutableStateFlow(true)
|
||||
val bootstrapping: StateFlow<Boolean> = _bootstrapping
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package ca.ksamad.encore.ui
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
@@ -44,9 +51,12 @@ import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import ca.ksamad.encore.data.AlbumViewMode
|
||||
import ca.ksamad.encore.mpd.model.MpdAlbum
|
||||
import ca.ksamad.encore.playback.AlbumArt
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -62,6 +72,8 @@ fun AlbumsScreen(
|
||||
onBack: () -> Unit,
|
||||
onOpenAlbum: (MpdAlbum) -> Unit,
|
||||
) {
|
||||
val viewMode by vm.albumViewMode.collectAsStateWithLifecycle()
|
||||
|
||||
// null = still loading.
|
||||
val albums by
|
||||
produceState<List<MpdAlbum>?>(initialValue = null) {
|
||||
@@ -173,6 +185,20 @@ fun AlbumsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
viewMode == AlbumViewMode.Grid -> {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
contentPadding = PaddingValues(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
items(filtered, key = { "${it.name} ${it.albumArtist}" }) { album ->
|
||||
AlbumGridCell(album = album, onTap = { onOpenAlbum(album) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
LazyColumn(modifier = Modifier.padding(innerPadding)) {
|
||||
items(filtered, key = { "${it.name} ${it.albumArtist}" }) { album ->
|
||||
@@ -228,6 +254,42 @@ private fun SwipeableAlbumRow(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One album tile for the grid layout: square cover art with the album name in bold underneath and
|
||||
* the artist in a smaller, muted font below that. Tapping opens the album ([onTap]).
|
||||
*/
|
||||
@Composable
|
||||
private fun AlbumGridCell(
|
||||
album: MpdAlbum,
|
||||
onTap: () -> Unit,
|
||||
) {
|
||||
Column(modifier = Modifier.clickable(onClick = onTap)) {
|
||||
ArtImage(
|
||||
model = AlbumArt(album.name, album.albumArtist),
|
||||
iconSize = 48.dp,
|
||||
modifier =
|
||||
Modifier.fillMaxWidth().aspectRatio(1f).clip(RoundedCornerShape(8.dp)),
|
||||
)
|
||||
Text(
|
||||
album.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(top = 6.dp),
|
||||
)
|
||||
album.albumArtist?.let {
|
||||
Text(
|
||||
it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline search box that lives in the top bar while searching. Auto-focuses and opens the keyboard;
|
||||
* the surrounding [TopAppBar] handles clearing/closing.
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import ca.ksamad.encore.EncoreApplication
|
||||
import ca.ksamad.encore.data.AlbumViewMode
|
||||
import ca.ksamad.encore.data.ConnectionSettings
|
||||
import ca.ksamad.encore.mpd.MpdConnectionState
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -39,6 +40,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
val currentSong = manager.currentSong
|
||||
val serverHost = manager.serverHost
|
||||
val settings = manager.settings
|
||||
val albumViewMode = manager.albumViewMode
|
||||
|
||||
val screen: StateFlow<AppScreen> =
|
||||
combine(
|
||||
@@ -86,6 +88,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
|
||||
fun setConsume(on: Boolean) = manager.setConsume(on)
|
||||
|
||||
fun setAlbumViewMode(mode: AlbumViewMode) = manager.setAlbumViewMode(mode)
|
||||
|
||||
fun playAlbum(
|
||||
album: String,
|
||||
albumArtist: String?,
|
||||
|
||||
@@ -23,6 +23,9 @@ import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -36,6 +39,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import ca.ksamad.encore.data.AlbumViewMode
|
||||
import ca.ksamad.encore.mpd.model.MpdStatistics
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
@@ -119,6 +123,41 @@ fun SettingsScreen(
|
||||
},
|
||||
)
|
||||
|
||||
Text(
|
||||
"Library",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 8.dp, top = 20.dp, bottom = 4.dp),
|
||||
)
|
||||
val albumViewMode by vm.albumViewMode.collectAsStateWithLifecycle()
|
||||
ListItem(
|
||||
headlineContent = { Text("Album view") },
|
||||
supportingContent = { Text("How the albums library is laid out") },
|
||||
trailingContent = {
|
||||
SingleChoiceSegmentedButtonRow {
|
||||
AlbumViewMode.entries.forEachIndexed { index, mode ->
|
||||
SegmentedButton(
|
||||
selected = albumViewMode == mode,
|
||||
onClick = { vm.setAlbumViewMode(mode) },
|
||||
shape =
|
||||
SegmentedButtonDefaults.itemShape(
|
||||
index = index,
|
||||
count = AlbumViewMode.entries.size,
|
||||
),
|
||||
icon = {},
|
||||
) {
|
||||
Text(
|
||||
when (mode) {
|
||||
AlbumViewMode.List -> "List"
|
||||
AlbumViewMode.Grid -> "Grid"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Text(
|
||||
"Server statistics",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
|
||||
Reference in New Issue
Block a user