diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/FloatingActionsContainer.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/FloatingActionsContainer.kt index 0aeb440..695dc1d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/FloatingActionsContainer.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/FloatingActionsContainer.kt @@ -60,7 +60,8 @@ fun BoxScope.FloatingActionsContainerWithScrollToTop( ) { val transitionState = remember { MutableTransitionState(ScrollingInfo()) - }.apply { targetState = if (visible) lazyListState.scrollingInfo() else null } + }.apply { targetState = lazyListState.scrollingInfo() } +// }.apply { targetState = if (visible) lazyListState.scrollingInfo() else null } FloatingActions( transitionState = transitionState, @@ -125,7 +126,7 @@ fun BoxScope.FloatingActions( onScrollToTop() } }, - enabled = transition.targetState?.isScrollingDown == false && transition.targetState?.isFar == true, +// enabled = transition.targetState?.isScrollingDown == false && transition.targetState?.isFar == true, iconId = R.drawable.chevron_up, modifier = Modifier .padding(bottom = 16.dp) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt index 76e7936..ac25051 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt @@ -2,20 +2,23 @@ package it.vfsfitvnm.vimusic.ui.components.themed import android.content.Intent import android.text.format.DateUtils +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentScope -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween import androidx.compose.animation.with +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -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.requiredHeight import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.BasicText @@ -30,11 +33,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip -import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.media3.common.MediaItem -import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R @@ -42,13 +47,17 @@ import it.vfsfitvnm.vimusic.enums.PlaylistSortBy import it.vfsfitvnm.vimusic.enums.SortOrder import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.Playlist +import it.vfsfitvnm.vimusic.models.Song import it.vfsfitvnm.vimusic.models.SongPlaylistMap import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.transaction +import it.vfsfitvnm.vimusic.ui.items.SongItem import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.artistRoute -import it.vfsfitvnm.vimusic.ui.screens.viewPlaylistsRoute +import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.ui.styling.favoritesIcon +import it.vfsfitvnm.vimusic.ui.styling.px import it.vfsfitvnm.vimusic.utils.addNext import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.enqueue @@ -56,27 +65,9 @@ import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf -@ExperimentalAnimationApi -@Composable -fun InFavoritesMediaItemMenu( - onDismiss: () -> Unit, - song: DetailedSong, - modifier: Modifier = Modifier, -) { - NonQueuedMediaItemMenu( - mediaItem = song.asMediaItem, - onDismiss = onDismiss, - onRemoveFromFavorites = { - query { - Database.like(song.id, null) - } - }, - modifier = modifier - ) -} - @ExperimentalAnimationApi @Composable fun InHistoryMediaItemMenu( @@ -143,7 +134,6 @@ fun NonQueuedMediaItemMenu( modifier: Modifier = Modifier, onRemoveFromPlaylist: (() -> Unit)? = null, onHideFromDatabase: (() -> Unit)? = null, - onRemoveFromFavorites: (() -> Unit)? = null, ) { val binder = LocalPlayerServiceBinder.current @@ -164,7 +154,6 @@ fun NonQueuedMediaItemMenu( onEnqueue = { binder?.player?.enqueue(mediaItem) }, onRemoveFromPlaylist = onRemoveFromPlaylist, onHideFromDatabase = onHideFromDatabase, - onRemoveFromFavorites = onRemoveFromFavorites, modifier = modifier ) } @@ -203,7 +192,6 @@ fun BaseMediaItemMenu( onRemoveFromQueue: (() -> Unit)? = null, onRemoveFromPlaylist: (() -> Unit)? = null, onHideFromDatabase: (() -> Unit)? = null, - onRemoveFromFavorites: (() -> Unit)? = null ) { val context = LocalContext.current @@ -228,7 +216,6 @@ fun BaseMediaItemMenu( } }, onHideFromDatabase = onHideFromDatabase, - onRemoveFromFavorites = onRemoveFromFavorites, onRemoveFromPlaylist = onRemoveFromPlaylist, onRemoveFromQueue = onRemoveFromQueue, onGoToAlbum = albumRoute::global, @@ -262,385 +249,425 @@ fun MediaItemMenu( onEnqueue: (() -> Unit)? = null, onHideFromDatabase: (() -> Unit)? = null, onRemoveFromQueue: (() -> Unit)? = null, - onRemoveFromFavorites: (() -> Unit)? = null, onRemoveFromPlaylist: (() -> Unit)? = null, onAddToPlaylist: ((Playlist, Int) -> Unit)? = null, onGoToAlbum: ((String) -> Unit)? = null, onGoToArtist: ((String) -> Unit)? = null, - onShare: (() -> Unit)? = null + onShare: () -> Unit ) { - Menu(modifier = modifier) { - RouteHandler( - transitionSpec = { - when (targetState.route) { - viewPlaylistsRoute -> slideIntoContainer(AnimatedContentScope.SlideDirection.Left) with - slideOutOfContainer(AnimatedContentScope.SlideDirection.Left) + val (colorPalette) = LocalAppearance.current + val density = LocalDensity.current - else -> when (initialState.route) { - viewPlaylistsRoute -> slideIntoContainer(AnimatedContentScope.SlideDirection.Right) with - slideOutOfContainer(AnimatedContentScope.SlideDirection.Right) + var isViewingPlaylists by remember { + mutableStateOf(false) + } - else -> EnterTransition.None with ExitTransition.None + var height by remember { + mutableStateOf(0.dp) + } + + val likedAt by remember(mediaItem.mediaId) { + Database.likedAt(mediaItem.mediaId).distinctUntilChanged() + }.collectAsState(initial = null, context = Dispatchers.IO) + + AnimatedContent( + targetState = isViewingPlaylists, + transitionSpec = { + val animationSpec = tween(400) + val slideDirection = if (targetState) AnimatedContentScope.SlideDirection.Left else AnimatedContentScope.SlideDirection.Right + + slideIntoContainer(slideDirection, animationSpec) with + slideOutOfContainer(slideDirection, animationSpec) + } + ) { currentIsViewingPlaylists -> + if (currentIsViewingPlaylists) { + val playlistPreviews by remember { + Database.playlistPreviews(PlaylistSortBy.DateAdded, SortOrder.Descending) + }.collectAsState(initial = emptyList(), context = Dispatchers.IO) + + var isCreatingNewPlaylist by rememberSaveable { + mutableStateOf(false) + } + + if (isCreatingNewPlaylist && onAddToPlaylist != null) { + TextFieldDialog( + hintText = "Enter the playlist name", + onDismiss = { isCreatingNewPlaylist = false }, + onDone = { text -> + onDismiss() + onAddToPlaylist(Playlist(name = text), 0) + } + ) + } + + BackHandler { + isViewingPlaylists = false + } + + Menu( + modifier = modifier + .requiredHeight(height) + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth() + ) { + IconButton( + onClick = { isViewingPlaylists = false }, + icon = R.drawable.chevron_back, + color = colorPalette.textSecondary, + modifier = Modifier + .padding(all = 4.dp) + .size(20.dp) + ) + + if (onAddToPlaylist != null) { + SecondaryTextButton( + text = "New playlist", + onClick = { isCreatingNewPlaylist = true }, + alternative = true + ) + } + } + + onAddToPlaylist?.let { onAddToPlaylist -> + playlistPreviews.forEach { playlistPreview -> + MenuEntry( + icon = R.drawable.playlist, + text = playlistPreview.playlist.name, + secondaryText = "${playlistPreview.songCount} songs", + onClick = { + onDismiss() + onAddToPlaylist( + playlistPreview.playlist, + playlistPreview.songCount + ) + } + ) } } } - ) { - viewPlaylistsRoute { - val playlistPreviews by remember { - Database.playlistPreviews(PlaylistSortBy.DateAdded, SortOrder.Descending) - }.collectAsState(initial = emptyList(), context = Dispatchers.IO) + } else { + Menu( + modifier = modifier + .onPlaced { height = with(density) { it.size.height.toDp() } } + ) { + val thumbnailSizeDp = Dimensions.thumbnails.song + val thumbnailSizePx = thumbnailSizeDp.px - var isCreatingNewPlaylist by rememberSaveable { - mutableStateOf(false) - } + SongItem( + song = mediaItem, + thumbnailSizeDp = thumbnailSizeDp, + thumbnailSizePx = thumbnailSizePx, + trailingContent = { + IconButton( + icon = if (likedAt == null) R.drawable.heart_outline else R.drawable.heart, + color = colorPalette.favoritesIcon, + onClick = { + query { + if (Database.like( + mediaItem.mediaId, + if (likedAt == null) System.currentTimeMillis() else null + ) == 0 + ) { + Database.insert(mediaItem, Song::toggleLike) + } + } + }, + modifier = Modifier + .padding(all = 4.dp) + .size(18.dp) + ) + }, + modifier = Modifier + .clickable(onClick = onShare) + ) - if (isCreatingNewPlaylist && onAddToPlaylist != null) { - TextFieldDialog( - hintText = "Enter the playlist name", - onDismiss = { - isCreatingNewPlaylist = false - }, - onDone = { text -> + Spacer( + modifier = Modifier + .height(8.dp) + ) + + Spacer( + modifier = Modifier + .alpha(0.5f) + .align(Alignment.CenterHorizontally) + .background(colorPalette.textDisabled) + .height(1.dp) + .fillMaxWidth(1f) + ) + + Spacer( + modifier = Modifier + .height(8.dp) + ) + + onStartRadio?.let { onStartRadio -> + MenuEntry( + icon = R.drawable.radio, + text = "Start radio", + onClick = { onDismiss() - onAddToPlaylist(Playlist(name = text), 0) + onStartRadio() } ) } - Column { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - ) { - MenuBackButton(onClick = pop) + onPlayNext?.let { onPlayNext -> + MenuEntry( + icon = R.drawable.play_skip_forward, + text = "Play next", + onClick = { + onDismiss() + onPlayNext() + } + ) + } - if (onAddToPlaylist != null) { - MenuIconButton( - icon = R.drawable.add, - onClick = { - isCreatingNewPlaylist = true + onEnqueue?.let { onEnqueue -> + MenuEntry( + icon = R.drawable.enqueue, + text = "Enqueue", + onClick = { + onDismiss() + onEnqueue() + } + ) + } + + onGoToEqualizer?.let { onGoToEqualizer -> + MenuEntry( + icon = R.drawable.equalizer, + text = "Equalizer", + onClick = { + onDismiss() + onGoToEqualizer() + } + ) + } + + // TODO: find solution to this shit + onShowSleepTimer?.let { + val binder = LocalPlayerServiceBinder.current + val (_, typography) = LocalAppearance.current + + var isShowingSleepTimerDialog by remember { + mutableStateOf(false) + } + + val sleepTimerMillisLeft by (binder?.sleepTimerMillisLeft + ?: flowOf(null)) + .collectAsState(initial = null) + + if (isShowingSleepTimerDialog) { + if (sleepTimerMillisLeft != null) { + ConfirmationDialog( + text = "Do you want to stop the sleep timer?", + cancelText = "No", + confirmText = "Stop", + onDismiss = { + isShowingSleepTimerDialog = false + onDismiss() + }, + onConfirm = { + binder?.cancelSleepTimer() + onDismiss() } ) + } else { + DefaultDialog(onDismiss = { + isShowingSleepTimerDialog = false + }) { + var amount by remember { + mutableStateOf(1) + } + + BasicText( + text = "Set sleep timer", + style = typography.s.semiBold, + modifier = Modifier + .padding(vertical = 8.dp, horizontal = 24.dp) + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy( + space = 16.dp, + alignment = Alignment.CenterHorizontally + ), + modifier = Modifier + .padding(vertical = 16.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .alpha(if (amount <= 1) 0.5f else 1f) + .clip(CircleShape) + .clickable(enabled = amount > 1) { amount-- } + .size(48.dp) + .background(colorPalette.background0) + ) { + BasicText( + text = "-", + style = typography.xs.semiBold + ) + } + + Box(contentAlignment = Alignment.Center) { + BasicText( + text = "88h 88m", + style = typography.s.semiBold, + modifier = Modifier + .alpha(0f) + ) + BasicText( + text = "${amount / 6}h ${(amount % 6) * 10}m", + style = typography.s.semiBold + ) + } + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .alpha(if (amount >= 60) 0.5f else 1f) + .clip(CircleShape) + .clickable(enabled = amount < 60) { amount++ } + .size(48.dp) + .background(colorPalette.background0) + ) { + BasicText( + text = "+", + style = typography.xs.semiBold + ) + } + } + + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + modifier = Modifier + .fillMaxWidth() + ) { + DialogTextButton( + text = "Cancel", + onClick = { + isShowingSleepTimerDialog = false + onDismiss() + } + ) + + DialogTextButton( + text = "Set", + enabled = amount > 0, + onClick = { + binder?.startSleepTimer(amount * 10 * 60 * 1000L) + isShowingSleepTimerDialog = false + onDismiss() + } + ) + } + } } } - onAddToPlaylist?.let { onAddToPlaylist -> - if (onRemoveFromFavorites == null) { - MenuEntry( - icon = R.drawable.heart, - text = "Favorites", - onClick = { - onDismiss() - query { - Database.insert(mediaItem) - Database.like(mediaItem.mediaId, System.currentTimeMillis()) - } - } - ) + MenuEntry( + icon = R.drawable.alarm, + text = "Sleep timer", + secondaryText = sleepTimerMillisLeft?.let { + "${ + DateUtils.formatElapsedTime( + it / 1000 + ) + } left" + }, + onClick = { + isShowingSleepTimerDialog = true } + ) + } - playlistPreviews.forEach { playlistPreview -> - MenuEntry( - icon = R.drawable.playlist, - text = playlistPreview.playlist.name, - secondaryText = "${playlistPreview.songCount} songs", - onClick = { - onDismiss() - onAddToPlaylist( - playlistPreview.playlist, - playlistPreview.songCount - ) - } + if (onAddToPlaylist != null) { + MenuEntry( + icon = R.drawable.playlist, + text = "Add to playlist", + onClick = { isViewingPlaylists = true }, + trailingContent = { + Image( + painter = painterResource(R.drawable.chevron_forward), + contentDescription = null, + colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(colorPalette.textSecondary), + modifier = Modifier + .size(16.dp) ) } + ) + } + + onGoToAlbum?.let { onGoToAlbum -> + mediaItem.mediaMetadata.extras?.getString("albumId")?.let { albumId -> + MenuEntry( + icon = R.drawable.disc, + text = "Go to album", + onClick = { + onDismiss() + onGoToAlbum(albumId) + } + ) } } - } - host { - Column( - modifier = Modifier - .pointerInput(Unit) { - detectTapGestures { } - } - ) { - onStartRadio?.let { onStartRadio -> - MenuEntry( - icon = R.drawable.radio, - text = "Start radio", - onClick = { - onDismiss() - onStartRadio() - } - ) - } - - onPlayNext?.let { onPlayNext -> - MenuEntry( - icon = R.drawable.play_skip_forward, - text = "Play next", - onClick = { - onDismiss() - onPlayNext() - } - ) - } - - onEnqueue?.let { onEnqueue -> - MenuEntry( - icon = R.drawable.enqueue, - text = "Enqueue", - onClick = { - onDismiss() - onEnqueue() - } - ) - } - - onGoToEqualizer?.let { onGoToEqualizer -> - MenuEntry( - icon = R.drawable.equalizer, - text = "Equalizer", - onClick = { - onDismiss() - onGoToEqualizer() - } - ) - } - - // TODO: find solution to this shit - onShowSleepTimer?.let { - val binder = LocalPlayerServiceBinder.current - val (colorPalette, typography) = LocalAppearance.current - - var isShowingSleepTimerDialog by remember { - mutableStateOf(false) - } - - val sleepTimerMillisLeft by (binder?.sleepTimerMillisLeft ?: flowOf(null)) - .collectAsState(initial = null) - - if (isShowingSleepTimerDialog) { - if (sleepTimerMillisLeft != null) { - ConfirmationDialog( - text = "Do you want to stop the sleep timer?", - cancelText = "No", - confirmText = "Stop", - onDismiss = { - isShowingSleepTimerDialog = false - onDismiss() - }, - onConfirm = { - binder?.cancelSleepTimer() - onDismiss() - } - ) - } else { - DefaultDialog(onDismiss = { isShowingSleepTimerDialog = false }) { - var amount by remember { - mutableStateOf(1) - } - - BasicText( - text = "Set sleep timer", - style = typography.s.semiBold, - modifier = Modifier - .padding(vertical = 8.dp, horizontal = 24.dp) - ) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy( - space = 16.dp, - alignment = Alignment.CenterHorizontally - ), - modifier = Modifier - .padding(vertical = 16.dp) - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .alpha(if (amount <= 1) 0.5f else 1f) - .clip(CircleShape) - .clickable(enabled = amount > 1) { amount-- } - .size(48.dp) - .background(colorPalette.background0) - ) { - BasicText( - text = "-", - style = typography.xs.semiBold - ) - } - - Box(contentAlignment = Alignment.Center) { - BasicText( - text = "88h 88m", - style = typography.s.semiBold, - modifier = Modifier - .alpha(0f) - ) - BasicText( - text = "${amount / 6}h ${(amount % 6) * 10}m", - style = typography.s.semiBold - ) - } - - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .alpha(if (amount >= 60) 0.5f else 1f) - .clip(CircleShape) - .clickable(enabled = amount < 60) { amount++ } - .size(48.dp) - .background(colorPalette.background0) - ) { - BasicText( - text = "+", - style = typography.xs.semiBold - ) - } - } - - Row( - horizontalArrangement = Arrangement.SpaceEvenly, - modifier = Modifier - .fillMaxWidth() - ) { - DialogTextButton( - text = "Cancel", - onClick = { - isShowingSleepTimerDialog = false - onDismiss() + onGoToArtist?.let { onGoToArtist -> + mediaItem.mediaMetadata.extras?.getStringArrayList("artistNames") + ?.let { artistNames -> + mediaItem.mediaMetadata.extras?.getStringArrayList("artistIds") + ?.let { artistIds -> + artistNames.zip(artistIds) + .forEach { (authorName, authorId) -> + if (authorId != null) { + MenuEntry( + icon = R.drawable.person, + text = "More of $authorName", + onClick = { + onDismiss() + onGoToArtist(authorId) + } + ) } - ) - - DialogTextButton( - text = "Set", - enabled = amount > 0, - onClick = { - binder?.startSleepTimer(amount * 10 * 60 * 1000L) - isShowingSleepTimerDialog = false - onDismiss() - } - ) - } + } } - } } + } - MenuEntry( - icon = R.drawable.alarm, - text = "Sleep timer", - secondaryText = sleepTimerMillisLeft?.let { - "${ - DateUtils.formatElapsedTime( - it / 1000 - ) - } left" - }, - onClick = { - isShowingSleepTimerDialog = true - } - ) - } - - if (onAddToPlaylist != null) { - MenuEntry( - icon = R.drawable.playlist, - text = "Add to playlist or favorites", - onClick = { - viewPlaylistsRoute() - } - ) - } - - onGoToAlbum?.let { onGoToAlbum -> - mediaItem.mediaMetadata.extras?.getString("albumId")?.let { albumId -> - MenuEntry( - icon = R.drawable.disc, - text = "Go to album", - onClick = { - onDismiss() - onGoToAlbum(albumId) - } - ) + onRemoveFromQueue?.let { onRemoveFromQueue -> + MenuEntry( + icon = R.drawable.trash, + text = "Remove from queue", + onClick = { + onDismiss() + onRemoveFromQueue() } - } + ) + } - onGoToArtist?.let { onGoToArtist -> - mediaItem.mediaMetadata.extras?.getStringArrayList("artistNames") - ?.let { artistNames -> - mediaItem.mediaMetadata.extras?.getStringArrayList("artistIds") - ?.let { artistIds -> - artistNames.zip(artistIds) - .forEach { (authorName, authorId) -> - if (authorId != null) { - MenuEntry( - icon = R.drawable.person, - text = "More of $authorName", - onClick = { - onDismiss() - onGoToArtist(authorId) - } - ) - } - } - } - } - } + onRemoveFromPlaylist?.let { onRemoveFromPlaylist -> + MenuEntry( + icon = R.drawable.trash, + text = "Remove from playlist", + onClick = { + onDismiss() + onRemoveFromPlaylist() + } + ) + } - onShare?.let { onShare -> - MenuEntry( - icon = R.drawable.share_social, - text = "Share", - onClick = { - onDismiss() - onShare() - } - ) - } - - onRemoveFromQueue?.let { onRemoveFromQueue -> - MenuEntry( - icon = R.drawable.trash, - text = "Remove from queue", - onClick = { - onDismiss() - onRemoveFromQueue() - } - ) - } - - onRemoveFromFavorites?.let { onRemoveFromFavorites -> - MenuEntry( - icon = R.drawable.heart_dislike, - text = "Remove from favorites", - onClick = { - onDismiss() - onRemoveFromFavorites() - } - ) - } - - onRemoveFromPlaylist?.let { onRemoveFromPlaylist -> - MenuEntry( - icon = R.drawable.trash, - text = "Remove from playlist", - onClick = { - onDismiss() - onRemoveFromPlaylist() - } - ) - } - - onHideFromDatabase?.let { onHideFromDatabase -> - MenuEntry( - icon = R.drawable.trash, - text = "Hide", - onClick = onHideFromDatabase - ) - } + onHideFromDatabase?.let { onHideFromDatabase -> + MenuEntry( + icon = R.drawable.trash, + text = "Hide", + onClick = onHideFromDatabase + ) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Menu.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Menu.kt index 9a1491c..0081f84 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Menu.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Menu.kt @@ -1,11 +1,11 @@ package it.vfsfitvnm.vimusic.ui.components.themed import androidx.annotation.DrawableRes +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row @@ -23,7 +23,6 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.secondary @@ -54,7 +53,8 @@ fun MenuEntry( text: String, onClick: () -> Unit, secondaryText: String? = null, - isEnabled: Boolean = true, + enabled: Boolean = true, + trailingContent: (@Composable () -> Unit)? = null ) { val (colorPalette, typography) = LocalAppearance.current @@ -62,9 +62,9 @@ fun MenuEntry( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(24.dp), modifier = Modifier - .clickable(enabled = isEnabled, onClick = onClick) + .clickable(enabled = enabled, onClick = onClick) .fillMaxWidth() - .alpha(if (isEnabled) 1f else 0.4f) + .alpha(if (enabled) 1f else 0.4f) .padding(horizontal = 24.dp, vertical = 16.dp) ) { Image( @@ -75,7 +75,10 @@ fun MenuEntry( .size(15.dp) ) - Column { + Column( + modifier = Modifier + .weight(1f) + ) { BasicText( text = text, style = typography.xs.medium @@ -88,41 +91,7 @@ fun MenuEntry( ) } } + + trailingContent?.invoke() } } - -@Composable -fun MenuIconButton( - @DrawableRes icon: Int, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - val (colorPalette) = LocalAppearance.current - - Box( - modifier = modifier - .padding(horizontal = 14.dp) - ) { - Image( - painter = painterResource(icon), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = onClick) - .padding(horizontal = 8.dp, vertical = 8.dp) - .size(20.dp) - ) - } -} - -@Composable -fun MenuBackButton( - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - MenuIconButton( - icon = R.drawable.chevron_back, - onClick = onClick, - modifier = modifier - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/SecondaryTextButton.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/SecondaryTextButton.kt index 2ac9aef..1a2727d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/SecondaryTextButton.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/SecondaryTextButton.kt @@ -17,7 +17,8 @@ fun SecondaryTextButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, - isEnabled: Boolean = true + enabled: Boolean = true, + alternative: Boolean = false ) { val (colorPalette, typography) = LocalAppearance.current @@ -26,8 +27,8 @@ fun SecondaryTextButton( style = typography.xxs.medium, modifier = modifier .clip(RoundedCornerShape(16.dp)) - .clickable(enabled = isEnabled, onClick = onClick) - .background(colorPalette.background2) + .clickable(enabled = enabled, onClick = onClick) + .background(if (alternative) colorPalette.background0 else colorPalette.background2) .padding(all = 8.dp) .padding(horizontal = 8.dp) ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/SongItem.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/SongItem.kt index 9ce9577..728af35 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/SongItem.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/SongItem.kt @@ -152,7 +152,7 @@ fun SongItem( text = title ?: "", style = typography.xs.semiBold, maxLines = 1, - overflow = TextOverflow.Clip, + overflow = TextOverflow.Ellipsis, modifier = Modifier .weight(1f) ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt index d7f61c7..f70cb89 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt @@ -20,7 +20,6 @@ val playlistRoute = Route1("playlistRoute") val searchResultRoute = Route1("searchResultRoute") val searchRoute = Route1("searchRoute") val settingsRoute = Route0("settingsRoute") -val viewPlaylistsRoute = Route0("createPlaylistRoute") @SuppressLint("ComposableNaming") @Suppress("NOTHING_TO_INLINE") diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt index d546a71..2c3c054 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt @@ -88,7 +88,7 @@ fun AlbumSongs( headerContent { SecondaryTextButton( text = "Enqueue", - isEnabled = songs.isNotEmpty(), + enabled = songs.isNotEmpty(), onClick = { binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem)) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt index ba0ccb9..fac6057 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt @@ -83,7 +83,7 @@ fun ArtistLocalSongs( headerContent { SecondaryTextButton( text = "Enqueue", - isEnabled = !songs.isNullOrEmpty(), + enabled = !songs.isNullOrEmpty(), onClick = { binder?.player?.enqueue(songs!!.map(DetailedSong::asMediaItem)) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistSongs.kt index de8ca30..3a188b4 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistSongs.kt @@ -23,9 +23,8 @@ import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop import it.vfsfitvnm.vimusic.ui.components.themed.Header -import it.vfsfitvnm.vimusic.ui.components.themed.InFavoritesMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu -import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton +import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.items.SongItem import it.vfsfitvnm.vimusic.ui.styling.Dimensions @@ -94,7 +93,7 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) { ) { SecondaryTextButton( text = "Enqueue", - isEnabled = songs.isNotEmpty(), + enabled = songs.isNotEmpty(), onClick = { binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem)) } @@ -121,8 +120,8 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) { onLongClick = { menuState.display { when (builtInPlaylist) { - BuiltInPlaylist.Favorites -> InFavoritesMediaItemMenu( - song = song, + BuiltInPlaylist.Favorites -> NonQueuedMediaItemMenu( + mediaItem = song.asMediaItem, onDismiss = menuState::hide ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongs.kt index cb69255..d298246 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongs.kt @@ -150,7 +150,7 @@ fun LocalPlaylistSongs( Header(title = playlistWithSongs?.playlist?.name ?: "Unknown") { SecondaryTextButton( text = "Enqueue", - isEnabled = playlistWithSongs?.songs?.isNotEmpty() == true, + enabled = playlistWithSongs?.songs?.isNotEmpty() == true, onClick = { playlistWithSongs?.songs ?.map(DetailedSong::asMediaItem) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt index f39cffc..8c3fa99 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt @@ -351,7 +351,7 @@ fun Lyrics( MenuEntry( icon = R.drawable.download, text = "Fetch lyrics again", - isEnabled = lyrics != null, + enabled = lyrics != null, onClick = { menuState.hide() query { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt index 81a6990..ea475a1 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt @@ -125,7 +125,7 @@ fun PlaylistSongList( Header(title = playlistPage?.title ?: "Unknown") { SecondaryTextButton( text = "Enqueue", - isEnabled = playlistPage?.songsPage?.items?.isNotEmpty() == true, + enabled = playlistPage?.songsPage?.items?.isNotEmpty() == true, onClick = { playlistPage?.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems -> binder?.player?.enqueue(mediaItems) diff --git a/app/src/main/res/drawable/chevron_forward.xml b/app/src/main/res/drawable/chevron_forward.xml new file mode 100644 index 0000000..24a5848 --- /dev/null +++ b/app/src/main/res/drawable/chevron_forward.xml @@ -0,0 +1,13 @@ + + +