Improve MediaItemMenu UI

This commit is contained in:
vfsfitvnm 2022-10-07 12:08:52 +02:00
parent 3364bb9f30
commit b2ad011cfd
13 changed files with 440 additions and 431 deletions

View file

@ -60,7 +60,8 @@ fun BoxScope.FloatingActionsContainerWithScrollToTop(
) { ) {
val transitionState = remember { val transitionState = remember {
MutableTransitionState<ScrollingInfo?>(ScrollingInfo()) MutableTransitionState<ScrollingInfo?>(ScrollingInfo())
}.apply { targetState = if (visible) lazyListState.scrollingInfo() else null } }.apply { targetState = lazyListState.scrollingInfo() }
// }.apply { targetState = if (visible) lazyListState.scrollingInfo() else null }
FloatingActions( FloatingActions(
transitionState = transitionState, transitionState = transitionState,
@ -125,7 +126,7 @@ fun BoxScope.FloatingActions(
onScrollToTop() onScrollToTop()
} }
}, },
enabled = transition.targetState?.isScrollingDown == false && transition.targetState?.isFar == true, // enabled = transition.targetState?.isScrollingDown == false && transition.targetState?.isFar == true,
iconId = R.drawable.chevron_up, iconId = R.drawable.chevron_up,
modifier = Modifier modifier = Modifier
.padding(bottom = 16.dp) .padding(bottom = 16.dp)

View file

@ -2,20 +2,23 @@ package it.vfsfitvnm.vimusic.ui.components.themed
import android.content.Intent import android.content.Intent
import android.text.format.DateUtils import android.text.format.DateUtils
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.with import androidx.compose.animation.with
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicText
@ -30,11 +33,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip 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.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.compose.ui.unit.dp
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R 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.enums.SortOrder
import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.Song
import it.vfsfitvnm.vimusic.models.SongPlaylistMap import it.vfsfitvnm.vimusic.models.SongPlaylistMap
import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.transaction 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.albumRoute
import it.vfsfitvnm.vimusic.ui.screens.artistRoute 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.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.addNext
import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.enqueue 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.vimusic.utils.semiBold
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf 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 @ExperimentalAnimationApi
@Composable @Composable
fun InHistoryMediaItemMenu( fun InHistoryMediaItemMenu(
@ -143,7 +134,6 @@ fun NonQueuedMediaItemMenu(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onRemoveFromPlaylist: (() -> Unit)? = null, onRemoveFromPlaylist: (() -> Unit)? = null,
onHideFromDatabase: (() -> Unit)? = null, onHideFromDatabase: (() -> Unit)? = null,
onRemoveFromFavorites: (() -> Unit)? = null,
) { ) {
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
@ -164,7 +154,6 @@ fun NonQueuedMediaItemMenu(
onEnqueue = { binder?.player?.enqueue(mediaItem) }, onEnqueue = { binder?.player?.enqueue(mediaItem) },
onRemoveFromPlaylist = onRemoveFromPlaylist, onRemoveFromPlaylist = onRemoveFromPlaylist,
onHideFromDatabase = onHideFromDatabase, onHideFromDatabase = onHideFromDatabase,
onRemoveFromFavorites = onRemoveFromFavorites,
modifier = modifier modifier = modifier
) )
} }
@ -203,7 +192,6 @@ fun BaseMediaItemMenu(
onRemoveFromQueue: (() -> Unit)? = null, onRemoveFromQueue: (() -> Unit)? = null,
onRemoveFromPlaylist: (() -> Unit)? = null, onRemoveFromPlaylist: (() -> Unit)? = null,
onHideFromDatabase: (() -> Unit)? = null, onHideFromDatabase: (() -> Unit)? = null,
onRemoveFromFavorites: (() -> Unit)? = null
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -228,7 +216,6 @@ fun BaseMediaItemMenu(
} }
}, },
onHideFromDatabase = onHideFromDatabase, onHideFromDatabase = onHideFromDatabase,
onRemoveFromFavorites = onRemoveFromFavorites,
onRemoveFromPlaylist = onRemoveFromPlaylist, onRemoveFromPlaylist = onRemoveFromPlaylist,
onRemoveFromQueue = onRemoveFromQueue, onRemoveFromQueue = onRemoveFromQueue,
onGoToAlbum = albumRoute::global, onGoToAlbum = albumRoute::global,
@ -262,385 +249,425 @@ fun MediaItemMenu(
onEnqueue: (() -> Unit)? = null, onEnqueue: (() -> Unit)? = null,
onHideFromDatabase: (() -> Unit)? = null, onHideFromDatabase: (() -> Unit)? = null,
onRemoveFromQueue: (() -> Unit)? = null, onRemoveFromQueue: (() -> Unit)? = null,
onRemoveFromFavorites: (() -> Unit)? = null,
onRemoveFromPlaylist: (() -> Unit)? = null, onRemoveFromPlaylist: (() -> Unit)? = null,
onAddToPlaylist: ((Playlist, Int) -> Unit)? = null, onAddToPlaylist: ((Playlist, Int) -> Unit)? = null,
onGoToAlbum: ((String) -> Unit)? = null, onGoToAlbum: ((String) -> Unit)? = null,
onGoToArtist: ((String) -> Unit)? = null, onGoToArtist: ((String) -> Unit)? = null,
onShare: (() -> Unit)? = null onShare: () -> Unit
) { ) {
Menu(modifier = modifier) { val (colorPalette) = LocalAppearance.current
RouteHandler( val density = LocalDensity.current
transitionSpec = {
when (targetState.route) {
viewPlaylistsRoute -> slideIntoContainer(AnimatedContentScope.SlideDirection.Left) with
slideOutOfContainer(AnimatedContentScope.SlideDirection.Left)
else -> when (initialState.route) { var isViewingPlaylists by remember {
viewPlaylistsRoute -> slideIntoContainer(AnimatedContentScope.SlideDirection.Right) with mutableStateOf(false)
slideOutOfContainer(AnimatedContentScope.SlideDirection.Right) }
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<IntOffset>(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
)
}
)
} }
} }
} }
) { } else {
viewPlaylistsRoute { Menu(
val playlistPreviews by remember { modifier = modifier
Database.playlistPreviews(PlaylistSortBy.DateAdded, SortOrder.Descending) .onPlaced { height = with(density) { it.size.height.toDp() } }
}.collectAsState(initial = emptyList(), context = Dispatchers.IO) ) {
val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSizePx = thumbnailSizeDp.px
var isCreatingNewPlaylist by rememberSaveable { SongItem(
mutableStateOf(false) 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) { Spacer(
TextFieldDialog( modifier = Modifier
hintText = "Enter the playlist name", .height(8.dp)
onDismiss = { )
isCreatingNewPlaylist = false
}, Spacer(
onDone = { text -> 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() onDismiss()
onAddToPlaylist(Playlist(name = text), 0) onStartRadio()
} }
) )
} }
Column { onPlayNext?.let { onPlayNext ->
Row( MenuEntry(
horizontalArrangement = Arrangement.SpaceBetween, icon = R.drawable.play_skip_forward,
modifier = Modifier text = "Play next",
.fillMaxWidth() onClick = {
) { onDismiss()
MenuBackButton(onClick = pop) onPlayNext()
}
)
}
if (onAddToPlaylist != null) { onEnqueue?.let { onEnqueue ->
MenuIconButton( MenuEntry(
icon = R.drawable.add, icon = R.drawable.enqueue,
onClick = { text = "Enqueue",
isCreatingNewPlaylist = true 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 -> MenuEntry(
if (onRemoveFromFavorites == null) { icon = R.drawable.alarm,
MenuEntry( text = "Sleep timer",
icon = R.drawable.heart, secondaryText = sleepTimerMillisLeft?.let {
text = "Favorites", "${
onClick = { DateUtils.formatElapsedTime(
onDismiss() it / 1000
query { )
Database.insert(mediaItem) } left"
Database.like(mediaItem.mediaId, System.currentTimeMillis()) },
} onClick = {
} isShowingSleepTimerDialog = true
)
} }
)
}
playlistPreviews.forEach { playlistPreview -> if (onAddToPlaylist != null) {
MenuEntry( MenuEntry(
icon = R.drawable.playlist, icon = R.drawable.playlist,
text = playlistPreview.playlist.name, text = "Add to playlist",
secondaryText = "${playlistPreview.songCount} songs", onClick = { isViewingPlaylists = true },
onClick = { trailingContent = {
onDismiss() Image(
onAddToPlaylist( painter = painterResource(R.drawable.chevron_forward),
playlistPreview.playlist, contentDescription = null,
playlistPreview.songCount 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 { onGoToArtist?.let { onGoToArtist ->
Column( mediaItem.mediaMetadata.extras?.getStringArrayList("artistNames")
modifier = Modifier ?.let { artistNames ->
.pointerInput(Unit) { mediaItem.mediaMetadata.extras?.getStringArrayList("artistIds")
detectTapGestures { } ?.let { artistIds ->
} artistNames.zip(artistIds)
) { .forEach { (authorName, authorId) ->
onStartRadio?.let { onStartRadio -> if (authorId != null) {
MenuEntry( MenuEntry(
icon = R.drawable.radio, icon = R.drawable.person,
text = "Start radio", text = "More of $authorName",
onClick = { onClick = {
onDismiss() onDismiss()
onStartRadio() onGoToArtist(authorId)
} }
) )
}
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()
} }
) }
DialogTextButton(
text = "Set",
enabled = amount > 0,
onClick = {
binder?.startSleepTimer(amount * 10 * 60 * 1000L)
isShowingSleepTimerDialog = false
onDismiss()
}
)
}
} }
}
} }
}
MenuEntry( onRemoveFromQueue?.let { onRemoveFromQueue ->
icon = R.drawable.alarm, MenuEntry(
text = "Sleep timer", icon = R.drawable.trash,
secondaryText = sleepTimerMillisLeft?.let { text = "Remove from queue",
"${ onClick = {
DateUtils.formatElapsedTime( onDismiss()
it / 1000 onRemoveFromQueue()
)
} 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)
}
)
} }
} )
}
onGoToArtist?.let { onGoToArtist -> onRemoveFromPlaylist?.let { onRemoveFromPlaylist ->
mediaItem.mediaMetadata.extras?.getStringArrayList("artistNames") MenuEntry(
?.let { artistNames -> icon = R.drawable.trash,
mediaItem.mediaMetadata.extras?.getStringArrayList("artistIds") text = "Remove from playlist",
?.let { artistIds -> onClick = {
artistNames.zip(artistIds) onDismiss()
.forEach { (authorName, authorId) -> onRemoveFromPlaylist()
if (authorId != null) { }
MenuEntry( )
icon = R.drawable.person, }
text = "More of $authorName",
onClick = {
onDismiss()
onGoToArtist(authorId)
}
)
}
}
}
}
}
onShare?.let { onShare -> onHideFromDatabase?.let { onHideFromDatabase ->
MenuEntry( MenuEntry(
icon = R.drawable.share_social, icon = R.drawable.trash,
text = "Share", text = "Hide",
onClick = { onClick = onHideFromDatabase
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
)
}
} }
} }
} }

View file

@ -1,11 +1,11 @@
package it.vfsfitvnm.vimusic.ui.components.themed package it.vfsfitvnm.vimusic.ui.components.themed
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row 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.graphics.ColorFilter
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.secondary
@ -54,7 +53,8 @@ fun MenuEntry(
text: String, text: String,
onClick: () -> Unit, onClick: () -> Unit,
secondaryText: String? = null, secondaryText: String? = null,
isEnabled: Boolean = true, enabled: Boolean = true,
trailingContent: (@Composable () -> Unit)? = null
) { ) {
val (colorPalette, typography) = LocalAppearance.current val (colorPalette, typography) = LocalAppearance.current
@ -62,9 +62,9 @@ fun MenuEntry(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp), horizontalArrangement = Arrangement.spacedBy(24.dp),
modifier = Modifier modifier = Modifier
.clickable(enabled = isEnabled, onClick = onClick) .clickable(enabled = enabled, onClick = onClick)
.fillMaxWidth() .fillMaxWidth()
.alpha(if (isEnabled) 1f else 0.4f) .alpha(if (enabled) 1f else 0.4f)
.padding(horizontal = 24.dp, vertical = 16.dp) .padding(horizontal = 24.dp, vertical = 16.dp)
) { ) {
Image( Image(
@ -75,7 +75,10 @@ fun MenuEntry(
.size(15.dp) .size(15.dp)
) )
Column { Column(
modifier = Modifier
.weight(1f)
) {
BasicText( BasicText(
text = text, text = text,
style = typography.xs.medium 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
)
}

View file

@ -17,7 +17,8 @@ fun SecondaryTextButton(
text: String, text: String,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isEnabled: Boolean = true enabled: Boolean = true,
alternative: Boolean = false
) { ) {
val (colorPalette, typography) = LocalAppearance.current val (colorPalette, typography) = LocalAppearance.current
@ -26,8 +27,8 @@ fun SecondaryTextButton(
style = typography.xxs.medium, style = typography.xxs.medium,
modifier = modifier modifier = modifier
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.clickable(enabled = isEnabled, onClick = onClick) .clickable(enabled = enabled, onClick = onClick)
.background(colorPalette.background2) .background(if (alternative) colorPalette.background0 else colorPalette.background2)
.padding(all = 8.dp) .padding(all = 8.dp)
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
) )

View file

@ -152,7 +152,7 @@ fun SongItem(
text = title ?: "", text = title ?: "",
style = typography.xs.semiBold, style = typography.xs.semiBold,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Clip, overflow = TextOverflow.Ellipsis,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
) )

View file

@ -20,7 +20,6 @@ val playlistRoute = Route1<String?>("playlistRoute")
val searchResultRoute = Route1<String>("searchResultRoute") val searchResultRoute = Route1<String>("searchResultRoute")
val searchRoute = Route1<String>("searchRoute") val searchRoute = Route1<String>("searchRoute")
val settingsRoute = Route0("settingsRoute") val settingsRoute = Route0("settingsRoute")
val viewPlaylistsRoute = Route0("createPlaylistRoute")
@SuppressLint("ComposableNaming") @SuppressLint("ComposableNaming")
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")

View file

@ -88,7 +88,7 @@ fun AlbumSongs(
headerContent { headerContent {
SecondaryTextButton( SecondaryTextButton(
text = "Enqueue", text = "Enqueue",
isEnabled = songs.isNotEmpty(), enabled = songs.isNotEmpty(),
onClick = { onClick = {
binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem)) binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem))
} }

View file

@ -83,7 +83,7 @@ fun ArtistLocalSongs(
headerContent { headerContent {
SecondaryTextButton( SecondaryTextButton(
text = "Enqueue", text = "Enqueue",
isEnabled = !songs.isNullOrEmpty(), enabled = !songs.isNullOrEmpty(),
onClick = { onClick = {
binder?.player?.enqueue(songs!!.map(DetailedSong::asMediaItem)) binder?.player?.enqueue(songs!!.map(DetailedSong::asMediaItem))
} }

View file

@ -23,9 +23,8 @@ import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop import it.vfsfitvnm.vimusic.ui.components.themed.FloatingActionsContainerWithScrollToTop
import it.vfsfitvnm.vimusic.ui.components.themed.Header 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.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.components.themed.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.items.SongItem import it.vfsfitvnm.vimusic.ui.items.SongItem
import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.Dimensions
@ -94,7 +93,7 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
) { ) {
SecondaryTextButton( SecondaryTextButton(
text = "Enqueue", text = "Enqueue",
isEnabled = songs.isNotEmpty(), enabled = songs.isNotEmpty(),
onClick = { onClick = {
binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem)) binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem))
} }
@ -121,8 +120,8 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
onLongClick = { onLongClick = {
menuState.display { menuState.display {
when (builtInPlaylist) { when (builtInPlaylist) {
BuiltInPlaylist.Favorites -> InFavoritesMediaItemMenu( BuiltInPlaylist.Favorites -> NonQueuedMediaItemMenu(
song = song, mediaItem = song.asMediaItem,
onDismiss = menuState::hide onDismiss = menuState::hide
) )

View file

@ -150,7 +150,7 @@ fun LocalPlaylistSongs(
Header(title = playlistWithSongs?.playlist?.name ?: "Unknown") { Header(title = playlistWithSongs?.playlist?.name ?: "Unknown") {
SecondaryTextButton( SecondaryTextButton(
text = "Enqueue", text = "Enqueue",
isEnabled = playlistWithSongs?.songs?.isNotEmpty() == true, enabled = playlistWithSongs?.songs?.isNotEmpty() == true,
onClick = { onClick = {
playlistWithSongs?.songs playlistWithSongs?.songs
?.map(DetailedSong::asMediaItem) ?.map(DetailedSong::asMediaItem)

View file

@ -351,7 +351,7 @@ fun Lyrics(
MenuEntry( MenuEntry(
icon = R.drawable.download, icon = R.drawable.download,
text = "Fetch lyrics again", text = "Fetch lyrics again",
isEnabled = lyrics != null, enabled = lyrics != null,
onClick = { onClick = {
menuState.hide() menuState.hide()
query { query {

View file

@ -125,7 +125,7 @@ fun PlaylistSongList(
Header(title = playlistPage?.title ?: "Unknown") { Header(title = playlistPage?.title ?: "Unknown") {
SecondaryTextButton( SecondaryTextButton(
text = "Enqueue", text = "Enqueue",
isEnabled = playlistPage?.songsPage?.items?.isNotEmpty() == true, enabled = playlistPage?.songsPage?.items?.isNotEmpty() == true,
onClick = { onClick = {
playlistPage?.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems -> playlistPage?.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
binder?.player?.enqueue(mediaItems) binder?.player?.enqueue(mediaItems)

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:pathData="M184,112l144,144l-144,144"
android:strokeLineJoin="round"
android:strokeWidth="48"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
</vector>