Add search lyrics online and edit lyrics features
This commit is contained in:
@@ -16,6 +16,7 @@ import androidx.compose.material.ripple.rememberRipple
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import it.vfsfitvnm.vimusic.ui.components.ChunkyButton
|
import it.vfsfitvnm.vimusic.ui.components.ChunkyButton
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
|
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
|
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
|
||||||
@@ -42,6 +44,8 @@ fun TextFieldDialog(
|
|||||||
cancelText: String = "Cancel",
|
cancelText: String = "Cancel",
|
||||||
doneText: String = "Done",
|
doneText: String = "Done",
|
||||||
initialTextInput: String = "",
|
initialTextInput: String = "",
|
||||||
|
singleLine: Boolean = true,
|
||||||
|
maxLines: Int = 1,
|
||||||
onCancel: () -> Unit = onDismiss,
|
onCancel: () -> Unit = onDismiss,
|
||||||
isTextInputValid: (String) -> Boolean = { it.isNotEmpty() }
|
isTextInputValid: (String) -> Boolean = { it.isNotEmpty() }
|
||||||
) {
|
) {
|
||||||
@@ -70,8 +74,8 @@ fun TextFieldDialog(
|
|||||||
textFieldValue = it
|
textFieldValue = it
|
||||||
},
|
},
|
||||||
textStyle = typography.xs.semiBold.center,
|
textStyle = typography.xs.semiBold.center,
|
||||||
singleLine = true,
|
singleLine = singleLine,
|
||||||
maxLines = 1,
|
maxLines = maxLines,
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onDone = {
|
onDone = {
|
||||||
@@ -106,6 +110,7 @@ fun TextFieldDialog(
|
|||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(all = 16.dp)
|
.padding(all = 16.dp)
|
||||||
|
.weight(weight = 1f, fill = false)
|
||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -194,6 +199,7 @@ fun ConfirmationDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
inline fun DefaultDialog(
|
inline fun DefaultDialog(
|
||||||
noinline onDismiss: () -> Unit,
|
noinline onDismiss: () -> Unit,
|
||||||
@@ -201,7 +207,10 @@ inline fun DefaultDialog(
|
|||||||
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
|
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
|
||||||
crossinline content: @Composable ColumnScope.() -> Unit
|
crossinline content: @Composable ColumnScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = horizontalAlignment,
|
horizontalAlignment = horizontalAlignment,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|||||||
118
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/LyricsView.kt
Normal file
118
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/LyricsView.kt
Normal file
@@ -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<String>,
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.views
|
package it.vfsfitvnm.vimusic.ui.views
|
||||||
|
|
||||||
|
import android.app.SearchManager
|
||||||
|
import android.content.Intent
|
||||||
import androidx.compose.animation.AnimatedContentScope
|
import androidx.compose.animation.AnimatedContentScope
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
@@ -9,30 +11,24 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicText
|
import androidx.compose.foundation.text.BasicText
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
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.scale
|
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 androidx.compose.ui.unit.dp
|
||||||
import com.valentinilk.shimmer.shimmer
|
|
||||||
import it.vfsfitvnm.route.Route
|
import it.vfsfitvnm.route.Route
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
import it.vfsfitvnm.route.empty
|
import it.vfsfitvnm.route.empty
|
||||||
import it.vfsfitvnm.route.rememberRoute
|
import it.vfsfitvnm.route.rememberRoute
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
import it.vfsfitvnm.vimusic.models.Song
|
import it.vfsfitvnm.vimusic.models.Song
|
||||||
import it.vfsfitvnm.vimusic.ui.components.BottomSheet
|
import it.vfsfitvnm.vimusic.ui.components.BottomSheet
|
||||||
import it.vfsfitvnm.vimusic.ui.components.BottomSheetState
|
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.components.themed.TextPlaceholder
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.rememberLyricsRoute
|
import it.vfsfitvnm.vimusic.ui.screens.rememberLyricsRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
|
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
|
||||||
@@ -46,6 +42,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun PlayerBottomSheet(
|
fun PlayerBottomSheet(
|
||||||
@@ -153,6 +150,10 @@ fun PlayerBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
var lyricsOutcome by remember(song) {
|
||||||
|
mutableStateOf(song?.lyrics?.let { Outcome.Success(it) } ?: Outcome.Initial)
|
||||||
|
}
|
||||||
|
|
||||||
RouteHandler(
|
RouteHandler(
|
||||||
route = route,
|
route = route,
|
||||||
onRouteChanged = {
|
onRouteChanged = {
|
||||||
@@ -174,15 +175,13 @@ fun PlayerBottomSheet(
|
|||||||
.background(colorPalette.elevatedBackground)
|
.background(colorPalette.elevatedBackground)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
var lyricsOutcome by remember(song) {
|
|
||||||
mutableStateOf(song?.lyrics?.let { Outcome.Success(it) } ?: Outcome.Initial)
|
|
||||||
}
|
|
||||||
|
|
||||||
lyricsRoute {
|
lyricsRoute {
|
||||||
OutcomeItem(
|
val context = LocalContext.current
|
||||||
outcome = lyricsOutcome,
|
|
||||||
|
LyricsView(
|
||||||
|
lyricsOutcome = lyricsOutcome,
|
||||||
|
nestedScrollConnectionProvider = layoutState::nestedScrollConnection,
|
||||||
onInitialize = {
|
onInitialize = {
|
||||||
println("onInitialize!!")
|
|
||||||
coroutineScope.launch(Dispatchers.Main) {
|
coroutineScope.launch(Dispatchers.Main) {
|
||||||
lyricsOutcome = Outcome.Loading
|
lyricsOutcome = Outcome.Loading
|
||||||
|
|
||||||
@@ -209,34 +208,22 @@ fun PlayerBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLoading = {
|
onSearchOnline = {
|
||||||
LyricsShimmer(
|
player.mediaMetadata.let {
|
||||||
modifier = Modifier
|
context.startActivity(Intent(Intent.ACTION_WEB_SEARCH).apply {
|
||||||
.shimmer()
|
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 {
|
host {
|
||||||
|
|||||||
Reference in New Issue
Block a user