mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Show chapters & layout playback overlay
This commit is contained in:
parent
9a588f38b4
commit
f3df36e1a1
2 changed files with 206 additions and 56 deletions
|
|
@ -75,6 +75,7 @@ fun PlaybackContent(
|
|||
val audioStreams by viewModel.audioStreams.observeAsState(listOf())
|
||||
val subtitleStreams by viewModel.subtitleStreams.observeAsState(listOf())
|
||||
val trickplay by viewModel.trickplay.observeAsState(null)
|
||||
val chapters by viewModel.chapters.observeAsState(listOf())
|
||||
|
||||
if (stream == null) {
|
||||
// TODO loading
|
||||
|
|
@ -238,6 +239,7 @@ fun PlaybackContent(
|
|||
audioStreams = audioStreams,
|
||||
trickplayInfo = trickplay,
|
||||
trickplayUrlFor = viewModel::getTrickplayUrl,
|
||||
chapters = chapters,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,43 @@
|
|||
package com.github.damontecres.dolphin.ui.playback
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.Player
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.dolphin.data.model.Chapter
|
||||
import com.github.damontecres.dolphin.ui.cards.ChapterCard
|
||||
import com.github.damontecres.dolphin.ui.ifElse
|
||||
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||
import org.jellyfin.sdk.model.api.TrickplayInfo
|
||||
import kotlin.time.Duration
|
||||
|
||||
|
|
@ -24,6 +45,7 @@ import kotlin.time.Duration
|
|||
fun PlaybackOverlay(
|
||||
title: String?,
|
||||
subtitleStreams: List<SubtitleStream>,
|
||||
chapters: List<Chapter>,
|
||||
playerControls: Player,
|
||||
controllerViewState: ControllerViewState,
|
||||
showPlay: Boolean,
|
||||
|
|
@ -47,49 +69,202 @@ fun PlaybackOverlay(
|
|||
trickplayUrlFor: (Int) -> String? = { null },
|
||||
seekBarInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
// Will be used for preview/trick play images
|
||||
var seekProgressMs by remember { mutableLongStateOf(-1L) }
|
||||
var seekProgressPercent = (seekProgressMs.toDouble() / playerControls.duration).toFloat()
|
||||
val seekBarFocused by seekBarInteractionSource.collectIsFocusedAsState()
|
||||
// Will be used for preview/trick play images
|
||||
var seekProgressMs by remember(seekBarFocused) { mutableLongStateOf(playerControls.currentPosition) }
|
||||
var seekProgressPercent = (seekProgressMs.toDouble() / playerControls.duration).toFloat()
|
||||
|
||||
val chapterInteractionSources =
|
||||
remember(chapters.size) { List(chapters.size) { MutableInteractionSource() } }
|
||||
// val chapterRowFocused = chapterInteractionSources.any { it.collectIsFocusedAsState().value }
|
||||
var chapterRowFocused by remember { mutableStateOf(false) }
|
||||
|
||||
val titleTextStyle = MaterialTheme.typography.displaySmall
|
||||
val subtitleTextStyle = MaterialTheme.typography.headlineMedium
|
||||
val density = LocalDensity.current
|
||||
|
||||
val titleHeight =
|
||||
if (title.isNotNullOrBlank()) with(density) { titleTextStyle.fontSize.toDp() } else 0.dp
|
||||
val subtitleHeight =
|
||||
if (subtitle.isNotNullOrBlank()) with(density) { subtitleTextStyle.fontSize.toDp() } else 0.dp
|
||||
|
||||
// Calculate height based on content
|
||||
// Base height (with or w/o chapters) + title + subtitle
|
||||
// The extra 8dp is for padding between title, subtitle, and playback controls
|
||||
// When chapter row is focused, the title/subtitle/playback controls will be hidden, but need extra height for the chapter images
|
||||
val height by animateDpAsState(
|
||||
(if (chapters.isNotEmpty()) 184.dp else 140.dp) +
|
||||
(if (chapterRowFocused) 40.dp else 0.dp) +
|
||||
(
|
||||
if (!chapterRowFocused && title.isNotNullOrBlank()) {
|
||||
titleHeight + 8.dp
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
) +
|
||||
(
|
||||
if (!chapterRowFocused && subtitle.isNotNullOrBlank()) {
|
||||
subtitleHeight + 8.dp
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
) {
|
||||
Column {
|
||||
title?.let {
|
||||
Text(
|
||||
text = it,
|
||||
)
|
||||
}
|
||||
subtitle?.let {
|
||||
Text(
|
||||
text = it,
|
||||
)
|
||||
}
|
||||
if (seekBarInteractionSource.collectIsFocusedAsState().value) {
|
||||
LaunchedEffect(Unit) {
|
||||
seekProgressPercent =
|
||||
(playerControls.currentPosition.toFloat() / playerControls.duration)
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(bottom = if (chapterRowFocused) 32.dp else 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
.height(height)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
item {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
.alpha(if (chapterRowFocused) 0f else 1f),
|
||||
) {
|
||||
title?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = titleTextStyle,
|
||||
)
|
||||
}
|
||||
subtitle?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = subtitleTextStyle,
|
||||
)
|
||||
}
|
||||
}
|
||||
PlaybackControls(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.alpha(if (chapterRowFocused) 0f else 1f),
|
||||
subtitleStreams = subtitleStreams,
|
||||
playerControls = playerControls,
|
||||
onPlaybackActionClick = onPlaybackActionClick,
|
||||
controllerViewState = controllerViewState,
|
||||
showDebugInfo = showDebugInfo,
|
||||
onSeekProgress = {
|
||||
seekProgressMs = it
|
||||
onSeekBarChange(it)
|
||||
},
|
||||
showPlay = showPlay,
|
||||
previousEnabled = previousEnabled,
|
||||
nextEnabled = nextEnabled,
|
||||
seekEnabled = seekEnabled,
|
||||
seekBarInteractionSource = seekBarInteractionSource,
|
||||
moreButtonOptions = moreButtonOptions,
|
||||
subtitleIndex = subtitleIndex,
|
||||
audioIndex = audioIndex,
|
||||
audioStreams = audioStreams,
|
||||
playbackSpeed = playbackSpeed,
|
||||
scale = scale,
|
||||
seekBarIntervals = 16,
|
||||
seekBack = seekBack,
|
||||
seekForward = seekForward,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (trickplayInfo != null) {
|
||||
AnimatedVisibility(seekProgressPercent >= 0 && seekBarFocused) {
|
||||
val tilesPerImage = trickplayInfo.tileWidth * trickplayInfo.tileHeight
|
||||
val index =
|
||||
(seekProgressMs / trickplayInfo.interval).toInt() / tilesPerImage
|
||||
val imageUrl = trickplayUrlFor(index)
|
||||
|
||||
if (imageUrl != null) {
|
||||
if (chapters.isNotEmpty()) {
|
||||
item {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
// .offset(y = 200.dp)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Chapters",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRestorer(focusRequester)
|
||||
.onFocusChanged {
|
||||
if (it.hasFocus) {
|
||||
controllerViewState.pulseControls()
|
||||
}
|
||||
chapterRowFocused = it.hasFocus || it.isFocused
|
||||
},
|
||||
) {
|
||||
itemsIndexed(chapters) { index, chapter ->
|
||||
val interactionSource = chapterInteractionSources[index]
|
||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||
LaunchedEffect(isFocused) {
|
||||
if (isFocused) controllerViewState.pulseControls()
|
||||
}
|
||||
ChapterCard(
|
||||
name = chapter.name,
|
||||
position = chapter.position,
|
||||
imageUrl = chapter.imageUrl,
|
||||
onClick = {
|
||||
playerControls.seekTo(chapter.position.inWholeMilliseconds)
|
||||
controllerViewState.hideControls()
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
modifier =
|
||||
Modifier.ifElse(
|
||||
index == 0,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (seekBarInteractionSource.collectIsFocusedAsState().value) {
|
||||
LaunchedEffect(Unit) {
|
||||
seekProgressPercent =
|
||||
(playerControls.currentPosition.toFloat() / playerControls.duration)
|
||||
}
|
||||
}
|
||||
if (trickplayInfo != null) {
|
||||
AnimatedVisibility(
|
||||
seekProgressPercent >= 0 && seekBarFocused,
|
||||
) {
|
||||
val tilesPerImage = trickplayInfo.tileWidth * trickplayInfo.tileHeight
|
||||
val index =
|
||||
(seekProgressMs / trickplayInfo.interval).toInt() / tilesPerImage
|
||||
val imageUrl = trickplayUrlFor(index)
|
||||
|
||||
if (imageUrl != null) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.Center)
|
||||
.fillMaxWidth(.95f),
|
||||
) {
|
||||
SeekPreviewImage(
|
||||
modifier =
|
||||
Modifier
|
||||
// .align(Alignment.TopStart)
|
||||
.align(Alignment.BottomStart)
|
||||
.offsetByPercent(
|
||||
xPercentage = seekProgressPercent.coerceIn(0f, 1f),
|
||||
// yOffset = heightPx,
|
||||
// yPercentage = 1 - controlHeight,
|
||||
),
|
||||
).padding(bottom = height - titleHeight - subtitleHeight),
|
||||
previewImageUrl = imageUrl,
|
||||
duration = playerControls.duration,
|
||||
seekProgressMs = seekProgressMs,
|
||||
|
|
@ -100,33 +275,6 @@ fun PlaybackOverlay(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlaybackControls(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
subtitleStreams = subtitleStreams,
|
||||
playerControls = playerControls,
|
||||
onPlaybackActionClick = onPlaybackActionClick,
|
||||
controllerViewState = controllerViewState,
|
||||
showDebugInfo = showDebugInfo,
|
||||
onSeekProgress = {
|
||||
seekProgressMs = it
|
||||
onSeekBarChange(it)
|
||||
},
|
||||
showPlay = showPlay,
|
||||
previousEnabled = previousEnabled,
|
||||
nextEnabled = nextEnabled,
|
||||
seekEnabled = seekEnabled,
|
||||
seekBarInteractionSource = seekBarInteractionSource,
|
||||
moreButtonOptions = moreButtonOptions,
|
||||
subtitleIndex = subtitleIndex,
|
||||
audioIndex = audioIndex,
|
||||
audioStreams = audioStreams,
|
||||
playbackSpeed = playbackSpeed,
|
||||
scale = scale,
|
||||
seekBarIntervals = 16,
|
||||
seekBack = seekBack,
|
||||
seekForward = seekForward,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue