mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +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 androidx.navigation.navArgument
|
||||||
import com.subtracks.data.repo.SourceRepository
|
import com.subtracks.data.repo.SourceRepository
|
||||||
import com.subtracks.playback.PlaybackController
|
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.LoadingState
|
||||||
import com.subtracks.ui.components.statusBarScrim
|
import com.subtracks.ui.components.statusBarScrim
|
||||||
import com.subtracks.ui.library.AlbumDetailRoute
|
import com.subtracks.ui.library.AlbumDetailRoute
|
||||||
@@ -93,6 +95,7 @@ class RootViewModel(
|
|||||||
|
|
||||||
private const val NAVIGATION_DURATION_MS = 260
|
private const val NAVIGATION_DURATION_MS = 260
|
||||||
private const val OVERLAY_DURATION_MS = 200
|
private const val OVERLAY_DURATION_MS = 200
|
||||||
|
private const val FADE_OUT_MS = 150
|
||||||
private const val EXPAND_FADE = 0.1f
|
private const val EXPAND_FADE = 0.1f
|
||||||
private const val FLING_VELOCITY = 1000f
|
private const val FLING_VELOCITY = 1000f
|
||||||
private const val MINI_PLAYER_ANIM_MS = 200
|
private const val MINI_PLAYER_ANIM_MS = 200
|
||||||
@@ -145,11 +148,16 @@ private fun MainNavigation() {
|
|||||||
var nowPlayingOpen by rememberSaveable { mutableStateOf(false) }
|
var nowPlayingOpen by rememberSaveable { mutableStateOf(false) }
|
||||||
var nowPlayingProgress by remember { mutableFloatStateOf(0f) }
|
var nowPlayingProgress by remember { mutableFloatStateOf(0f) }
|
||||||
var miniPlayerTopPx by remember { mutableFloatStateOf(0f) }
|
var miniPlayerTopPx by remember { mutableFloatStateOf(0f) }
|
||||||
|
var nowPlayingFadeOut by remember { mutableStateOf(false) }
|
||||||
var settleJob by remember { mutableStateOf<Job?>(null) }
|
var settleJob by remember { mutableStateOf<Job?>(null) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val contextMenuHost = remember { ContextMenuHost() }
|
||||||
|
|
||||||
fun settleNowPlaying(open: Boolean) {
|
fun settleNowPlaying(open: Boolean) {
|
||||||
if (open) nowPlayingOpen = true
|
if (open) {
|
||||||
|
nowPlayingOpen = true
|
||||||
|
nowPlayingFadeOut = false
|
||||||
|
}
|
||||||
settleJob?.cancel()
|
settleJob?.cancel()
|
||||||
settleJob =
|
settleJob =
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -159,6 +167,30 @@ private fun MainNavigation() {
|
|||||||
if (!open) nowPlayingOpen = false
|
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) { playbackController.connect() }
|
||||||
LaunchedEffect(Unit) { if (nowPlayingOpen) nowPlayingProgress = 1f }
|
LaunchedEffect(Unit) { if (nowPlayingOpen) nowPlayingProgress = 1f }
|
||||||
|
|
||||||
@@ -216,12 +248,21 @@ private fun MainNavigation() {
|
|||||||
) {
|
) {
|
||||||
composable(Routes.LIBRARY) {
|
composable(Routes.LIBRARY) {
|
||||||
LibraryRoute(
|
LibraryRoute(
|
||||||
onAlbumClick = { album -> navController.navigate(Routes.album(album.id, album.coverArt)) },
|
onAlbumClick = { album ->
|
||||||
onArtistClick = { artist -> navController.navigate(Routes.artist(artist.id, artist.coverArt)) },
|
navigateDetail(Routes.ALBUM_DETAIL, "albumId", album.id, Routes.album(album.id, album.coverArt))
|
||||||
onPlaylistClick = { playlist -> navController.navigate(Routes.playlist(playlist.id)) },
|
},
|
||||||
|
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) },
|
onOpenSettings = { navController.navigate(Routes.SETTINGS) },
|
||||||
onViewAlbum = { albumId -> navController.navigate(Routes.album(albumId)) },
|
onViewAlbum = { albumId -> navigateDetail(Routes.ALBUM_DETAIL, "albumId", albumId, Routes.album(albumId)) },
|
||||||
onViewArtist = { artistId -> navController.navigate(Routes.artist(artistId)) },
|
onViewArtist = { artistId ->
|
||||||
|
navigateDetail(Routes.ARTIST_DETAIL, "artistId", artistId, Routes.artist(artistId))
|
||||||
|
},
|
||||||
|
contextMenuHost = contextMenuHost,
|
||||||
bottomInset = bottomInset,
|
bottomInset = bottomInset,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -250,7 +291,10 @@ private fun MainNavigation() {
|
|||||||
.orEmpty()
|
.orEmpty()
|
||||||
.ifEmpty { null },
|
.ifEmpty { null },
|
||||||
onBack = { navController.popBackStack() },
|
onBack = { navController.popBackStack() },
|
||||||
onViewArtist = { artistId -> navController.navigate(Routes.artist(artistId)) },
|
onViewArtist = { artistId ->
|
||||||
|
navigateDetail(Routes.ARTIST_DETAIL, "artistId", artistId, Routes.artist(artistId))
|
||||||
|
},
|
||||||
|
contextMenuHost = contextMenuHost,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
@@ -272,8 +316,11 @@ private fun MainNavigation() {
|
|||||||
.orEmpty()
|
.orEmpty()
|
||||||
.ifEmpty { null },
|
.ifEmpty { null },
|
||||||
onBack = { navController.popBackStack() },
|
onBack = { navController.popBackStack() },
|
||||||
onAlbumClick = { album -> navController.navigate(Routes.album(album.id, album.coverArt)) },
|
onAlbumClick = { album ->
|
||||||
onViewAlbum = { albumId -> navController.navigate(Routes.album(albumId)) },
|
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(
|
composable(
|
||||||
@@ -283,8 +330,11 @@ private fun MainNavigation() {
|
|||||||
PlaylistDetailRoute(
|
PlaylistDetailRoute(
|
||||||
playlistId = entry.arguments?.getString("playlistId").orEmpty(),
|
playlistId = entry.arguments?.getString("playlistId").orEmpty(),
|
||||||
onBack = { navController.popBackStack() },
|
onBack = { navController.popBackStack() },
|
||||||
onViewAlbum = { albumId -> navController.navigate(Routes.album(albumId)) },
|
onViewAlbum = { albumId -> navigateDetail(Routes.ALBUM_DETAIL, "albumId", albumId, Routes.album(albumId)) },
|
||||||
onViewArtist = { artistId -> navController.navigate(Routes.artist(artistId)) },
|
onViewArtist = { artistId ->
|
||||||
|
navigateDetail(Routes.ARTIST_DETAIL, "artistId", artistId, Routes.artist(artistId))
|
||||||
|
},
|
||||||
|
contextMenuHost = contextMenuHost,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Routes.ADD_SERVER) {
|
composable(Routes.ADD_SERVER) {
|
||||||
@@ -330,8 +380,8 @@ private fun MainNavigation() {
|
|||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
translationY = (1f - nowPlayingProgress) * miniPlayerTopPx
|
translationY = if (nowPlayingFadeOut) 0f else (1f - nowPlayingProgress) * miniPlayerTopPx
|
||||||
alpha = (nowPlayingProgress / EXPAND_FADE).coerceAtMost(1f)
|
alpha = if (nowPlayingFadeOut) nowPlayingProgress else (nowPlayingProgress / EXPAND_FADE).coerceAtMost(1f)
|
||||||
}.draggable(
|
}.draggable(
|
||||||
orientation = Orientation.Vertical,
|
orientation = Orientation.Vertical,
|
||||||
state =
|
state =
|
||||||
@@ -348,13 +398,14 @@ private fun MainNavigation() {
|
|||||||
onBack = { settleNowPlaying(false) },
|
onBack = { settleNowPlaying(false) },
|
||||||
onQueue = { showingQueue = true },
|
onQueue = { showingQueue = true },
|
||||||
onViewAlbum = { albumId ->
|
onViewAlbum = { albumId ->
|
||||||
settleNowPlaying(false)
|
navigateDetail(Routes.ALBUM_DETAIL, "albumId", albumId, Routes.album(albumId))
|
||||||
navController.navigate(Routes.album(albumId))
|
fadeNowPlaying()
|
||||||
},
|
},
|
||||||
onViewArtist = { artistId ->
|
onViewArtist = { artistId ->
|
||||||
settleNowPlaying(false)
|
navigateDetail(Routes.ARTIST_DETAIL, "artistId", artistId, Routes.artist(artistId))
|
||||||
navController.navigate(Routes.artist(artistId))
|
fadeNowPlaying()
|
||||||
},
|
},
|
||||||
|
contextMenuHost = contextMenuHost,
|
||||||
modifier = Modifier.fillMaxSize(),
|
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
|
package com.subtracks.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
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.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -25,6 +32,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -92,6 +100,25 @@ class ItemActions(
|
|||||||
val viewArtist: ((String) -> Unit)? = null,
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ItemContextMenu(
|
fun ItemContextMenu(
|
||||||
@@ -103,9 +130,9 @@ fun ItemContextMenu(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
fun dismiss(after: () -> Unit = {}) {
|
fun dismiss(after: () -> Unit = {}) {
|
||||||
|
after()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
sheetState.hide()
|
sheetState.hide()
|
||||||
after()
|
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,6 +140,7 @@ fun ItemContextMenu(
|
|||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = { dismiss() },
|
onDismissRequest = { dismiss() },
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
|
dragHandle = null,
|
||||||
) {
|
) {
|
||||||
MenuHeader(target)
|
MenuHeader(target)
|
||||||
when (target) {
|
when (target) {
|
||||||
@@ -159,39 +187,40 @@ fun ItemContextMenu(
|
|||||||
MenuItem(Icons.Rounded.Shuffle, "Shuffle") { dismiss { actions.shufflePlaylist(target.playlist) } }
|
MenuItem(Icons.Rounded.Shuffle, "Shuffle") { dismiss { actions.shufflePlaylist(target.playlist) } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MenuHeader(target: MenuTarget) {
|
private fun MenuHeader(target: MenuTarget) {
|
||||||
ListItem(
|
Row(
|
||||||
headlineContent = {
|
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(
|
||||||
text = target.title,
|
text = target.title,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
},
|
|
||||||
supportingContent =
|
|
||||||
target.subtitle?.takeIf { it.isNotBlank() }?.let { subtitle ->
|
target.subtitle?.takeIf { it.isNotBlank() }?.let { subtitle ->
|
||||||
{
|
Text(
|
||||||
Text(
|
text = subtitle,
|
||||||
text = subtitle,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
maxLines = 1,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
overflow = TextOverflow.Ellipsis,
|
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),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.QueueKind
|
||||||
import com.subtracks.data.model.Song
|
import com.subtracks.data.model.Song
|
||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
|
import com.subtracks.ui.components.ContextMenuHost
|
||||||
import com.subtracks.ui.components.HeroDetailScaffold
|
import com.subtracks.ui.components.HeroDetailScaffold
|
||||||
import com.subtracks.ui.components.HeroHeader
|
import com.subtracks.ui.components.HeroHeader
|
||||||
import com.subtracks.ui.components.ItemActions
|
import com.subtracks.ui.components.ItemActions
|
||||||
import com.subtracks.ui.components.ItemContextMenu
|
|
||||||
import com.subtracks.ui.components.MenuTarget
|
import com.subtracks.ui.components.MenuTarget
|
||||||
import com.subtracks.ui.theme.ArtworkColors
|
import com.subtracks.ui.theme.ArtworkColors
|
||||||
import com.subtracks.ui.theme.rememberArtworkColors
|
import com.subtracks.ui.theme.rememberArtworkColors
|
||||||
@@ -48,6 +46,7 @@ fun AlbumDetailRoute(
|
|||||||
coverArtId: String?,
|
coverArtId: String?,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onViewArtist: (String) -> Unit,
|
onViewArtist: (String) -> Unit,
|
||||||
|
contextMenuHost: ContextMenuHost? = null,
|
||||||
viewModel: AlbumDetailViewModel = koinViewModel(key = albumId) { parametersOf(albumId) },
|
viewModel: AlbumDetailViewModel = koinViewModel(key = albumId) { parametersOf(albumId) },
|
||||||
playbackController: PlaybackController = koinInject(),
|
playbackController: PlaybackController = koinInject(),
|
||||||
) {
|
) {
|
||||||
@@ -57,7 +56,6 @@ fun AlbumDetailRoute(
|
|||||||
val playback by playbackController.state.collectAsStateWithLifecycle()
|
val playback by playbackController.state.collectAsStateWithLifecycle()
|
||||||
val context = playback.context
|
val context = playback.context
|
||||||
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
|
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
|
||||||
var menuTarget by remember { mutableStateOf<MenuTarget?>(null) }
|
|
||||||
val actions =
|
val actions =
|
||||||
ItemActions(
|
ItemActions(
|
||||||
playSong = { song -> songs.indexOfFirst { it.id == song.id }.takeIf { it >= 0 }?.let(viewModel::play) },
|
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),
|
artwork = rememberArtworkColors(shortcutArt ?: viewModel.coverArt(album?.coverArt, true), THEME_TRANSITION_MS),
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onSongClick = viewModel::play,
|
onSongClick = viewModel::play,
|
||||||
onSongLongClick = { menuTarget = it },
|
onSongLongClick = { contextMenuHost?.show(it, actions) },
|
||||||
onShuffle = viewModel::shuffle,
|
onShuffle = viewModel::shuffle,
|
||||||
onPlay = viewModel::playAll,
|
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 },
|
playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Album && context.refId == albumId },
|
||||||
)
|
)
|
||||||
menuTarget?.let { target ->
|
|
||||||
ItemContextMenu(target = target, actions = actions, onDismiss = { menuTarget = null })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -61,9 +61,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import com.subtracks.data.model.Album
|
import com.subtracks.data.model.Album
|
||||||
import com.subtracks.data.model.Artist
|
import com.subtracks.data.model.Artist
|
||||||
import com.subtracks.data.model.CoverArtRef
|
import com.subtracks.data.model.CoverArtRef
|
||||||
|
import com.subtracks.ui.components.ContextMenuHost
|
||||||
import com.subtracks.ui.components.CoverArt
|
import com.subtracks.ui.components.CoverArt
|
||||||
import com.subtracks.ui.components.ItemActions
|
import com.subtracks.ui.components.ItemActions
|
||||||
import com.subtracks.ui.components.ItemContextMenu
|
|
||||||
import com.subtracks.ui.components.MenuTarget
|
import com.subtracks.ui.components.MenuTarget
|
||||||
import com.subtracks.ui.components.rememberViewportFill
|
import com.subtracks.ui.components.rememberViewportFill
|
||||||
import com.subtracks.ui.theme.ArtworkColors
|
import com.subtracks.ui.theme.ArtworkColors
|
||||||
@@ -91,6 +91,7 @@ fun ArtistDetailRoute(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onAlbumClick: (Album) -> Unit,
|
onAlbumClick: (Album) -> Unit,
|
||||||
onViewAlbum: (String) -> Unit,
|
onViewAlbum: (String) -> Unit,
|
||||||
|
contextMenuHost: ContextMenuHost? = null,
|
||||||
viewModel: ArtistDetailViewModel = koinViewModel(key = artistId) { parametersOf(artistId) },
|
viewModel: ArtistDetailViewModel = koinViewModel(key = artistId) { parametersOf(artistId) },
|
||||||
) {
|
) {
|
||||||
val artist by viewModel.artist.collectAsStateWithLifecycle(initialValue = null)
|
val artist by viewModel.artist.collectAsStateWithLifecycle(initialValue = null)
|
||||||
@@ -98,7 +99,6 @@ fun ArtistDetailRoute(
|
|||||||
val art by viewModel.art.collectAsStateWithLifecycle()
|
val art by viewModel.art.collectAsStateWithLifecycle()
|
||||||
val artThumbnail by viewModel.artThumbnail.collectAsStateWithLifecycle()
|
val artThumbnail by viewModel.artThumbnail.collectAsStateWithLifecycle()
|
||||||
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
|
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
|
||||||
var menuTarget by remember { mutableStateOf<MenuTarget?>(null) }
|
|
||||||
val actions =
|
val actions =
|
||||||
ItemActions(
|
ItemActions(
|
||||||
playAlbum = { viewModel.playAlbum(it.id) },
|
playAlbum = { viewModel.playAlbum(it.id) },
|
||||||
@@ -115,12 +115,9 @@ fun ArtistDetailRoute(
|
|||||||
coverArt = viewModel::coverArt,
|
coverArt = viewModel::coverArt,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onAlbumClick = onAlbumClick,
|
onAlbumClick = onAlbumClick,
|
||||||
onAlbumLongClick = { menuTarget = it },
|
onAlbumLongClick = { contextMenuHost?.show(it, actions) },
|
||||||
onMore = { artist?.let { menuTarget = MenuTarget.Artist(it, artThumbnail ?: art) } },
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ import com.subtracks.data.prefs.PlaylistSort
|
|||||||
import com.subtracks.data.prefs.SongSort
|
import com.subtracks.data.prefs.SongSort
|
||||||
import com.subtracks.data.prefs.StarredFilter
|
import com.subtracks.data.prefs.StarredFilter
|
||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
|
import com.subtracks.ui.components.ContextMenuHost
|
||||||
import com.subtracks.ui.components.ItemActions
|
import com.subtracks.ui.components.ItemActions
|
||||||
import com.subtracks.ui.components.ItemContextMenu
|
|
||||||
import com.subtracks.ui.components.MenuTarget
|
import com.subtracks.ui.components.MenuTarget
|
||||||
import com.subtracks.ui.components.statusBarScrim
|
import com.subtracks.ui.components.statusBarScrim
|
||||||
import com.subtracks.ui.theme.ArtworkColors
|
import com.subtracks.ui.theme.ArtworkColors
|
||||||
@@ -194,13 +194,13 @@ fun LibraryRoute(
|
|||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
onViewAlbum: (String) -> Unit,
|
onViewAlbum: (String) -> Unit,
|
||||||
onViewArtist: (String) -> Unit,
|
onViewArtist: (String) -> Unit,
|
||||||
|
contextMenuHost: ContextMenuHost? = null,
|
||||||
bottomInset: Dp,
|
bottomInset: Dp,
|
||||||
viewModel: LibraryViewModel = koinViewModel(),
|
viewModel: LibraryViewModel = koinViewModel(),
|
||||||
playbackController: PlaybackController = koinInject(),
|
playbackController: PlaybackController = koinInject(),
|
||||||
) {
|
) {
|
||||||
var selectedTab by rememberSaveable { mutableStateOf(LibraryTab.Albums) }
|
var selectedTab by rememberSaveable { mutableStateOf(LibraryTab.Albums) }
|
||||||
var previousTab 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 syncing by viewModel.syncing.collectAsStateWithLifecycle()
|
||||||
val playingSongId by viewModel.playingSongId.collectAsStateWithLifecycle()
|
val playingSongId by viewModel.playingSongId.collectAsStateWithLifecycle()
|
||||||
val listTab = selectedTab.listTab()
|
val listTab = selectedTab.listTab()
|
||||||
@@ -220,6 +220,17 @@ 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 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(
|
LibraryScreen(
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = { selectedTab = it },
|
onTabSelected = { selectedTab = it },
|
||||||
@@ -237,7 +248,7 @@ fun LibraryRoute(
|
|||||||
onSongClick = viewModel::playSong,
|
onSongClick = viewModel::playSong,
|
||||||
onSync = viewModel::sync,
|
onSync = viewModel::sync,
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
onItemLongClick = { menuTarget = it },
|
onItemLongClick = { contextMenuHost?.show(it, itemActions) },
|
||||||
playingSongId = playingSongId,
|
playingSongId = playingSongId,
|
||||||
listQuery = listQuery,
|
listQuery = listQuery,
|
||||||
resetKeys = resetKeys,
|
resetKeys = resetKeys,
|
||||||
@@ -261,23 +272,6 @@ fun LibraryRoute(
|
|||||||
search = search,
|
search = search,
|
||||||
onSearchChange = { viewModel.setSearch(listTab, it) },
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -531,7 +525,10 @@ fun LibraryScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showOptions) {
|
if (showOptions) {
|
||||||
ModalBottomSheet(onDismissRequest = { showOptions = false }) {
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = { showOptions = false },
|
||||||
|
dragHandle = null,
|
||||||
|
) {
|
||||||
ListOptionsSheet(
|
ListOptionsSheet(
|
||||||
listQuery = listQuery,
|
listQuery = listQuery,
|
||||||
sortOptions = sortOptions,
|
sortOptions = sortOptions,
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import com.subtracks.data.model.Playlist
|
|||||||
import com.subtracks.data.model.QueueKind
|
import com.subtracks.data.model.QueueKind
|
||||||
import com.subtracks.data.model.SongListItem
|
import com.subtracks.data.model.SongListItem
|
||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
|
import com.subtracks.ui.components.ContextMenuHost
|
||||||
import com.subtracks.ui.components.HeroDetailScaffold
|
import com.subtracks.ui.components.HeroDetailScaffold
|
||||||
import com.subtracks.ui.components.HeroHeader
|
import com.subtracks.ui.components.HeroHeader
|
||||||
import com.subtracks.ui.components.ItemActions
|
import com.subtracks.ui.components.ItemActions
|
||||||
import com.subtracks.ui.components.ItemContextMenu
|
|
||||||
import com.subtracks.ui.components.MenuTarget
|
import com.subtracks.ui.components.MenuTarget
|
||||||
import com.subtracks.ui.theme.ArtworkColors
|
import com.subtracks.ui.theme.ArtworkColors
|
||||||
import com.subtracks.ui.theme.rememberArtworkColors
|
import com.subtracks.ui.theme.rememberArtworkColors
|
||||||
@@ -36,13 +36,13 @@ fun PlaylistDetailRoute(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onViewAlbum: (String) -> Unit,
|
onViewAlbum: (String) -> Unit,
|
||||||
onViewArtist: (String) -> Unit,
|
onViewArtist: (String) -> Unit,
|
||||||
|
contextMenuHost: ContextMenuHost? = null,
|
||||||
viewModel: PlaylistDetailViewModel = koinViewModel(key = playlistId) { parametersOf(playlistId) },
|
viewModel: PlaylistDetailViewModel = koinViewModel(key = playlistId) { parametersOf(playlistId) },
|
||||||
playbackController: PlaybackController = koinInject(),
|
playbackController: PlaybackController = koinInject(),
|
||||||
) {
|
) {
|
||||||
val playlist by viewModel.playlist.collectAsStateWithLifecycle(initialValue = null)
|
val playlist by viewModel.playlist.collectAsStateWithLifecycle(initialValue = null)
|
||||||
val playback by playbackController.state.collectAsStateWithLifecycle()
|
val playback by playbackController.state.collectAsStateWithLifecycle()
|
||||||
val context = playback.context
|
val context = playback.context
|
||||||
var menuTarget by remember { mutableStateOf<MenuTarget?>(null) }
|
|
||||||
val actions =
|
val actions =
|
||||||
ItemActions(
|
ItemActions(
|
||||||
playPlaylist = { viewModel.playAll() },
|
playPlaylist = { viewModel.playAll() },
|
||||||
@@ -58,15 +58,12 @@ fun PlaylistDetailRoute(
|
|||||||
artwork = rememberArtworkColors(viewModel.coverArt(playlist?.coverArt, true), THEME_TRANSITION_MS),
|
artwork = rememberArtworkColors(viewModel.coverArt(playlist?.coverArt, true), THEME_TRANSITION_MS),
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onSongClick = viewModel::play,
|
onSongClick = viewModel::play,
|
||||||
onSongLongClick = { menuTarget = it },
|
onSongLongClick = { contextMenuHost?.show(it, actions) },
|
||||||
onShuffle = viewModel::shuffle,
|
onShuffle = viewModel::shuffle,
|
||||||
onPlay = viewModel::playAll,
|
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 },
|
playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Playlist && context.refId == playlistId },
|
||||||
)
|
)
|
||||||
menuTarget?.let { target ->
|
|
||||||
ItemContextMenu(target = target, actions = actions, onDismiss = { menuTarget = null })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ import com.subtracks.data.repo.LibraryRepository
|
|||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
import com.subtracks.playback.PlaybackState
|
import com.subtracks.playback.PlaybackState
|
||||||
import com.subtracks.playback.RepeatMode
|
import com.subtracks.playback.RepeatMode
|
||||||
|
import com.subtracks.ui.components.ContextMenuHost
|
||||||
import com.subtracks.ui.components.CoverArt
|
import com.subtracks.ui.components.CoverArt
|
||||||
import com.subtracks.ui.components.ItemActions
|
import com.subtracks.ui.components.ItemActions
|
||||||
import com.subtracks.ui.components.ItemContextMenu
|
|
||||||
import com.subtracks.ui.components.MenuTarget
|
import com.subtracks.ui.components.MenuTarget
|
||||||
import com.subtracks.ui.theme.ArtworkColors
|
import com.subtracks.ui.theme.ArtworkColors
|
||||||
import com.subtracks.ui.theme.ArtworkSeedCache
|
import com.subtracks.ui.theme.ArtworkSeedCache
|
||||||
@@ -87,6 +87,7 @@ fun NowPlayingRoute(
|
|||||||
onQueue: () -> Unit,
|
onQueue: () -> Unit,
|
||||||
onViewAlbum: (String) -> Unit,
|
onViewAlbum: (String) -> Unit,
|
||||||
onViewArtist: (String) -> Unit,
|
onViewArtist: (String) -> Unit,
|
||||||
|
contextMenuHost: ContextMenuHost? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
controller: PlaybackController = koinInject(),
|
controller: PlaybackController = koinInject(),
|
||||||
libraryRepository: LibraryRepository = koinInject(),
|
libraryRepository: LibraryRepository = koinInject(),
|
||||||
@@ -101,9 +102,14 @@ fun NowPlayingRoute(
|
|||||||
libraryRepository.song(sourceId, songId).collect { value = it }
|
libraryRepository.song(sourceId, songId).collect { value = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var menuTarget by remember { mutableStateOf<MenuTarget?>(null) }
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val context = LocalPlatformContext.current
|
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 art = controller.coverArt(state.item)
|
||||||
val thumbnail = controller.coverArt(state.item, thumbnail = true)
|
val thumbnail = controller.coverArt(state.item, thumbnail = true)
|
||||||
val queueContext = state.context
|
val queueContext = state.context
|
||||||
@@ -138,22 +144,10 @@ fun NowPlayingRoute(
|
|||||||
onPrevious = controller::previous,
|
onPrevious = controller::previous,
|
||||||
onShuffle = controller::toggleShuffle,
|
onShuffle = controller::toggleShuffle,
|
||||||
onRepeat = controller::cycleRepeat,
|
onRepeat = controller::cycleRepeat,
|
||||||
onMore = { song?.let { menuTarget = MenuTarget.Song(it, thumbnail) } },
|
onMore = { song?.let { contextMenuHost?.show(MenuTarget.Song(it, thumbnail), actions) } },
|
||||||
onSeek = controller::seekTo,
|
onSeek = controller::seekTo,
|
||||||
modifier = modifier,
|
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(
|
internal fun prefetchImage(
|
||||||
|
|||||||
Reference in New Issue
Block a user