trim the play entries, calm the badge, and only watch the visible tab

The album and playlist menus lose Play and Shuffle and the song menu loses Play;
tapping the row or the detail hero buttons already covers those, so the
ItemActions play/shuffle callbacks and the artist view-model methods behind them
go too.

The badge was a filled circle that stood out over the art. It is now a white
glyph with a soft shadow in the icon's own shape, with no background behind it.

The library screen held all three grouped status flows at once, so the two tabs
off screen ran queries for badges nobody could see. It now collects only the
visible tab's map.
This commit is contained in:
austinried
2026-10-04 00:30:03 +00:00
parent b4f69affc9
commit 837ab9ac5d
8 changed files with 52 additions and 98 deletions
@@ -1,17 +1,17 @@
package com.subtracks.ui.components package com.subtracks.ui.components
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.DownloadDone import androidx.compose.material.icons.rounded.DownloadDone
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -38,34 +38,44 @@ fun ListDownloadIndicator(
val current = status ?: return val current = status ?: return
val kind = current.indicator() val kind = current.indicator()
if (kind == DownloadIndicator.Hidden) return if (kind == DownloadIndicator.Hidden) return
Surface( Box(modifier = modifier.size(20.dp), contentAlignment = Alignment.Center) {
shape = CircleShape, when (kind) {
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.85f), DownloadIndicator.Complete -> {
modifier = modifier.size(24.dp), Icon(
) { imageVector = Icons.Rounded.DownloadDone,
Box(contentAlignment = Alignment.Center) { contentDescription = null,
when (kind) { tint = ICON_SHADOW,
DownloadIndicator.Complete -> { modifier = Modifier.size(18.dp).blur(3.dp).offset(y = 1.dp),
Icon( )
imageVector = Icons.Rounded.DownloadDone, Icon(
contentDescription = DOWNLOADED_INDICATOR_TAG, imageVector = Icons.Rounded.DownloadDone,
tint = MaterialTheme.colorScheme.primary, contentDescription = DOWNLOADED_INDICATOR_TAG,
modifier = Modifier.size(16.dp), tint = Color.White,
) modifier = Modifier.size(18.dp),
} )
}
DownloadIndicator.InProgress -> { DownloadIndicator.InProgress -> {
CircularProgressIndicator( val progress = { current.downloaded.toFloat() / current.total }
progress = { current.downloaded.toFloat() / current.total }, CircularProgressIndicator(
strokeWidth = 2.dp, progress = progress,
modifier = Modifier.size(14.dp).semantics { contentDescription = DOWNLOADING_INDICATOR_TAG }, color = ICON_SHADOW,
) strokeWidth = 2.dp,
} modifier = Modifier.size(16.dp).blur(3.dp).offset(y = 1.dp),
)
CircularProgressIndicator(
progress = progress,
color = Color.White,
strokeWidth = 2.dp,
modifier = Modifier.size(16.dp).semantics { contentDescription = DOWNLOADING_INDICATOR_TAG },
)
}
DownloadIndicator.Hidden -> { DownloadIndicator.Hidden -> {
Unit Unit
}
} }
} }
} }
} }
private val ICON_SHADOW = Color.Black.copy(alpha = 0.45f)
@@ -18,8 +18,6 @@ import androidx.compose.material.icons.rounded.Cancel
import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Download import androidx.compose.material.icons.rounded.Download
import androidx.compose.material.icons.rounded.Person import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.material.icons.rounded.Shuffle
import androidx.compose.material.icons.rounded.SkipNext import androidx.compose.material.icons.rounded.SkipNext
import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarBorder import androidx.compose.material.icons.rounded.StarBorder
@@ -137,11 +135,6 @@ fun MenuTarget.listDownload(): ListDownloadStatus? =
} }
class ItemActions( class ItemActions(
val playSong: ((SongModel) -> Unit)? = null,
val playAlbum: (AlbumModel) -> Unit = {},
val shuffleAlbum: (AlbumModel) -> Unit = {},
val playPlaylist: (PlaylistModel) -> Unit = {},
val shufflePlaylist: (PlaylistModel) -> Unit = {},
val playNext: ((QueueRef) -> Unit)? = null, val playNext: ((QueueRef) -> Unit)? = null,
val addToQueue: ((QueueRef) -> Unit)? = null, val addToQueue: ((QueueRef) -> Unit)? = null,
val download: ((SongModel) -> Unit)? = null, val download: ((SongModel) -> Unit)? = null,
@@ -213,9 +206,6 @@ fun ItemContextMenu(
MenuHeader(target) MenuHeader(target)
when (target) { when (target) {
is MenuTarget.Song -> { is MenuTarget.Song -> {
actions.playSong?.let { play ->
MenuItem(Icons.Rounded.PlayArrow, "Play") { dismiss { play(target.song) } }
}
queueItems() queueItems()
DownloadItem(target.download, target.song, actions, ::dismiss) DownloadItem(target.download, target.song, actions, ::dismiss)
StarItem( StarItem(
@@ -240,8 +230,6 @@ fun ItemContextMenu(
} }
is MenuTarget.Album -> { is MenuTarget.Album -> {
MenuItem(Icons.Rounded.PlayArrow, "Play") { dismiss { actions.playAlbum(target.album) } }
MenuItem(Icons.Rounded.Shuffle, "Shuffle") { dismiss { actions.shuffleAlbum(target.album) } }
queueItems() queueItems()
BulkDownloadItem(target, actions, ::dismiss) BulkDownloadItem(target, actions, ::dismiss)
StarItem( StarItem(
@@ -268,8 +256,6 @@ fun ItemContextMenu(
} }
is MenuTarget.Playlist -> { is MenuTarget.Playlist -> {
MenuItem(Icons.Rounded.PlayArrow, "Play") { dismiss { actions.playPlaylist(target.playlist) } }
MenuItem(Icons.Rounded.Shuffle, "Shuffle") { dismiss { actions.shufflePlaylist(target.playlist) } }
queueItems() queueItems()
BulkDownloadItem(target, actions, ::dismiss) BulkDownloadItem(target, actions, ::dismiss)
} }
@@ -79,9 +79,6 @@ fun AlbumDetailRoute(
} }
val actions = val actions =
ItemActions( ItemActions(
playSong = { song -> songs.indexOfFirst { it.id == song.id }.takeIf { it >= 0 }?.let(viewModel::play) },
playAlbum = { viewModel.playAll() },
shuffleAlbum = { viewModel.shuffle() },
playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) }, playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) },
addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) }, addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) },
download = viewModel::download, download = viewModel::download,
@@ -154,8 +154,6 @@ fun ArtistDetailRoute(
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } } val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
val actions = val actions =
ItemActions( ItemActions(
playAlbum = { viewModel.playAlbum(it.id) },
shuffleAlbum = { viewModel.shuffleAlbum(it.id) },
playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) }, playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) },
addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) }, addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) },
bulkDownload = { target, action -> bulkDownload = { target, action ->
@@ -99,16 +99,4 @@ class ArtistDetailViewModel(
coverArt: String?, coverArt: String?,
thumbnail: Boolean, thumbnail: Boolean,
): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail) ): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail)
fun playAlbum(albumId: String) {
viewModelScope.launch {
playbackController.playAlbum(sourceId.first(), albumId, 0)
}
}
fun shuffleAlbum(albumId: String) {
viewModelScope.launch {
playbackController.shuffleAlbum(sourceId.first(), albumId)
}
}
} }
@@ -216,17 +216,11 @@ fun LibraryRoute(
} }
val playback by playbackController.state.collectAsStateWithLifecycle() val playback by playbackController.state.collectAsStateWithLifecycle()
val artwork = rememberArtworkColors(playbackController.coverArt(playback.item, thumbnail = true)) val artwork = rememberArtworkColors(playbackController.coverArt(playback.item, thumbnail = true))
val albumDownloads by viewModel.albumDownloads.collectAsStateWithLifecycle() val downloads by viewModel.downloads(listTab).collectAsStateWithLifecycle()
val artistDownloads by viewModel.artistDownloads.collectAsStateWithLifecycle()
val playlistDownloads by viewModel.playlistDownloads.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var pendingDelete by remember { mutableStateOf<PendingDownloadDelete?>(null) } var pendingDelete by remember { mutableStateOf<PendingDownloadDelete?>(null) }
val itemActions = val itemActions =
ItemActions( ItemActions(
playAlbum = { viewModel.playAlbum(it.id) },
shuffleAlbum = { viewModel.shuffleAlbum(it.id) },
playPlaylist = { viewModel.playPlaylist(it.id) },
shufflePlaylist = { viewModel.shufflePlaylist(it.id) },
playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) }, playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) },
addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) }, addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) },
bulkDownload = { target, action -> bulkDownload = { target, action ->
@@ -282,9 +276,9 @@ fun LibraryRoute(
}, },
search = search, search = search,
onSearchChange = { viewModel.setSearch(listTab, it) }, onSearchChange = { viewModel.setSearch(listTab, it) },
albumDownloads = albumDownloads, albumDownloads = downloads.takeIf { listTab == LibraryListTab.Albums }.orEmpty(),
artistDownloads = artistDownloads, artistDownloads = downloads.takeIf { listTab == LibraryListTab.Artists }.orEmpty(),
playlistDownloads = playlistDownloads, playlistDownloads = downloads.takeIf { listTab == LibraryListTab.Playlists }.orEmpty(),
) )
pendingDelete?.let { pending -> pendingDelete?.let { pending ->
@@ -95,21 +95,28 @@ class LibraryViewModel(
private val activeSourceId = libraryRepository.activeSourceId.filterNotNull() private val activeSourceId = libraryRepository.activeSourceId.filterNotNull()
val albumDownloads: StateFlow<Map<String, ListDownloadStatus>> = private val albumDownloads: StateFlow<Map<String, ListDownloadStatus>> =
activeSourceId activeSourceId
.flatMapLatest { downloadRepository.statuses(it, DownloadList.Album) } .flatMapLatest { downloadRepository.statuses(it, DownloadList.Album) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyMap()) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyMap())
val artistDownloads: StateFlow<Map<String, ListDownloadStatus>> = private val artistDownloads: StateFlow<Map<String, ListDownloadStatus>> =
activeSourceId activeSourceId
.flatMapLatest { downloadRepository.statuses(it, DownloadList.Artist) } .flatMapLatest { downloadRepository.statuses(it, DownloadList.Artist) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyMap()) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyMap())
val playlistDownloads: StateFlow<Map<String, ListDownloadStatus>> = private val playlistDownloads: StateFlow<Map<String, ListDownloadStatus>> =
activeSourceId activeSourceId
.flatMapLatest { downloadRepository.statuses(it, DownloadList.Playlist) } .flatMapLatest { downloadRepository.statuses(it, DownloadList.Playlist) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyMap()) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyMap())
fun downloads(tab: LibraryListTab): StateFlow<Map<String, ListDownloadStatus>> =
when (tab) {
LibraryListTab.Albums -> albumDownloads
LibraryListTab.Artists -> artistDownloads
LibraryListTab.Playlists -> playlistDownloads
}
suspend fun downloadedBytes( suspend fun downloadedBytes(
list: DownloadList, list: DownloadList,
refId: String, refId: String,
@@ -149,30 +156,6 @@ class LibraryViewModel(
thumbnail: Boolean, thumbnail: Boolean,
): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail) ): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail)
fun playAlbum(albumId: String) {
viewModelScope.launch {
sourceRepository.activeSourceIdOnce()?.let { playbackController.playAlbum(it, albumId, 0) }
}
}
fun shuffleAlbum(albumId: String) {
viewModelScope.launch {
sourceRepository.activeSourceIdOnce()?.let { playbackController.shuffleAlbum(it, albumId) }
}
}
fun playPlaylist(playlistId: String) {
viewModelScope.launch {
sourceRepository.activeSourceIdOnce()?.let { playbackController.playPlaylist(it, playlistId, 0) }
}
}
fun shufflePlaylist(playlistId: String) {
viewModelScope.launch {
sourceRepository.activeSourceIdOnce()?.let { playbackController.shufflePlaylist(it, playlistId) }
}
}
fun sync() = syncManager.requestSync() fun sync() = syncManager.requestSync()
} }
@@ -64,8 +64,6 @@ fun PlaylistDetailRoute(
} }
val actions = val actions =
ItemActions( ItemActions(
playPlaylist = { viewModel.playAll() },
shufflePlaylist = { viewModel.shuffle() },
playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) }, playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) },
addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) }, addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) },
download = viewModel::download, download = viewModel::download,