From 32fdbf437c18b3d99a44172ad9c2b8fc78809d30 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Wed, 15 Jun 2022 16:46:49 +0200 Subject: [PATCH] Add search lyrics online and edit lyrics features --- .../vimusic/ui/components/themed/Dialog.kt | 15 ++- .../vfsfitvnm/vimusic/ui/views/LyricsView.kt | 118 ++++++++++++++++++ .../vimusic/ui/views/PlayerBottomSheet.kt | 69 +++++----- 3 files changed, 158 insertions(+), 44 deletions(-) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/LyricsView.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Dialog.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Dialog.kt index 9369f85..ff4f9e1 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Dialog.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Dialog.kt @@ -16,6 +16,7 @@ import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester @@ -27,6 +28,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import it.vfsfitvnm.vimusic.ui.components.ChunkyButton import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalTypography @@ -42,6 +44,8 @@ fun TextFieldDialog( cancelText: String = "Cancel", doneText: String = "Done", initialTextInput: String = "", + singleLine: Boolean = true, + maxLines: Int = 1, onCancel: () -> Unit = onDismiss, isTextInputValid: (String) -> Boolean = { it.isNotEmpty() } ) { @@ -70,8 +74,8 @@ fun TextFieldDialog( textFieldValue = it }, textStyle = typography.xs.semiBold.center, - singleLine = true, - maxLines = 1, + singleLine = singleLine, + maxLines = maxLines, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions( onDone = { @@ -106,6 +110,7 @@ fun TextFieldDialog( }, modifier = Modifier .padding(all = 16.dp) + .weight(weight = 1f, fill = false) .focusRequester(focusRequester) ) @@ -194,6 +199,7 @@ fun ConfirmationDialog( } } +@OptIn(ExperimentalComposeUiApi::class) @Composable inline fun DefaultDialog( noinline onDismiss: () -> Unit, @@ -201,7 +207,10 @@ inline fun DefaultDialog( horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, crossinline content: @Composable ColumnScope.() -> Unit ) { - Dialog(onDismissRequest = onDismiss) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { Column( horizontalAlignment = horizontalAlignment, modifier = modifier diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/LyricsView.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/LyricsView.kt new file mode 100644 index 0000000..882e613 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/LyricsView.kt @@ -0,0 +1,118 @@ +package it.vfsfitvnm.vimusic.ui.views + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import com.valentinilk.shimmer.shimmer +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.ui.components.Message +import it.vfsfitvnm.vimusic.ui.components.OutcomeItem +import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog +import it.vfsfitvnm.vimusic.ui.styling.LocalTypography +import it.vfsfitvnm.vimusic.utils.center +import it.vfsfitvnm.vimusic.utils.secondary +import it.vfsfitvnm.youtubemusic.Outcome + +@Composable +fun LyricsView( + lyricsOutcome: Outcome, + onInitialize: () -> Unit, + onSearchOnline: () -> Unit, + onLyricsUpdate: (String) -> Unit, + nestedScrollConnectionProvider: () -> NestedScrollConnection, +) { + val typography = LocalTypography.current + + var isEditingLyrics by remember { + mutableStateOf(false) + } + + if (isEditingLyrics) { + TextFieldDialog( + hintText = "Enter the lyrics", + initialTextInput = lyricsOutcome.valueOrNull ?: "", + singleLine = false, + maxLines = Int.MAX_VALUE, + isTextInputValid = { true }, + onDismiss = { + isEditingLyrics = false + }, + onDone = onLyricsUpdate + ) + } + + OutcomeItem( + outcome = lyricsOutcome, + onInitialize = onInitialize, + onLoading = { + LyricsShimmer( + modifier = Modifier + .fillMaxSize() + .shimmer() + ) + } + ) { lyrics -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(top = 64.dp) + .nestedScroll(remember { nestedScrollConnectionProvider() }) + .verticalScroll(rememberScrollState()) + .fillMaxWidth() + .padding(vertical = 16.dp) + .padding(horizontal = 48.dp) + ) { + if (lyrics.isEmpty()) { + Message( + text = "Lyrics not available", + icon = R.drawable.text, + ) + } else { + BasicText( + text = lyrics, + style = typography.xs.center, + ) + } + + Row( + modifier = Modifier + .padding(top = 32.dp) + ) { + BasicText( + text = "Search online", + style = typography.xs.secondary.copy(textDecoration = TextDecoration.Underline), + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = onSearchOnline + ) + .padding(horizontal = 8.dp) + ) + + BasicText( + text = "Edit", + style = typography.xs.secondary.copy(textDecoration = TextDecoration.Underline), + modifier = Modifier + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + isEditingLyrics = true + } + .padding(horizontal = 8.dp) + ) + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt index 2f9377a..d843c74 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt @@ -1,5 +1,7 @@ package it.vfsfitvnm.vimusic.ui.views +import android.app.SearchManager +import android.content.Intent import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.animateColorAsState @@ -9,30 +11,24 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText -import androidx.compose.foundation.verticalScroll import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.scale -import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import com.valentinilk.shimmer.shimmer import it.vfsfitvnm.route.Route import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.route.empty import it.vfsfitvnm.route.rememberRoute import it.vfsfitvnm.vimusic.Database -import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.Song import it.vfsfitvnm.vimusic.ui.components.BottomSheet import it.vfsfitvnm.vimusic.ui.components.BottomSheetState -import it.vfsfitvnm.vimusic.ui.components.Message -import it.vfsfitvnm.vimusic.ui.components.OutcomeItem import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.screens.rememberLyricsRoute import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette @@ -46,6 +42,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext + @ExperimentalAnimationApi @Composable fun PlayerBottomSheet( @@ -153,6 +150,10 @@ fun PlayerBottomSheet( } } ) { + var lyricsOutcome by remember(song) { + mutableStateOf(song?.lyrics?.let { Outcome.Success(it) } ?: Outcome.Initial) + } + RouteHandler( route = route, onRouteChanged = { @@ -174,15 +175,13 @@ fun PlayerBottomSheet( .background(colorPalette.elevatedBackground) .fillMaxSize() ) { - var lyricsOutcome by remember(song) { - mutableStateOf(song?.lyrics?.let { Outcome.Success(it) } ?: Outcome.Initial) - } - lyricsRoute { - OutcomeItem( - outcome = lyricsOutcome, + val context = LocalContext.current + + LyricsView( + lyricsOutcome = lyricsOutcome, + nestedScrollConnectionProvider = layoutState::nestedScrollConnection, onInitialize = { - println("onInitialize!!") coroutineScope.launch(Dispatchers.Main) { lyricsOutcome = Outcome.Loading @@ -209,34 +208,22 @@ fun PlayerBottomSheet( } } }, - onLoading = { - LyricsShimmer( - modifier = Modifier - .shimmer() - ) + onSearchOnline = { + player.mediaMetadata.let { + context.startActivity(Intent(Intent.ACTION_WEB_SEARCH).apply { + putExtra( + SearchManager.QUERY, + "${it.title} ${it.artist} lyrics" + ) + }) + } + }, + onLyricsUpdate = { + coroutineScope.launch(Dispatchers.IO) { + Database.update((song ?: Database.insert(player.mediaItem!!)).copy(lyrics = it)) + } } - ) { lyrics -> - if (lyrics.isEmpty()) { - Message( - text = "Lyrics not available", - icon = R.drawable.text, - modifier = Modifier - .padding(top = 64.dp) - ) - } else { - BasicText( - text = lyrics, - style = typography.xs.center, - modifier = Modifier - .padding(top = 64.dp) - .nestedScroll(remember { layoutState.nestedScrollConnection() }) - .verticalScroll(rememberScrollState()) - .fillMaxWidth() - .padding(vertical = 16.dp) - .padding(horizontal = 48.dp) - ) - } - } + ) } host {