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.animateColorAsState
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
import androidx.compose.animation.core.tween
|
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.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
|
@ -24,27 +21,38 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
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.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Surface
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
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.LyricDto
|
||||||
import org.jellyfin.sdk.model.api.LyricLine
|
import org.jellyfin.sdk.model.api.LyricLine
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LyricsContent(
|
fun LyricsContent(
|
||||||
|
lyricsHaveFocus: Boolean,
|
||||||
synced: Boolean,
|
synced: Boolean,
|
||||||
lyrics: LyricDto?,
|
lyrics: LyricDto?,
|
||||||
currentLyricPosition: Int?,
|
currentLyricPosition: Int?,
|
||||||
onClick: (LyricLine) -> Unit,
|
onClick: (LyricLine) -> Unit,
|
||||||
|
onFocusLyrics: (Boolean) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val focusRequesters =
|
||||||
|
remember(lyrics) { List(lyrics?.lyrics.orEmpty().size) { FocusRequester() } }
|
||||||
val listState = rememberLazyListState(currentLyricPosition ?: 0)
|
val listState = rememberLazyListState(currentLyricPosition ?: 0)
|
||||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
if (synced) {
|
if (synced) {
|
||||||
LaunchedEffect(currentLyricPosition) {
|
LaunchedEffect(currentLyricPosition) {
|
||||||
if (currentLyricPosition != null) {
|
if (currentLyricPosition != null && !lyricsHaveFocus) {
|
||||||
listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index?.let {
|
listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index?.let {
|
||||||
if (currentLyricPosition !in 0..it) {
|
if (currentLyricPosition !in 0..it) {
|
||||||
listState.animateScrollToItem(currentLyricPosition)
|
listState.animateScrollToItem(currentLyricPosition)
|
||||||
|
|
@ -58,11 +66,30 @@ fun LyricsContent(
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
contentPadding = PaddingValues(),
|
contentPadding = PaddingValues(),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
modifier = Modifier.fillMaxSize(),
|
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) {
|
if (lyrics?.lyrics?.isNotEmpty() == true) {
|
||||||
itemsIndexed(lyrics.lyrics) { index, lyric ->
|
itemsIndexed(lyrics.lyrics) { index, lyric ->
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val focused by interactionSource.collectIsFocusedAsState()
|
||||||
val color by animateColorAsState(
|
val color by animateColorAsState(
|
||||||
if (index == currentLyricPosition || currentLyricPosition == null) {
|
if (index == currentLyricPosition || currentLyricPosition == null) {
|
||||||
MaterialTheme.colorScheme.onSurface
|
MaterialTheme.colorScheme.onSurface
|
||||||
|
|
@ -71,28 +98,32 @@ fun LyricsContent(
|
||||||
},
|
},
|
||||||
animationSpec = tween(durationMillis = 500, easing = LinearEasing),
|
animationSpec = tween(durationMillis = 500, easing = LinearEasing),
|
||||||
)
|
)
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
Surface(
|
||||||
val focused by interactionSource.collectIsFocusedAsState()
|
onClick = { onClick.invoke(lyric) },
|
||||||
Box(
|
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 =
|
||||||
Modifier
|
Modifier
|
||||||
.background(
|
.focusRequester(focusRequesters[index]),
|
||||||
color = if (focused) MaterialTheme.colorScheme.border.copy(alpha = .66f) else Color.Unspecified,
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = lyric.text,
|
text = lyric.text,
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = color,
|
color = if (focused) MaterialTheme.colorScheme.onSurface else color,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(4.dp)
|
.padding(8.dp)
|
||||||
.clickable(
|
.ifElse(
|
||||||
enabled = !synced,
|
|
||||||
onClick = { onClick.invoke(lyric) },
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
).ifElse(
|
|
||||||
index == currentLyricPosition,
|
index == currentLyricPosition,
|
||||||
Modifier.bringIntoViewRequester(bringIntoViewRequester),
|
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.playback.PlaybackKeyHandler
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
|
|
@ -124,9 +125,20 @@ fun NowPlayingPage(
|
||||||
|
|
||||||
var showViewOptionsDialog by remember { mutableStateOf(false) }
|
var showViewOptionsDialog by remember { mutableStateOf(false) }
|
||||||
var itemMoreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
var itemMoreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||||
|
var lyricsHaveFocus by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||||
|
|
||||||
|
LaunchedEffect(lyricsHaveFocus) {
|
||||||
|
if (lyricsHaveFocus) {
|
||||||
|
controllerViewState.hideControls()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackHandler(lyricsHaveFocus) {
|
||||||
|
focusRequester.tryRequestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
|
|
@ -193,31 +205,38 @@ fun NowPlayingPage(
|
||||||
.align(Alignment.CenterStart),
|
.align(Alignment.CenterStart),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AnimatedVisibility(
|
}
|
||||||
visible = musicPrefs.showLyrics && state.hasLyrics,
|
AnimatedVisibility(
|
||||||
enter = expandHorizontally(expandFrom = Alignment.End),
|
visible = musicPrefs.showLyrics && state.hasLyrics,
|
||||||
exit = shrinkHorizontally(shrinkTowards = Alignment.End),
|
enter = expandHorizontally(expandFrom = Alignment.End),
|
||||||
modifier = Modifier.align(Alignment.CenterEnd),
|
exit = shrinkHorizontally(shrinkTowards = Alignment.End),
|
||||||
|
modifier = Modifier.align(Alignment.CenterEnd),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(.5f)
|
||||||
|
.fillMaxHeight(),
|
||||||
) {
|
) {
|
||||||
Box(
|
LyricsContent(
|
||||||
contentAlignment = Alignment.Center,
|
synced = true,
|
||||||
|
lyrics = state.lyrics,
|
||||||
|
currentLyricPosition = state.currentLyricIndex,
|
||||||
|
lyricsHaveFocus = lyricsHaveFocus,
|
||||||
|
onFocusLyrics = { lyricsHaveFocus = it },
|
||||||
|
onClick = {
|
||||||
|
it.start
|
||||||
|
?.ticks
|
||||||
|
?.inWholeMilliseconds
|
||||||
|
?.let { player.seekTo(it) }
|
||||||
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth(.5f)
|
.padding(vertical = 120.dp)
|
||||||
.fillMaxHeight(),
|
.fillMaxHeight()
|
||||||
) {
|
.width(320.dp),
|
||||||
LyricsContent(
|
)
|
||||||
synced = true,
|
|
||||||
lyrics = state.lyrics,
|
|
||||||
currentLyricPosition = state.currentLyricIndex,
|
|
||||||
onClick = {},
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.padding(vertical = 120.dp)
|
|
||||||
.fillMaxHeight()
|
|
||||||
.width(320.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val showContextForItem =
|
val showContextForItem =
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue