Make lyrics scrollable & clickable

This commit is contained in:
Damontecres 2026-03-06 14:53:34 -05:00
parent 1a22980a51
commit 9f9552f870
No known key found for this signature in database
2 changed files with 92 additions and 42 deletions

View file

@ -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),
),

View file

@ -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,6 +205,7 @@ fun NowPlayingPage(
.align(Alignment.CenterStart),
)
}
}
AnimatedVisibility(
visible = musicPrefs.showLyrics && state.hasLyrics,
enter = expandHorizontally(expandFrom = Alignment.End),
@ -210,7 +223,14 @@ fun NowPlayingPage(
synced = true,
lyrics = state.lyrics,
currentLyricPosition = state.currentLyricIndex,
onClick = {},
lyricsHaveFocus = lyricsHaveFocus,
onFocusLyrics = { lyricsHaveFocus = it },
onClick = {
it.start
?.ticks
?.inWholeMilliseconds
?.let { player.seekTo(it) }
},
modifier =
Modifier
.padding(vertical = 120.dp)
@ -219,7 +239,6 @@ fun NowPlayingPage(
)
}
}
}
val showContextForItem =
remember {
{ fromLongClick: Boolean, index: Int, song: AudioItem ->