From d9e9a6b8656449608055acdf7ccb16dccc4d9a34 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sat, 26 Sep 2026 02:31:02 +0000 Subject: [PATCH] add item menus to the detail screens and now playing Album, artist and playlist detail screens get a more button plus long-press on their song and album rows; now playing gets a more button for the current song. Song menus can reach the album and artist and star; the now-playing song is read from Room so its star state is current. --- .../com/subtracks/data/db/LibraryDao.kt | 6 +++ .../subtracks/data/repo/LibraryRepository.kt | 5 +++ .../main/kotlin/com/subtracks/di/AppModule.kt | 2 +- .../main/kotlin/com/subtracks/ui/AppRoot.kt | 12 ++++++ .../ui/components/ItemContextMenu.kt | 22 +++++++---- .../subtracks/ui/library/AlbumDetailScreen.kt | 29 ++++++++++++++- .../ui/library/AlbumDetailViewModel.kt | 9 +++++ .../ui/library/ArtistDetailScreen.kt | 33 ++++++++++++++++- .../ui/library/ArtistDetailViewModel.kt | 26 ++++++++++++- .../ui/library/PlaylistDetailScreen.kt | 34 +++++++++++++++-- .../ui/library/PlaylistDetailViewModel.kt | 9 +++++ .../subtracks/ui/playback/NowPlayingScreen.kt | 37 ++++++++++++++++++- 12 files changed, 208 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt b/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt index 6156896c..f971de80 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt @@ -611,6 +611,12 @@ interface LibraryDao { artistId: String, ): Flow + @Query("SELECT * FROM songs WHERE sourceId = :sourceId AND id = :songId") + fun song( + sourceId: Long, + songId: String, + ): Flow + @Query("SELECT * FROM albums WHERE sourceId = :sourceId AND artistId = :artistId ORDER BY year DESC, name COLLATE NOCASE, id") fun albumsForArtist( sourceId: Long, diff --git a/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt b/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt index f2f6cc7d..fb699076 100644 --- a/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt +++ b/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt @@ -211,6 +211,11 @@ class LibraryRepository( artistId: String, ): Flow = db.libraryDao().artist(sourceId, artistId) + fun song( + sourceId: Long, + songId: String, + ): Flow = db.libraryDao().song(sourceId, songId) + fun artistAlbums( sourceId: Long, artistId: String, diff --git a/app/src/main/kotlin/com/subtracks/di/AppModule.kt b/app/src/main/kotlin/com/subtracks/di/AppModule.kt index 62a05dcf..bacbb952 100644 --- a/app/src/main/kotlin/com/subtracks/di/AppModule.kt +++ b/app/src/main/kotlin/com/subtracks/di/AppModule.kt @@ -50,7 +50,7 @@ fun appModule( viewModel { SettingsViewModel(get(), get()) } viewModel { AddSourceViewModel(get(), get()) } viewModel { params -> AlbumDetailViewModel(get(), get(), get(), params.get()) } - viewModel { params -> ArtistDetailViewModel(get(), get(), params.get()) } + viewModel { params -> ArtistDetailViewModel(get(), get(), get(), params.get()) } viewModel { params -> PlaylistDetailViewModel(get(), get(), get(), params.get()) } viewModel { QueueViewModel(get(), get()) } } diff --git a/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt b/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt index 80c98ef6..e15c527b 100644 --- a/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt +++ b/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt @@ -250,6 +250,7 @@ private fun MainNavigation() { .orEmpty() .ifEmpty { null }, onBack = { navController.popBackStack() }, + onViewArtist = { artistId -> navController.navigate(Routes.artist(artistId)) }, ) } composable( @@ -272,6 +273,7 @@ private fun MainNavigation() { .ifEmpty { null }, onBack = { navController.popBackStack() }, onAlbumClick = { album -> navController.navigate(Routes.album(album.id, album.coverArt)) }, + onViewAlbum = { albumId -> navController.navigate(Routes.album(albumId)) }, ) } composable( @@ -281,6 +283,8 @@ 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)) }, ) } composable(Routes.ADD_SERVER) { @@ -343,6 +347,14 @@ private fun MainNavigation() { NowPlayingRoute( onBack = { settleNowPlaying(false) }, onQueue = { showingQueue = true }, + onViewAlbum = { albumId -> + settleNowPlaying(false) + navController.navigate(Routes.album(albumId)) + }, + onViewArtist = { artistId -> + settleNowPlaying(false) + navController.navigate(Routes.artist(artistId)) + }, modifier = Modifier.fillMaxSize(), ) } diff --git a/app/src/main/kotlin/com/subtracks/ui/components/ItemContextMenu.kt b/app/src/main/kotlin/com/subtracks/ui/components/ItemContextMenu.kt index 88a4bd4e..d0b73329 100644 --- a/app/src/main/kotlin/com/subtracks/ui/components/ItemContextMenu.kt +++ b/app/src/main/kotlin/com/subtracks/ui/components/ItemContextMenu.kt @@ -64,14 +64,14 @@ sealed interface MenuTarget { } class ItemActions( - val playSong: (SongModel) -> Unit = {}, + val playSong: ((SongModel) -> Unit)? = null, val playAlbum: (AlbumModel) -> Unit = {}, val shuffleAlbum: (AlbumModel) -> Unit = {}, val playPlaylist: (PlaylistModel) -> Unit = {}, val shufflePlaylist: (PlaylistModel) -> Unit = {}, val setStar: (StarType, String, Boolean) -> Unit = { _, _, _ -> }, - val viewAlbum: (String) -> Unit = {}, - val viewArtist: (String) -> Unit = {}, + val viewAlbum: ((String) -> Unit)? = null, + val viewArtist: ((String) -> Unit)? = null, ) @OptIn(ExperimentalMaterial3Api::class) @@ -89,15 +89,21 @@ fun ItemContextMenu( MenuHeader(target) when (target) { is MenuTarget.Song -> { - MenuItem(Icons.Rounded.PlayArrow, "Play") { dismissThen { actions.playSong(target.song) } } + actions.playSong?.let { play -> + MenuItem(Icons.Rounded.PlayArrow, "Play") { dismissThen { play(target.song) } } + } MenuItem(starIcon(target.song.starred), starLabel(target.song.starred)) { dismissThen { actions.setStar(StarType.Song, target.song.id, target.song.starred == null) } } target.song.albumId?.let { albumId -> - MenuItem(Icons.Rounded.Album, "View album") { dismissThen { actions.viewAlbum(albumId) } } + actions.viewAlbum?.let { view -> + MenuItem(Icons.Rounded.Album, "View album") { dismissThen { view(albumId) } } + } } target.song.artistId?.let { artistId -> - MenuItem(Icons.Rounded.Person, "View artist") { dismissThen { actions.viewArtist(artistId) } } + actions.viewArtist?.let { view -> + MenuItem(Icons.Rounded.Person, "View artist") { dismissThen { view(artistId) } } + } } } @@ -108,7 +114,9 @@ fun ItemContextMenu( dismissThen { actions.setStar(StarType.Album, target.album.id, target.album.starred == null) } } target.album.artistId?.let { artistId -> - MenuItem(Icons.Rounded.Person, "View artist") { dismissThen { actions.viewArtist(artistId) } } + actions.viewArtist?.let { view -> + MenuItem(Icons.Rounded.Person, "View artist") { dismissThen { view(artistId) } } + } } } diff --git a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt index bb44498c..25d03dce 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt @@ -1,6 +1,6 @@ package com.subtracks.ui.library -import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding @@ -13,7 +13,9 @@ 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,6 +30,9 @@ import com.subtracks.data.model.Song import com.subtracks.playback.PlaybackController 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 import org.koin.compose.koinInject @@ -42,6 +47,7 @@ fun AlbumDetailRoute( albumId: String, coverArtId: String?, onBack: () -> Unit, + onViewArtist: (String) -> Unit, viewModel: AlbumDetailViewModel = koinViewModel(key = albumId) { parametersOf(albumId) }, playbackController: PlaybackController = koinInject(), ) { @@ -51,6 +57,15 @@ 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(null) } + val actions = + ItemActions( + playSong = { song -> songs.indexOfFirst { it.id == song.id }.takeIf { it >= 0 }?.let(viewModel::play) }, + playAlbum = { viewModel.playAll() }, + shuffleAlbum = { viewModel.shuffle() }, + setStar = viewModel::setStar, + viewArtist = onViewArtist, + ) AlbumDetailScreen( album = album, songs = songs, @@ -59,10 +74,15 @@ fun AlbumDetailRoute( artwork = rememberArtworkColors(shortcutArt ?: viewModel.coverArt(album?.coverArt, true), THEME_TRANSITION_MS), onBack = onBack, onSongClick = viewModel::play, + onSongLongClick = { menuTarget = it }, onShuffle = viewModel::shuffle, onPlay = viewModel::playAll, + onMore = { album?.let { menuTarget = MenuTarget.Album(it) } }, playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Album && context.refId == albumId }, ) + menuTarget?.let { target -> + ItemContextMenu(target = target, actions = actions, onDismiss = { menuTarget = null }) + } } @Composable @@ -74,6 +94,7 @@ fun AlbumDetailScreen( artwork: ArtworkColors?, onBack: () -> Unit, onSongClick: (Int) -> Unit, + onSongLongClick: (MenuTarget) -> Unit = {}, onShuffle: () -> Unit = {}, onPlay: () -> Unit = { onSongClick(0) }, onDownload: () -> Unit = {}, @@ -137,7 +158,11 @@ fun AlbumDetailScreen( song = song, isPlaying = song.id == playingSongId, trackNumber = song.track, - modifier = rowModifier.clickable { onSongClick(index) }, + modifier = + rowModifier.combinedClickable( + onClick = { onSongClick(index) }, + onLongClick = { onSongLongClick(MenuTarget.Song(song)) }, + ), ) } } diff --git a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailViewModel.kt index a5d089d9..5bf03370 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailViewModel.kt @@ -8,6 +8,7 @@ import com.subtracks.data.model.Disc import com.subtracks.data.model.Song import com.subtracks.data.repo.LibraryRepository import com.subtracks.data.repo.SourceRepository +import com.subtracks.data.source.StarType import com.subtracks.playback.PlaybackController import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -53,4 +54,12 @@ class AlbumDetailViewModel( playbackController.shuffleAlbum(sourceId.first(), albumId) } } + + fun setStar( + type: StarType, + id: String, + starred: Boolean, + ) { + viewModelScope.launch { libraryRepository.setStar(type, id, starred) } + } } diff --git a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt index b1124c8b..8cdeb610 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -27,6 +28,7 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.MoreHoriz import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -39,7 +41,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.derivedStateOf 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.draw.alpha @@ -58,6 +62,9 @@ import com.subtracks.data.model.Album import com.subtracks.data.model.Artist import com.subtracks.data.model.CoverArtRef 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 import com.subtracks.ui.theme.ArtworkTheme @@ -83,6 +90,7 @@ fun ArtistDetailRoute( coverArtId: String?, onBack: () -> Unit, onAlbumClick: (Album) -> Unit, + onViewAlbum: (String) -> Unit, viewModel: ArtistDetailViewModel = koinViewModel(key = artistId) { parametersOf(artistId) }, ) { val artist by viewModel.artist.collectAsStateWithLifecycle(initialValue = null) @@ -90,6 +98,14 @@ 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(null) } + val actions = + ItemActions( + playAlbum = { viewModel.playAlbum(it.id) }, + shuffleAlbum = { viewModel.shuffleAlbum(it.id) }, + setStar = viewModel::setStar, + viewAlbum = onViewAlbum, + ) ArtistDetailScreen( artist = artist, albums = albums, @@ -99,7 +115,12 @@ fun ArtistDetailRoute( coverArt = viewModel::coverArt, onBack = onBack, onAlbumClick = onAlbumClick, + onAlbumLongClick = { menuTarget = it }, + onMore = { artist?.let { menuTarget = MenuTarget.Artist(it) } }, ) + menuTarget?.let { target -> + ItemContextMenu(target = target, actions = actions, onDismiss = { menuTarget = null }) + } } @OptIn(ExperimentalMaterial3Api::class) @@ -113,6 +134,8 @@ fun ArtistDetailScreen( coverArt: (String?, Boolean) -> CoverArtRef?, onBack: () -> Unit, onAlbumClick: (Album) -> Unit, + onAlbumLongClick: (MenuTarget) -> Unit = {}, + onMore: () -> Unit = {}, modifier: Modifier = Modifier, ) { val listState = rememberLazyGridState() @@ -213,7 +236,10 @@ fun ArtistDetailScreen( .padding( start = if (index % 2 == 0) 16.dp else 0.dp, end = if (index % 2 == 1) 16.dp else 0.dp, - ).clickable { onAlbumClick(album) }, + ).combinedClickable( + onClick = { onAlbumClick(album) }, + onLongClick = { onAlbumLongClick(MenuTarget.Album(album)) }, + ), ) { CoverArt( ref = coverArt(album.coverArt, false), @@ -267,6 +293,11 @@ fun ArtistDetailScreen( Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back") } }, + actions = { + IconButton(onClick = onMore, modifier = Modifier.graphicsLayer { alpha = barFraction }) { + Icon(Icons.Rounded.MoreHoriz, contentDescription = "More options") + } + }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), modifier = Modifier diff --git a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailViewModel.kt index 1252aa8e..9d05b021 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailViewModel.kt @@ -7,6 +7,8 @@ import com.subtracks.data.model.Artist import com.subtracks.data.model.CoverArtRef import com.subtracks.data.repo.LibraryRepository import com.subtracks.data.repo.SourceRepository +import com.subtracks.data.source.StarType +import com.subtracks.playback.PlaybackController import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -14,13 +16,15 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch @OptIn(ExperimentalCoroutinesApi::class) class ArtistDetailViewModel( - libraryRepository: LibraryRepository, + private val libraryRepository: LibraryRepository, private val sourceRepository: SourceRepository, + private val playbackController: PlaybackController, private val artistId: String, ) : ViewModel() { private val sourceId = libraryRepository.activeSourceId.filterNotNull() @@ -63,4 +67,24 @@ class ArtistDetailViewModel( coverArt: String?, thumbnail: Boolean, ): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail) + + fun setStar( + type: StarType, + id: String, + starred: Boolean, + ) { + viewModelScope.launch { libraryRepository.setStar(type, id, starred) } + } + + fun playAlbum(albumId: String) { + viewModelScope.launch { + playbackController.playAlbum(sourceId.first(), albumId, 0) + } + } + + fun shuffleAlbum(albumId: String) { + viewModelScope.launch { + playbackController.shuffleAlbum(sourceId.first(), albumId) + } + } } diff --git a/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailScreen.kt index 06770828..acf113a4 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailScreen.kt @@ -1,8 +1,11 @@ package com.subtracks.ui.library -import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable 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.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.compose.LazyPagingItems @@ -15,6 +18,9 @@ import com.subtracks.data.model.SongListItem import com.subtracks.playback.PlaybackController 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 import org.koin.compose.koinInject @@ -28,12 +34,23 @@ private const val THEME_TRANSITION_MS = 100 fun PlaylistDetailRoute( playlistId: String, onBack: () -> Unit, + onViewAlbum: (String) -> Unit, + onViewArtist: (String) -> Unit, 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(null) } + val actions = + ItemActions( + playPlaylist = { viewModel.playAll() }, + shufflePlaylist = { viewModel.shuffle() }, + setStar = viewModel::setStar, + viewAlbum = onViewAlbum, + viewArtist = onViewArtist, + ) PlaylistDetailScreen( playlist = playlist, songs = viewModel.songs.collectAsLazyPagingItems(), @@ -41,10 +58,15 @@ fun PlaylistDetailRoute( artwork = rememberArtworkColors(viewModel.coverArt(playlist?.coverArt, true), THEME_TRANSITION_MS), onBack = onBack, onSongClick = viewModel::play, + onSongLongClick = { menuTarget = it }, onShuffle = viewModel::shuffle, onPlay = viewModel::playAll, + onMore = { playlist?.let { menuTarget = MenuTarget.Playlist(it) } }, playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Playlist && context.refId == playlistId }, ) + menuTarget?.let { target -> + ItemContextMenu(target = target, actions = actions, onDismiss = { menuTarget = null }) + } } @Composable @@ -55,8 +77,10 @@ fun PlaylistDetailScreen( artwork: ArtworkColors?, onBack: () -> Unit, onSongClick: (Int) -> Unit, + onSongLongClick: (MenuTarget) -> Unit = {}, onShuffle: () -> Unit = {}, onPlay: () -> Unit = { onSongClick(0) }, + onMore: () -> Unit = {}, playingSongId: String? = null, modifier: Modifier = Modifier, ) { @@ -75,7 +99,7 @@ fun PlaylistDetailScreen( onPlay = onPlay, onShuffle = onShuffle, onDownload = {}, - onMore = {}, + onMore = onMore, topInset = topInset, controlsModifier = controlsModifier, ) @@ -89,7 +113,11 @@ fun PlaylistDetailScreen( coverArtId = item.coverArt, coverArt = coverArt, isPlaying = item.song.id == playingSongId, - modifier = rowModifier.clickable { onSongClick(index) }, + modifier = + rowModifier.combinedClickable( + onClick = { onSongClick(index) }, + onLongClick = { onSongLongClick(MenuTarget.Song(item.song)) }, + ), ) } } diff --git a/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailViewModel.kt index c160d9a0..fa1d585f 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailViewModel.kt @@ -9,6 +9,7 @@ import com.subtracks.data.model.Playlist import com.subtracks.data.model.SongListItem import com.subtracks.data.repo.LibraryRepository import com.subtracks.data.repo.SourceRepository +import com.subtracks.data.source.StarType import com.subtracks.playback.PlaybackController import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -55,4 +56,12 @@ class PlaylistDetailViewModel( playbackController.shufflePlaylist(sourceId.first(), playlistId) } } + + fun setStar( + type: StarType, + id: String, + starred: Boolean, + ) { + viewModelScope.launch { libraryRepository.setStar(type, id, starred) } + } } diff --git a/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt b/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt index d1b936e1..dbfa12b7 100644 --- a/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt @@ -43,7 +43,9 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState 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 @@ -62,26 +64,45 @@ import coil3.request.CachePolicy import coil3.request.ImageRequest import com.subtracks.data.model.CoverArtRef import com.subtracks.data.model.QueueKind +import com.subtracks.data.model.Song +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.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 import com.subtracks.ui.theme.ArtworkTheme import com.subtracks.ui.theme.HeroGradient import com.subtracks.ui.theme.rememberArtworkColors +import kotlinx.coroutines.launch import org.koin.compose.koinInject @Composable fun NowPlayingRoute( onBack: () -> Unit, onQueue: () -> Unit, + onViewAlbum: (String) -> Unit, + onViewArtist: (String) -> Unit, modifier: Modifier = Modifier, controller: PlaybackController = koinInject(), + libraryRepository: LibraryRepository = koinInject(), ) { val state by controller.state.collectAsStateWithLifecycle() val positionMs by controller.positionMs.collectAsStateWithLifecycle() + val sourceId = state.context?.sourceId + val songId = state.item?.id + val song by + produceState(null, sourceId, songId) { + if (sourceId != null && songId != null) { + libraryRepository.song(sourceId, songId).collect { value = it } + } + } + var menuTarget by remember { mutableStateOf(null) } + val scope = rememberCoroutineScope() val context = LocalPlatformContext.current val art = controller.coverArt(state.item) val thumbnail = controller.coverArt(state.item, thumbnail = true) @@ -117,9 +138,22 @@ fun NowPlayingRoute( onPrevious = controller::previous, onShuffle = controller::toggleShuffle, onRepeat = controller::cycleRepeat, + onMore = { song?.let { menuTarget = MenuTarget.Song(it) } }, 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( @@ -159,6 +193,7 @@ fun NowPlayingScreen( onPrevious: () -> Unit, onShuffle: () -> Unit = {}, onRepeat: () -> Unit = {}, + onMore: () -> Unit = {}, onSeek: (Long) -> Unit, thumbnailRef: CoverArtRef? = null, modifier: Modifier = Modifier, @@ -395,7 +430,7 @@ fun NowPlayingScreen( modifier = Modifier.size(30.dp), ) } - IconButton(onClick = {}, modifier = Modifier.size(40.dp)) { + IconButton(onClick = onMore, modifier = Modifier.size(40.dp)) { Icon( imageVector = Icons.Rounded.MoreHoriz, contentDescription = "More",