Improve MediaItemMenu UI
This commit is contained in:
parent
3364bb9f30
commit
b2ad011cfd
13 changed files with 440 additions and 431 deletions
|
@ -60,7 +60,8 @@ fun BoxScope.FloatingActionsContainerWithScrollToTop(
|
|||
) {
|
||||
val transitionState = remember {
|
||||
MutableTransitionState<ScrollingInfo?>(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)
|
||||
|
|
|
@ -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<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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -152,7 +152,7 @@ fun SongItem(
|
|||
text = title ?: "",
|
||||
style = typography.xs.semiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Clip,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
)
|
||||
|
|
|
@ -20,7 +20,6 @@ val playlistRoute = Route1<String?>("playlistRoute")
|
|||
val searchResultRoute = Route1<String>("searchResultRoute")
|
||||
val searchRoute = Route1<String>("searchRoute")
|
||||
val settingsRoute = Route0("settingsRoute")
|
||||
val viewPlaylistsRoute = Route0("createPlaylistRoute")
|
||||
|
||||
@SuppressLint("ComposableNaming")
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
|
|
|
@ -88,7 +88,7 @@ fun AlbumSongs(
|
|||
headerContent {
|
||||
SecondaryTextButton(
|
||||
text = "Enqueue",
|
||||
isEnabled = songs.isNotEmpty(),
|
||||
enabled = songs.isNotEmpty(),
|
||||
onClick = {
|
||||
binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem))
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ fun ArtistLocalSongs(
|
|||
headerContent {
|
||||
SecondaryTextButton(
|
||||
text = "Enqueue",
|
||||
isEnabled = !songs.isNullOrEmpty(),
|
||||
enabled = !songs.isNullOrEmpty(),
|
||||
onClick = {
|
||||
binder?.player?.enqueue(songs!!.map(DetailedSong::asMediaItem))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
13
app/src/main/res/drawable/chevron_forward.xml
Normal file
13
app/src/main/res/drawable/chevron_forward.xml
Normal 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>
|
Loading…
Reference in a new issue