mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user