address feedback: immediate navigation, now-playing fade, stack focus, sheet polish

- menu actions run before the sheet collapses, so tapping an album/artist navigates
  immediately instead of waiting for the dismiss; the sheet still animates closed
- host the sheet in the app shell so it survives the navigation it triggers
- now playing fades out (rather than sliding) when its menu navigates to an album/artist,
  and navigates first
- navigating to a detail screen already on top of the stack focuses it instead of pushing
  a duplicate
- drop the drag handle from both sheets, even the menu header's left/top margins, and add
  a bottom margin
This commit is contained in:
austinried
2026-10-04 00:29:59 +00:00
parent c28271590e
commit 5aea1d6b4e
7 changed files with 163 additions and 99 deletions
+72 -17
View File
@@ -60,6 +60,8 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.subtracks.data.repo.SourceRepository
import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.ContextMenuHost
import com.subtracks.ui.components.ItemContextMenu
import com.subtracks.ui.components.LoadingState
import com.subtracks.ui.components.statusBarScrim
import com.subtracks.ui.library.AlbumDetailRoute
@@ -93,6 +95,7 @@ class RootViewModel(
private const val NAVIGATION_DURATION_MS = 260
private const val OVERLAY_DURATION_MS = 200
private const val FADE_OUT_MS = 150
private const val EXPAND_FADE = 0.1f
private const val FLING_VELOCITY = 1000f
private const val MINI_PLAYER_ANIM_MS = 200
@@ -145,11 +148,16 @@ private fun MainNavigation() {
var nowPlayingOpen by rememberSaveable { mutableStateOf(false) }
var nowPlayingProgress by remember { mutableFloatStateOf(0f) }
var miniPlayerTopPx by remember { mutableFloatStateOf(0f) }
var nowPlayingFadeOut by remember { mutableStateOf(false) }
var settleJob by remember { mutableStateOf<Job?>(null) }
val scope = rememberCoroutineScope()
val contextMenuHost = remember { ContextMenuHost() }
fun settleNowPlaying(open: Boolean) {
if (open) nowPlayingOpen = true
if (open) {
nowPlayingOpen = true
nowPlayingFadeOut = false
}
settleJob?.cancel()
settleJob =
scope.launch {
@@ -159,6 +167,30 @@ private fun MainNavigation() {
if (!open) nowPlayingOpen = false
}
}
fun fadeNowPlaying() {
nowPlayingFadeOut = true
settleJob?.cancel()
settleJob =
scope.launch {
animate(nowPlayingProgress, 0f, animationSpec = tween(FADE_OUT_MS)) { value, _ ->
nowPlayingProgress = value
}
nowPlayingOpen = false
nowPlayingFadeOut = false
}
}
fun navigateDetail(
pattern: String,
argument: String,
value: String,
route: String,
) {
val top = navController.currentBackStackEntry
if (top?.destination?.route == pattern && top.arguments?.getString(argument) == value) return
navController.navigate(route)
}
LaunchedEffect(Unit) { playbackController.connect() }
LaunchedEffect(Unit) { if (nowPlayingOpen) nowPlayingProgress = 1f }
@@ -216,12 +248,21 @@ private fun MainNavigation() {
) {
composable(Routes.LIBRARY) {
LibraryRoute(
onAlbumClick = { album -> navController.navigate(Routes.album(album.id, album.coverArt)) },
onArtistClick = { artist -> navController.navigate(Routes.artist(artist.id, artist.coverArt)) },
onPlaylistClick = { playlist -> navController.navigate(Routes.playlist(playlist.id)) },
onAlbumClick = { album ->
navigateDetail(Routes.ALBUM_DETAIL, "albumId", album.id, Routes.album(album.id, album.coverArt))
},
onArtistClick = { artist ->
navigateDetail(Routes.ARTIST_DETAIL, "artistId", artist.id, Routes.artist(artist.id, artist.coverArt))
},
onPlaylistClick = { playlist ->
navigateDetail(Routes.PLAYLIST_DETAIL, "playlistId", playlist.id, Routes.playlist(playlist.id))
},
onOpenSettings = { navController.navigate(Routes.SETTINGS) },
onViewAlbum = { albumId -> navController.navigate(Routes.album(albumId)) },
onViewArtist = { artistId -> navController.navigate(Routes.artist(artistId)) },
onViewAlbum = { albumId -> navigateDetail(Routes.ALBUM_DETAIL, "albumId", albumId, Routes.album(albumId)) },
onViewArtist = { artistId ->
navigateDetail(Routes.ARTIST_DETAIL, "artistId", artistId, Routes.artist(artistId))
},
contextMenuHost = contextMenuHost,
bottomInset = bottomInset,
)
}
@@ -250,7 +291,10 @@ private fun MainNavigation() {
.orEmpty()
.ifEmpty { null },
onBack = { navController.popBackStack() },
onViewArtist = { artistId -> navController.navigate(Routes.artist(artistId)) },
onViewArtist = { artistId ->
navigateDetail(Routes.ARTIST_DETAIL, "artistId", artistId, Routes.artist(artistId))
},
contextMenuHost = contextMenuHost,
)
}
composable(
@@ -272,8 +316,11 @@ private fun MainNavigation() {
.orEmpty()
.ifEmpty { null },
onBack = { navController.popBackStack() },
onAlbumClick = { album -> navController.navigate(Routes.album(album.id, album.coverArt)) },
onViewAlbum = { albumId -> navController.navigate(Routes.album(albumId)) },
onAlbumClick = { album ->
navigateDetail(Routes.ALBUM_DETAIL, "albumId", album.id, Routes.album(album.id, album.coverArt))
},
onViewAlbum = { albumId -> navigateDetail(Routes.ALBUM_DETAIL, "albumId", albumId, Routes.album(albumId)) },
contextMenuHost = contextMenuHost,
)
}
composable(
@@ -283,8 +330,11 @@ private fun MainNavigation() {
PlaylistDetailRoute(
playlistId = entry.arguments?.getString("playlistId").orEmpty(),
onBack = { navController.popBackStack() },
onViewAlbum = { albumId -> navController.navigate(Routes.album(albumId)) },
onViewArtist = { artistId -> navController.navigate(Routes.artist(artistId)) },
onViewAlbum = { albumId -> navigateDetail(Routes.ALBUM_DETAIL, "albumId", albumId, Routes.album(albumId)) },
onViewArtist = { artistId ->
navigateDetail(Routes.ARTIST_DETAIL, "artistId", artistId, Routes.artist(artistId))
},
contextMenuHost = contextMenuHost,
)
}
composable(Routes.ADD_SERVER) {
@@ -330,8 +380,8 @@ private fun MainNavigation() {
Modifier
.fillMaxSize()
.graphicsLayer {
translationY = (1f - nowPlayingProgress) * miniPlayerTopPx
alpha = (nowPlayingProgress / EXPAND_FADE).coerceAtMost(1f)
translationY = if (nowPlayingFadeOut) 0f else (1f - nowPlayingProgress) * miniPlayerTopPx
alpha = if (nowPlayingFadeOut) nowPlayingProgress else (nowPlayingProgress / EXPAND_FADE).coerceAtMost(1f)
}.draggable(
orientation = Orientation.Vertical,
state =
@@ -348,13 +398,14 @@ private fun MainNavigation() {
onBack = { settleNowPlaying(false) },
onQueue = { showingQueue = true },
onViewAlbum = { albumId ->
settleNowPlaying(false)
navController.navigate(Routes.album(albumId))
navigateDetail(Routes.ALBUM_DETAIL, "albumId", albumId, Routes.album(albumId))
fadeNowPlaying()
},
onViewArtist = { artistId ->
settleNowPlaying(false)
navController.navigate(Routes.artist(artistId))
navigateDetail(Routes.ARTIST_DETAIL, "artistId", artistId, Routes.artist(artistId))
fadeNowPlaying()
},
contextMenuHost = contextMenuHost,
modifier = Modifier.fillMaxSize(),
)
}
@@ -389,6 +440,10 @@ private fun MainNavigation() {
)
}
}
contextMenuHost.target?.let { target ->
ItemContextMenu(target = target, actions = contextMenuHost.actions, onDismiss = { contextMenuHost.dismiss() })
}
}
}
@@ -1,7 +1,14 @@
package com.subtracks.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
@@ -25,6 +32,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
@@ -92,6 +100,25 @@ class ItemActions(
val viewArtist: ((String) -> Unit)? = null,
)
class ContextMenuHost {
var target by mutableStateOf<MenuTarget?>(null)
private set
var actions by mutableStateOf(ItemActions())
private set
fun show(
target: MenuTarget,
actions: ItemActions,
) {
this.target = target
this.actions = actions
}
fun dismiss() {
target = null
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemContextMenu(
@@ -103,9 +130,9 @@ fun ItemContextMenu(
val scope = rememberCoroutineScope()
fun dismiss(after: () -> Unit = {}) {
after()
scope.launch {
sheetState.hide()
after()
onDismiss()
}
}
@@ -113,6 +140,7 @@ fun ItemContextMenu(
ModalBottomSheet(
onDismissRequest = { dismiss() },
sheetState = sheetState,
dragHandle = null,
) {
MenuHeader(target)
when (target) {
@@ -159,39 +187,40 @@ fun ItemContextMenu(
MenuItem(Icons.Rounded.Shuffle, "Shuffle") { dismiss { actions.shufflePlaylist(target.playlist) } }
}
}
Spacer(Modifier.height(16.dp))
}
}
@Composable
private fun MenuHeader(target: MenuTarget) {
ListItem(
headlineContent = {
Row(
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CoverArt(
ref = target.coverArt,
name = target.title,
modifier = Modifier.size(48.dp).clip(if (target is MenuTarget.Artist) CircleShape else RoundedCornerShape(8.dp)),
)
Spacer(Modifier.width(16.dp))
Column(Modifier.weight(1f)) {
Text(
text = target.title,
style = MaterialTheme.typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
supportingContent =
target.subtitle?.takeIf { it.isNotBlank() }?.let { subtitle ->
{
Text(
text = subtitle,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
},
leadingContent = {
CoverArt(
ref = target.coverArt,
name = target.title,
modifier = Modifier.size(48.dp).clip(if (target is MenuTarget.Artist) CircleShape else RoundedCornerShape(8.dp)),
)
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
@Composable
@@ -13,9 +13,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
@@ -28,10 +26,10 @@ import com.subtracks.data.model.Disc
import com.subtracks.data.model.QueueKind
import com.subtracks.data.model.Song
import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.ContextMenuHost
import com.subtracks.ui.components.HeroDetailScaffold
import com.subtracks.ui.components.HeroHeader
import com.subtracks.ui.components.ItemActions
import com.subtracks.ui.components.ItemContextMenu
import com.subtracks.ui.components.MenuTarget
import com.subtracks.ui.theme.ArtworkColors
import com.subtracks.ui.theme.rememberArtworkColors
@@ -48,6 +46,7 @@ fun AlbumDetailRoute(
coverArtId: String?,
onBack: () -> Unit,
onViewArtist: (String) -> Unit,
contextMenuHost: ContextMenuHost? = null,
viewModel: AlbumDetailViewModel = koinViewModel(key = albumId) { parametersOf(albumId) },
playbackController: PlaybackController = koinInject(),
) {
@@ -57,7 +56,6 @@ fun AlbumDetailRoute(
val playback by playbackController.state.collectAsStateWithLifecycle()
val context = playback.context
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
var menuTarget by remember { mutableStateOf<MenuTarget?>(null) }
val actions =
ItemActions(
playSong = { song -> songs.indexOfFirst { it.id == song.id }.takeIf { it >= 0 }?.let(viewModel::play) },
@@ -74,15 +72,12 @@ fun AlbumDetailRoute(
artwork = rememberArtworkColors(shortcutArt ?: viewModel.coverArt(album?.coverArt, true), THEME_TRANSITION_MS),
onBack = onBack,
onSongClick = viewModel::play,
onSongLongClick = { menuTarget = it },
onSongLongClick = { contextMenuHost?.show(it, actions) },
onShuffle = viewModel::shuffle,
onPlay = viewModel::playAll,
onMore = { album?.let { menuTarget = MenuTarget.Album(it, viewModel.coverArt(it.coverArt, true)) } },
onMore = { album?.let { contextMenuHost?.show(MenuTarget.Album(it, viewModel.coverArt(it.coverArt, true)), actions) } },
playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Album && context.refId == albumId },
)
menuTarget?.let { target ->
ItemContextMenu(target = target, actions = actions, onDismiss = { menuTarget = null })
}
}
@Composable
@@ -61,9 +61,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.subtracks.data.model.Album
import com.subtracks.data.model.Artist
import com.subtracks.data.model.CoverArtRef
import com.subtracks.ui.components.ContextMenuHost
import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.components.ItemActions
import com.subtracks.ui.components.ItemContextMenu
import com.subtracks.ui.components.MenuTarget
import com.subtracks.ui.components.rememberViewportFill
import com.subtracks.ui.theme.ArtworkColors
@@ -91,6 +91,7 @@ fun ArtistDetailRoute(
onBack: () -> Unit,
onAlbumClick: (Album) -> Unit,
onViewAlbum: (String) -> Unit,
contextMenuHost: ContextMenuHost? = null,
viewModel: ArtistDetailViewModel = koinViewModel(key = artistId) { parametersOf(artistId) },
) {
val artist by viewModel.artist.collectAsStateWithLifecycle(initialValue = null)
@@ -98,7 +99,6 @@ fun ArtistDetailRoute(
val art by viewModel.art.collectAsStateWithLifecycle()
val artThumbnail by viewModel.artThumbnail.collectAsStateWithLifecycle()
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
var menuTarget by remember { mutableStateOf<MenuTarget?>(null) }
val actions =
ItemActions(
playAlbum = { viewModel.playAlbum(it.id) },
@@ -115,12 +115,9 @@ fun ArtistDetailRoute(
coverArt = viewModel::coverArt,
onBack = onBack,
onAlbumClick = onAlbumClick,
onAlbumLongClick = { menuTarget = it },
onMore = { artist?.let { menuTarget = MenuTarget.Artist(it, artThumbnail ?: art) } },
onAlbumLongClick = { contextMenuHost?.show(it, actions) },
onMore = { artist?.let { contextMenuHost?.show(MenuTarget.Artist(it, artThumbnail ?: art), actions) } },
)
menuTarget?.let { target ->
ItemContextMenu(target = target, actions = actions, onDismiss = { menuTarget = null })
}
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -114,8 +114,8 @@ import com.subtracks.data.prefs.PlaylistSort
import com.subtracks.data.prefs.SongSort
import com.subtracks.data.prefs.StarredFilter
import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.ContextMenuHost
import com.subtracks.ui.components.ItemActions
import com.subtracks.ui.components.ItemContextMenu
import com.subtracks.ui.components.MenuTarget
import com.subtracks.ui.components.statusBarScrim
import com.subtracks.ui.theme.ArtworkColors
@@ -194,13 +194,13 @@ fun LibraryRoute(
onOpenSettings: () -> Unit,
onViewAlbum: (String) -> Unit,
onViewArtist: (String) -> Unit,
contextMenuHost: ContextMenuHost? = null,
bottomInset: Dp,
viewModel: LibraryViewModel = koinViewModel(),
playbackController: PlaybackController = koinInject(),
) {
var selectedTab by rememberSaveable { mutableStateOf(LibraryTab.Albums) }
var previousTab by rememberSaveable { mutableStateOf(LibraryTab.Albums) }
var menuTarget by remember { mutableStateOf<MenuTarget?>(null) }
val syncing by viewModel.syncing.collectAsStateWithLifecycle()
val playingSongId by viewModel.playingSongId.collectAsStateWithLifecycle()
val listTab = selectedTab.listTab()
@@ -220,6 +220,17 @@ fun LibraryRoute(
}
val playback by playbackController.state.collectAsStateWithLifecycle()
val artwork = rememberArtworkColors(playbackController.coverArt(playback.item, thumbnail = true))
val itemActions =
ItemActions(
playSong = { viewModel.playSong(it.id) },
playAlbum = { viewModel.playAlbum(it.id) },
shuffleAlbum = { viewModel.shuffleAlbum(it.id) },
playPlaylist = { viewModel.playPlaylist(it.id) },
shufflePlaylist = { viewModel.shufflePlaylist(it.id) },
setStar = viewModel::setStar,
viewAlbum = onViewAlbum,
viewArtist = onViewArtist,
)
LibraryScreen(
selectedTab = selectedTab,
onTabSelected = { selectedTab = it },
@@ -237,7 +248,7 @@ fun LibraryRoute(
onSongClick = viewModel::playSong,
onSync = viewModel::sync,
onOpenSettings = onOpenSettings,
onItemLongClick = { menuTarget = it },
onItemLongClick = { contextMenuHost?.show(it, itemActions) },
playingSongId = playingSongId,
listQuery = listQuery,
resetKeys = resetKeys,
@@ -261,23 +272,6 @@ fun LibraryRoute(
search = search,
onSearchChange = { viewModel.setSearch(listTab, it) },
)
menuTarget?.let { target ->
ItemContextMenu(
target = target,
actions =
ItemActions(
playSong = { viewModel.playSong(it.id) },
playAlbum = { viewModel.playAlbum(it.id) },
shuffleAlbum = { viewModel.shuffleAlbum(it.id) },
playPlaylist = { viewModel.playPlaylist(it.id) },
shufflePlaylist = { viewModel.shufflePlaylist(it.id) },
setStar = viewModel::setStar,
viewAlbum = onViewAlbum,
viewArtist = onViewArtist,
),
onDismiss = { menuTarget = null },
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -531,7 +525,10 @@ fun LibraryScreen(
}
if (showOptions) {
ModalBottomSheet(onDismissRequest = { showOptions = false }) {
ModalBottomSheet(
onDismissRequest = { showOptions = false },
dragHandle = null,
) {
ListOptionsSheet(
listQuery = listQuery,
sortOptions = sortOptions,
@@ -16,10 +16,10 @@ import com.subtracks.data.model.Playlist
import com.subtracks.data.model.QueueKind
import com.subtracks.data.model.SongListItem
import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.ContextMenuHost
import com.subtracks.ui.components.HeroDetailScaffold
import com.subtracks.ui.components.HeroHeader
import com.subtracks.ui.components.ItemActions
import com.subtracks.ui.components.ItemContextMenu
import com.subtracks.ui.components.MenuTarget
import com.subtracks.ui.theme.ArtworkColors
import com.subtracks.ui.theme.rememberArtworkColors
@@ -36,13 +36,13 @@ fun PlaylistDetailRoute(
onBack: () -> Unit,
onViewAlbum: (String) -> Unit,
onViewArtist: (String) -> Unit,
contextMenuHost: ContextMenuHost? = null,
viewModel: PlaylistDetailViewModel = koinViewModel(key = playlistId) { parametersOf(playlistId) },
playbackController: PlaybackController = koinInject(),
) {
val playlist by viewModel.playlist.collectAsStateWithLifecycle(initialValue = null)
val playback by playbackController.state.collectAsStateWithLifecycle()
val context = playback.context
var menuTarget by remember { mutableStateOf<MenuTarget?>(null) }
val actions =
ItemActions(
playPlaylist = { viewModel.playAll() },
@@ -58,15 +58,12 @@ fun PlaylistDetailRoute(
artwork = rememberArtworkColors(viewModel.coverArt(playlist?.coverArt, true), THEME_TRANSITION_MS),
onBack = onBack,
onSongClick = viewModel::play,
onSongLongClick = { menuTarget = it },
onSongLongClick = { contextMenuHost?.show(it, actions) },
onShuffle = viewModel::shuffle,
onPlay = viewModel::playAll,
onMore = { playlist?.let { menuTarget = MenuTarget.Playlist(it, viewModel.coverArt(it.coverArt, true)) } },
onMore = { playlist?.let { contextMenuHost?.show(MenuTarget.Playlist(it, viewModel.coverArt(it.coverArt, true)), actions) } },
playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Playlist && context.refId == playlistId },
)
menuTarget?.let { target ->
ItemContextMenu(target = target, actions = actions, onDismiss = { menuTarget = null })
}
}
@Composable
@@ -69,9 +69,9 @@ import com.subtracks.data.repo.LibraryRepository
import com.subtracks.playback.PlaybackController
import com.subtracks.playback.PlaybackState
import com.subtracks.playback.RepeatMode
import com.subtracks.ui.components.ContextMenuHost
import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.components.ItemActions
import com.subtracks.ui.components.ItemContextMenu
import com.subtracks.ui.components.MenuTarget
import com.subtracks.ui.theme.ArtworkColors
import com.subtracks.ui.theme.ArtworkSeedCache
@@ -87,6 +87,7 @@ fun NowPlayingRoute(
onQueue: () -> Unit,
onViewAlbum: (String) -> Unit,
onViewArtist: (String) -> Unit,
contextMenuHost: ContextMenuHost? = null,
modifier: Modifier = Modifier,
controller: PlaybackController = koinInject(),
libraryRepository: LibraryRepository = koinInject(),
@@ -101,9 +102,14 @@ fun NowPlayingRoute(
libraryRepository.song(sourceId, songId).collect { value = it }
}
}
var menuTarget by remember { mutableStateOf<MenuTarget?>(null) }
val scope = rememberCoroutineScope()
val context = LocalPlatformContext.current
val scope = rememberCoroutineScope()
val actions =
ItemActions(
setStar = { type, id, starred -> scope.launch { libraryRepository.setStar(type, id, starred) } },
viewAlbum = onViewAlbum,
viewArtist = onViewArtist,
)
val art = controller.coverArt(state.item)
val thumbnail = controller.coverArt(state.item, thumbnail = true)
val queueContext = state.context
@@ -138,22 +144,10 @@ fun NowPlayingRoute(
onPrevious = controller::previous,
onShuffle = controller::toggleShuffle,
onRepeat = controller::cycleRepeat,
onMore = { song?.let { menuTarget = MenuTarget.Song(it, thumbnail) } },
onMore = { song?.let { contextMenuHost?.show(MenuTarget.Song(it, thumbnail), actions) } },
onSeek = controller::seekTo,
modifier = modifier,
)
menuTarget?.let { target ->
ItemContextMenu(
target = target,
actions =
ItemActions(
setStar = { type, id, starred -> scope.launch { libraryRepository.setStar(type, id, starred) } },
viewAlbum = onViewAlbum,
viewArtist = onViewArtist,
),
onDismiss = { menuTarget = null },
)
}
}
internal fun prefetchImage(