mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Make lyrics scrollable & clickable
This commit is contained in:
parent
1a22980a51
commit
9f9552f870
2 changed files with 92 additions and 42 deletions
|
|
@ -3,12 +3,9 @@ package com.github.damontecres.wholphin.ui.detail.music
|
|||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
|
@ -24,27 +21,38 @@ import androidx.compose.runtime.LaunchedEffect
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Surface
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import org.jellyfin.sdk.model.api.LyricDto
|
||||
import org.jellyfin.sdk.model.api.LyricLine
|
||||
|
||||
@Composable
|
||||
fun LyricsContent(
|
||||
lyricsHaveFocus: Boolean,
|
||||
synced: Boolean,
|
||||
lyrics: LyricDto?,
|
||||
currentLyricPosition: Int?,
|
||||
onClick: (LyricLine) -> Unit,
|
||||
onFocusLyrics: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val focusRequesters =
|
||||
remember(lyrics) { List(lyrics?.lyrics.orEmpty().size) { FocusRequester() } }
|
||||
val listState = rememberLazyListState(currentLyricPosition ?: 0)
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
if (synced) {
|
||||
LaunchedEffect(currentLyricPosition) {
|
||||
if (currentLyricPosition != null) {
|
||||
if (currentLyricPosition != null && !lyricsHaveFocus) {
|
||||
listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index?.let {
|
||||
if (currentLyricPosition !in 0..it) {
|
||||
listState.animateScrollToItem(currentLyricPosition)
|
||||
|
|
@ -58,11 +66,30 @@ fun LyricsContent(
|
|||
LazyColumn(
|
||||
state = listState,
|
||||
contentPadding = PaddingValues(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.focusProperties {
|
||||
onEnter = {
|
||||
if (currentLyricPosition != null) {
|
||||
currentLyricPosition.let {
|
||||
focusRequesters
|
||||
.getOrNull(currentLyricPosition)
|
||||
?.tryRequestFocus()
|
||||
}
|
||||
} else {
|
||||
focusRequesters.getOrNull(0)?.tryRequestFocus()
|
||||
}
|
||||
}
|
||||
}.onFocusChanged {
|
||||
onFocusLyrics.invoke(it.hasFocus)
|
||||
},
|
||||
) {
|
||||
if (lyrics?.lyrics?.isNotEmpty() == true) {
|
||||
itemsIndexed(lyrics.lyrics) { index, lyric ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val color by animateColorAsState(
|
||||
if (index == currentLyricPosition || currentLyricPosition == null) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
|
|
@ -71,28 +98,32 @@ fun LyricsContent(
|
|||
},
|
||||
animationSpec = tween(durationMillis = 500, easing = LinearEasing),
|
||||
)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
Box(
|
||||
Surface(
|
||||
onClick = { onClick.invoke(lyric) },
|
||||
interactionSource = interactionSource,
|
||||
colors =
|
||||
ClickableSurfaceDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
focusedContainerColor = MaterialTheme.colorScheme.border.copy(alpha = .33f),
|
||||
),
|
||||
shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(8.dp)),
|
||||
scale =
|
||||
ClickableSurfaceDefaults.scale(
|
||||
focusedScale = 1f,
|
||||
pressedScale = .9f,
|
||||
),
|
||||
modifier =
|
||||
Modifier
|
||||
.background(
|
||||
color = if (focused) MaterialTheme.colorScheme.border.copy(alpha = .66f) else Color.Unspecified,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
),
|
||||
.focusRequester(focusRequesters[index]),
|
||||
) {
|
||||
Text(
|
||||
text = lyric.text,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = color,
|
||||
color = if (focused) MaterialTheme.colorScheme.onSurface else color,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(4.dp)
|
||||
.clickable(
|
||||
enabled = !synced,
|
||||
onClick = { onClick.invoke(lyric) },
|
||||
interactionSource = interactionSource,
|
||||
).ifElse(
|
||||
.padding(8.dp)
|
||||
.ifElse(
|
||||
index == currentLyricPosition,
|
||||
Modifier.bringIntoViewRequester(bringIntoViewRequester),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ import com.github.damontecres.wholphin.ui.playback.BottomDialogItem
|
|||
import com.github.damontecres.wholphin.ui.playback.PlaybackKeyHandler
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
|
|
@ -124,9 +125,20 @@ fun NowPlayingPage(
|
|||
|
||||
var showViewOptionsDialog by remember { mutableStateOf(false) }
|
||||
var itemMoreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
var lyricsHaveFocus by remember { mutableStateOf(false) }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||
|
||||
LaunchedEffect(lyricsHaveFocus) {
|
||||
if (lyricsHaveFocus) {
|
||||
controllerViewState.hideControls()
|
||||
}
|
||||
}
|
||||
BackHandler(lyricsHaveFocus) {
|
||||
focusRequester.tryRequestFocus()
|
||||
}
|
||||
|
||||
Box(modifier) {
|
||||
Box(
|
||||
modifier =
|
||||
|
|
@ -193,31 +205,38 @@ fun NowPlayingPage(
|
|||
.align(Alignment.CenterStart),
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = musicPrefs.showLyrics && state.hasLyrics,
|
||||
enter = expandHorizontally(expandFrom = Alignment.End),
|
||||
exit = shrinkHorizontally(shrinkTowards = Alignment.End),
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = musicPrefs.showLyrics && state.hasLyrics,
|
||||
enter = expandHorizontally(expandFrom = Alignment.End),
|
||||
exit = shrinkHorizontally(shrinkTowards = Alignment.End),
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(.5f)
|
||||
.fillMaxHeight(),
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
LyricsContent(
|
||||
synced = true,
|
||||
lyrics = state.lyrics,
|
||||
currentLyricPosition = state.currentLyricIndex,
|
||||
lyricsHaveFocus = lyricsHaveFocus,
|
||||
onFocusLyrics = { lyricsHaveFocus = it },
|
||||
onClick = {
|
||||
it.start
|
||||
?.ticks
|
||||
?.inWholeMilliseconds
|
||||
?.let { player.seekTo(it) }
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth(.5f)
|
||||
.fillMaxHeight(),
|
||||
) {
|
||||
LyricsContent(
|
||||
synced = true,
|
||||
lyrics = state.lyrics,
|
||||
currentLyricPosition = state.currentLyricIndex,
|
||||
onClick = {},
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(vertical = 120.dp)
|
||||
.fillMaxHeight()
|
||||
.width(320.dp),
|
||||
)
|
||||
}
|
||||
.padding(vertical = 120.dp)
|
||||
.fillMaxHeight()
|
||||
.width(320.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
val showContextForItem =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue