Show lyrics menu even when the fetch fails (#221)

This commit is contained in:
vfsfitvnm
2022-08-17 11:41:10 +02:00
parent 1682228ece
commit 59f66c5b65
2 changed files with 122 additions and 109 deletions

View File

@@ -57,15 +57,16 @@ import it.vfsfitvnm.vimusic.ui.components.themed.Menu
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.PureBlackColorPalette
import it.vfsfitvnm.vimusic.ui.styling.DefaultDarkColorPalette import it.vfsfitvnm.vimusic.ui.styling.DefaultDarkColorPalette
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.PureBlackColorPalette
import it.vfsfitvnm.vimusic.ui.styling.onOverlayShimmer import it.vfsfitvnm.vimusic.ui.styling.onOverlayShimmer
import it.vfsfitvnm.vimusic.utils.SynchronizedLyrics import it.vfsfitvnm.vimusic.utils.SynchronizedLyrics
import it.vfsfitvnm.vimusic.utils.center import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.color import it.vfsfitvnm.vimusic.utils.color
import it.vfsfitvnm.vimusic.utils.isShowingSynchronizedLyricsKey import it.vfsfitvnm.vimusic.utils.isShowingSynchronizedLyricsKey
import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.relaunchableEffect
import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.vimusic.utils.verticalFadingEdge import it.vfsfitvnm.vimusic.utils.verticalFadingEdge
import it.vfsfitvnm.youtubemusic.YouTube import it.vfsfitvnm.youtubemusic.YouTube
@@ -89,29 +90,22 @@ fun Lyrics(
nestedScrollConnectionProvider: () -> NestedScrollConnection, nestedScrollConnectionProvider: () -> NestedScrollConnection,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val (colorPalette, typography) = LocalAppearance.current
val context = LocalContext.current
AnimatedVisibility( AnimatedVisibility(
visible = isDisplayed, visible = isDisplayed,
enter = fadeIn(), enter = fadeIn(),
exit = fadeOut(), exit = fadeOut(),
) { ) {
val (colorPalette, typography) = LocalAppearance.current
val context = LocalContext.current
val menuState = LocalMenuState.current
var isShowingSynchronizedLyrics by rememberPreference(isShowingSynchronizedLyricsKey, false) var isShowingSynchronizedLyrics by rememberPreference(isShowingSynchronizedLyricsKey, false)
var isLoading by remember(mediaId, isShowingSynchronizedLyrics) { var state by remember(mediaId, isShowingSynchronizedLyrics) {
mutableStateOf(false) mutableStateOf(LyricsState())
} }
var isEditingLyrics by remember(mediaId, isShowingSynchronizedLyrics) { val fetchLyrics = relaunchableEffect(mediaId, isShowingSynchronizedLyrics) {
mutableStateOf(false)
}
var lyrics by remember(mediaId, isShowingSynchronizedLyrics) {
mutableStateOf<String?>(".")
}
LaunchedEffect(mediaId, isShowingSynchronizedLyrics) {
if (isShowingSynchronizedLyrics) { if (isShowingSynchronizedLyrics) {
Database.synchronizedLyrics(mediaId) Database.synchronizedLyrics(mediaId)
} else { } else {
@@ -119,7 +113,7 @@ fun Lyrics(
}.distinctUntilChanged().map flowMap@{ lyrics -> }.distinctUntilChanged().map flowMap@{ lyrics ->
if (lyrics != null) return@flowMap lyrics if (lyrics != null) return@flowMap lyrics
isLoading = true state = state.copy(isLoading = true)
if (isShowingSynchronizedLyrics) { if (isShowingSynchronizedLyrics) {
val mediaMetadata = mediaMetadataProvider() val mediaMetadata = mediaMetadataProvider()
@@ -134,32 +128,33 @@ fun Lyrics(
} }
} }
KuGou.lyrics(mediaMetadata.artist?.toString() ?: "", mediaMetadata.title?.toString() ?: "", duration / 1000)?.map { KuGou.lyrics(
it?.value artist = mediaMetadata.artist?.toString() ?: "",
} title = mediaMetadata.title?.toString() ?: "",
duration = duration / 1000
)?.map { it?.value }
} else { } else {
YouTube.next(mediaId, null)?.map { nextResult -> nextResult.lyrics?.text()?.getOrNull() } YouTube.next(mediaId, null)
?.map { nextResult -> nextResult.lyrics?.text()?.getOrNull() }
}?.map { newLyrics -> }?.map { newLyrics ->
onLyricsUpdate(isShowingSynchronizedLyrics, mediaId, newLyrics ?: "") onLyricsUpdate(isShowingSynchronizedLyrics, mediaId, newLyrics ?: "")
isLoading = false state = state.copy(isLoading = false)
return@flowMap newLyrics ?: "" return@flowMap newLyrics ?: ""
} }
isLoading = false state = state.copy(isLoading = false)
null null
}.flowOn(Dispatchers.IO).collect { lyrics = it } }.flowOn(Dispatchers.IO).collect { state = state.copy(lyrics = it) }
} }
if (isEditingLyrics) { if (state.isEditing) {
TextFieldDialog( TextFieldDialog(
hintText = "Enter the lyrics", hintText = "Enter the lyrics",
initialTextInput = lyrics ?: "", initialTextInput = state.lyrics ?: "",
singleLine = false, singleLine = false,
maxLines = 10, maxLines = 10,
isTextInputValid = { true }, isTextInputValid = { true },
onDismiss = { onDismiss = { state = state.copy(isEditing = false) },
isEditingLyrics = false
},
onDone = { onDone = {
query { query {
if (isShowingSynchronizedLyrics) { if (isShowingSynchronizedLyrics) {
@@ -178,16 +173,14 @@ fun Lyrics(
modifier = modifier modifier = modifier
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onTap = { onTap = { onDismiss() }
onDismiss()
}
) )
} }
.fillMaxSize() .fillMaxSize()
.background(Color.Black.copy(0.8f)) .background(Color.Black.copy(0.8f))
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = !isLoading && lyrics == null, visible = !state.isLoading && state.lyrics == null,
enter = slideInVertically { -it }, enter = slideInVertically { -it },
exit = slideOutVertically { -it }, exit = slideOutVertically { -it },
modifier = Modifier modifier = Modifier
@@ -204,7 +197,7 @@ fun Lyrics(
} }
AnimatedVisibility( AnimatedVisibility(
visible = lyrics?.let(String::isEmpty) ?: false, visible = state.lyrics?.let(String::isEmpty) ?: false,
enter = slideInVertically { -it }, enter = slideInVertically { -it },
exit = slideOutVertically { -it }, exit = slideOutVertically { -it },
modifier = Modifier modifier = Modifier
@@ -220,7 +213,7 @@ fun Lyrics(
) )
} }
if (isLoading) { if (state.isLoading) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
@@ -235,11 +228,12 @@ fun Lyrics(
} }
} }
} else { } else {
lyrics?.let { lyrics -> state.lyrics?.let { lyrics ->
if (lyrics.isNotEmpty() && lyrics != ".") { if (lyrics.isNotEmpty() && lyrics != ".") {
if (isShowingSynchronizedLyrics) { if (isShowingSynchronizedLyrics) {
val density = LocalDensity.current val density = LocalDensity.current
val player = LocalPlayerServiceBinder.current?.player ?: return@AnimatedVisibility val player = LocalPlayerServiceBinder.current?.player
?: return@AnimatedVisibility
val synchronizedLyrics = remember(lyrics) { val synchronizedLyrics = remember(lyrics) {
SynchronizedLyrics(KuGou.Lyrics(lyrics).sentences) { SynchronizedLyrics(KuGou.Lyrics(lyrics).sentences) {
@@ -247,15 +241,20 @@ fun Lyrics(
} }
} }
val lazyListState = rememberLazyListState(synchronizedLyrics.index, with (density) { size.roundToPx() } / 6) val lazyListState = rememberLazyListState(
synchronizedLyrics.index,
with(density) { size.roundToPx() } / 6)
LaunchedEffect(synchronizedLyrics) { LaunchedEffect(synchronizedLyrics) {
val center = with (density) { size.roundToPx() } / 6 val center = with(density) { size.roundToPx() } / 6
while (isActive) { while (isActive) {
delay(50) delay(50)
if (synchronizedLyrics.update()) { if (synchronizedLyrics.update()) {
lazyListState.animateScrollToItem(synchronizedLyrics.index, center) lazyListState.animateScrollToItem(
synchronizedLyrics.index,
center
)
} }
} }
} }
@@ -289,89 +288,101 @@ fun Lyrics(
) )
} }
} }
}
val menuState = LocalMenuState.current Image(
painter = painterResource(R.drawable.ellipsis_horizontal),
contentDescription = null,
colorFilter = ColorFilter.tint(DefaultDarkColorPalette.text),
modifier = Modifier
.padding(all = 4.dp)
.clickable {
menuState.display {
Menu {
MenuEntry(
icon = R.drawable.time,
text = "Show ${if (isShowingSynchronizedLyrics) "un" else ""}synchronized lyrics",
secondaryText = if (isShowingSynchronizedLyrics) null else "Provided by kugou.com",
onClick = {
menuState.hide()
isShowingSynchronizedLyrics =
!isShowingSynchronizedLyrics
}
)
Image( MenuEntry(
painter = painterResource(R.drawable.ellipsis_horizontal), icon = R.drawable.pencil,
contentDescription = null, text = "Edit lyrics",
colorFilter = ColorFilter.tint(DefaultDarkColorPalette.text), onClick = {
modifier = Modifier menuState.hide()
.padding(all = 4.dp) state = state.copy(isEditing = true)
.clickable { }
menuState.display { )
Menu {
MenuEntry(
icon = R.drawable.time,
text = "Show ${if (isShowingSynchronizedLyrics) "un" else ""}synchronized lyrics",
secondaryText = if (isShowingSynchronizedLyrics) null else "Provided by kugou.com",
onClick = {
menuState.hide()
isShowingSynchronizedLyrics = !isShowingSynchronizedLyrics
}
)
MenuEntry( MenuEntry(
icon = R.drawable.pencil, icon = R.drawable.search,
text = "Edit lyrics", text = "Search lyrics online",
onClick = { onClick = {
menuState.hide() menuState.hide()
isEditingLyrics = true val mediaMetadata = mediaMetadataProvider()
}
)
MenuEntry( val intent =
icon = R.drawable.search, Intent(Intent.ACTION_WEB_SEARCH).apply {
text = "Search lyrics online", putExtra(
onClick = { SearchManager.QUERY,
menuState.hide() "${mediaMetadata.title} ${mediaMetadata.artist} lyrics"
val mediaMetadata = mediaMetadataProvider() )
val intent =
Intent(Intent.ACTION_WEB_SEARCH).apply {
putExtra(
SearchManager.QUERY,
"${mediaMetadata.title} ${mediaMetadata.artist} lyrics"
)
}
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
Toast
.makeText(
context,
"No browser app found!",
Toast.LENGTH_SHORT
)
.show()
} }
}
)
MenuEntry( if (intent.resolveActivity(context.packageManager) != null) {
icon = R.drawable.download, context.startActivity(intent)
text = "Fetch lyrics again", } else {
onClick = { Toast
menuState.hide() .makeText(
context,
"No browser app found!",
Toast.LENGTH_SHORT
)
.show()
}
}
)
MenuEntry(
icon = R.drawable.download,
text = "Fetch lyrics again",
onClick = {
menuState.hide()
if (state.lyrics == null) {
fetchLyrics()
} else {
query { query {
if (isShowingSynchronizedLyrics) { if (isShowingSynchronizedLyrics) {
Database.updateSynchronizedLyrics(mediaId, null) Database.updateSynchronizedLyrics(
mediaId,
null
)
} else { } else {
Database.updateLyrics(mediaId, null) Database.updateLyrics(mediaId, null)
} }
} }
} }
) }
} )
} }
} }
.padding(all = 8.dp) }
.size(20.dp) .padding(all = 8.dp)
.align(Alignment.BottomEnd) .size(20.dp)
) .align(Alignment.BottomEnd)
} )
} }
} }
} }
} }
private data class LyricsState(
val isLoading: Boolean = false,
val isEditing: Boolean = false,
val lyrics: String? = ".",
)

View File

@@ -25,10 +25,12 @@ fun relaunchableEffect(
@Composable @Composable
@NonRestartableComposable @NonRestartableComposable
fun relaunchableEffect2( fun relaunchableEffect(
key1: Any?, key1: Any?,
key2: Any?,
block: suspend CoroutineScope.() -> Unit block: suspend CoroutineScope.() -> Unit
): RememberObserver { ): () -> Unit {
val applyContext = currentComposer.applyCoroutineContext val applyContext = currentComposer.applyCoroutineContext
return remember(key1) { LaunchedEffectImpl(applyContext, block) } val launchedEffect = remember(key1, key2) { LaunchedEffectImpl(applyContext, block) }
return launchedEffect::onRemembered
} }