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
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.DownloadDone
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
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.semantics
import androidx.compose.ui.unit.dp
@@ -38,34 +38,44 @@ fun ListDownloadIndicator(
val current = status ?: return
val kind = current.indicator()
if (kind == DownloadIndicator.Hidden) return
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.85f),
modifier = modifier.size(24.dp),
) {
Box(contentAlignment = Alignment.Center) {
when (kind) {
DownloadIndicator.Complete -> {
Icon(
imageVector = Icons.Rounded.DownloadDone,
contentDescription = DOWNLOADED_INDICATOR_TAG,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(16.dp),
)
}
Box(modifier = modifier.size(20.dp), contentAlignment = Alignment.Center) {
when (kind) {
DownloadIndicator.Complete -> {
Icon(
imageVector = Icons.Rounded.DownloadDone,
contentDescription = null,
tint = ICON_SHADOW,
modifier = Modifier.size(18.dp).blur(3.dp).offset(y = 1.dp),
)
Icon(
imageVector = Icons.Rounded.DownloadDone,
contentDescription = DOWNLOADED_INDICATOR_TAG,
tint = Color.White,
modifier = Modifier.size(18.dp),
)
}
DownloadIndicator.InProgress -> {
CircularProgressIndicator(
progress = { current.downloaded.toFloat() / current.total },
strokeWidth = 2.dp,
modifier = Modifier.size(14.dp).semantics { contentDescription = DOWNLOADING_INDICATOR_TAG },
)
}
DownloadIndicator.InProgress -> {
val progress = { current.downloaded.toFloat() / current.total }
CircularProgressIndicator(
progress = progress,
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 -> {
Unit
}
DownloadIndicator.Hidden -> {
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.Download
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.Star
import androidx.compose.material.icons.rounded.StarBorder
@@ -137,11 +135,6 @@ fun MenuTarget.listDownload(): ListDownloadStatus? =
}
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 addToQueue: ((QueueRef) -> Unit)? = null,
val download: ((SongModel) -> Unit)? = null,
@@ -213,9 +206,6 @@ fun ItemContextMenu(
MenuHeader(target)
when (target) {
is MenuTarget.Song -> {
actions.playSong?.let { play ->
MenuItem(Icons.Rounded.PlayArrow, "Play") { dismiss { play(target.song) } }
}
queueItems()
DownloadItem(target.download, target.song, actions, ::dismiss)
StarItem(
@@ -240,8 +230,6 @@ fun ItemContextMenu(
}
is MenuTarget.Album -> {
MenuItem(Icons.Rounded.PlayArrow, "Play") { dismiss { actions.playAlbum(target.album) } }
MenuItem(Icons.Rounded.Shuffle, "Shuffle") { dismiss { actions.shuffleAlbum(target.album) } }
queueItems()
BulkDownloadItem(target, actions, ::dismiss)
StarItem(
@@ -268,8 +256,6 @@ fun ItemContextMenu(
}
is MenuTarget.Playlist -> {
MenuItem(Icons.Rounded.PlayArrow, "Play") { dismiss { actions.playPlaylist(target.playlist) } }
MenuItem(Icons.Rounded.Shuffle, "Shuffle") { dismiss { actions.shufflePlaylist(target.playlist) } }
queueItems()
BulkDownloadItem(target, actions, ::dismiss)
}
@@ -79,9 +79,6 @@ fun AlbumDetailRoute(
}
val actions =
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) },
addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) },
download = viewModel::download,
@@ -154,8 +154,6 @@ fun ArtistDetailRoute(
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
val actions =
ItemActions(
playAlbum = { viewModel.playAlbum(it.id) },
shuffleAlbum = { viewModel.shuffleAlbum(it.id) },
playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) },
addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) },
bulkDownload = { target, action ->
@@ -99,16 +99,4 @@ class ArtistDetailViewModel(
coverArt: String?,
thumbnail: Boolean,
): 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 artwork = rememberArtworkColors(playbackController.coverArt(playback.item, thumbnail = true))
val albumDownloads by viewModel.albumDownloads.collectAsStateWithLifecycle()
val artistDownloads by viewModel.artistDownloads.collectAsStateWithLifecycle()
val playlistDownloads by viewModel.playlistDownloads.collectAsStateWithLifecycle()
val downloads by viewModel.downloads(listTab).collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
var pendingDelete by remember { mutableStateOf<PendingDownloadDelete?>(null) }
val 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) },
addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) },
bulkDownload = { target, action ->
@@ -282,9 +276,9 @@ fun LibraryRoute(
},
search = search,
onSearchChange = { viewModel.setSearch(listTab, it) },
albumDownloads = albumDownloads,
artistDownloads = artistDownloads,
playlistDownloads = playlistDownloads,
albumDownloads = downloads.takeIf { listTab == LibraryListTab.Albums }.orEmpty(),
artistDownloads = downloads.takeIf { listTab == LibraryListTab.Artists }.orEmpty(),
playlistDownloads = downloads.takeIf { listTab == LibraryListTab.Playlists }.orEmpty(),
)
pendingDelete?.let { pending ->
@@ -95,21 +95,28 @@ class LibraryViewModel(
private val activeSourceId = libraryRepository.activeSourceId.filterNotNull()
val albumDownloads: StateFlow<Map<String, ListDownloadStatus>> =
private val albumDownloads: StateFlow<Map<String, ListDownloadStatus>> =
activeSourceId
.flatMapLatest { downloadRepository.statuses(it, DownloadList.Album) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyMap())
val artistDownloads: StateFlow<Map<String, ListDownloadStatus>> =
private val artistDownloads: StateFlow<Map<String, ListDownloadStatus>> =
activeSourceId
.flatMapLatest { downloadRepository.statuses(it, DownloadList.Artist) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyMap())
val playlistDownloads: StateFlow<Map<String, ListDownloadStatus>> =
private val playlistDownloads: StateFlow<Map<String, ListDownloadStatus>> =
activeSourceId
.flatMapLatest { downloadRepository.statuses(it, DownloadList.Playlist) }
.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(
list: DownloadList,
refId: String,
@@ -149,30 +156,6 @@ class LibraryViewModel(
thumbnail: Boolean,
): 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()
}
@@ -64,8 +64,6 @@ fun PlaylistDetailRoute(
}
val actions =
ItemActions(
playPlaylist = { viewModel.playAll() },
shufflePlaylist = { viewModel.shuffle() },
playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) },
addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) },
download = viewModel::download,