mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Yet another rewrite of the playback overlay
This commit is contained in:
parent
88038bfcfd
commit
005d6b11ce
9 changed files with 268 additions and 199 deletions
|
|
@ -35,7 +35,7 @@ fun ChapterRow(
|
||||||
LazyRow(
|
LazyRow(
|
||||||
state = rememberLazyListState(),
|
state = rememberLazyListState(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
contentPadding = PaddingValues(8.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 16.dp)
|
.padding(start = 16.dp)
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,6 @@ fun DialogPopup(
|
||||||
@Composable
|
@Composable
|
||||||
fun ScrollableDialog(
|
fun ScrollableDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
content: LazyListScope.() -> Unit,
|
content: LazyListScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val scrollAmount = 100f
|
val scrollAmount = 100f
|
||||||
|
|
@ -258,7 +257,7 @@ fun ScrollableDialog(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
content = content,
|
content = content,
|
||||||
modifier =
|
modifier =
|
||||||
modifier
|
Modifier
|
||||||
.width(600.dp)
|
.width(600.dp)
|
||||||
.heightIn(max = 380.dp)
|
.heightIn(max = 380.dp)
|
||||||
.focusable()
|
.focusable()
|
||||||
|
|
|
||||||
|
|
@ -21,36 +21,32 @@ data class ItemDetailsDialogInfo(
|
||||||
fun ItemDetailsDialog(
|
fun ItemDetailsDialog(
|
||||||
info: ItemDetailsDialogInfo,
|
info: ItemDetailsDialogInfo,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
) = ScrollableDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
) {
|
) {
|
||||||
ScrollableDialog(
|
item {
|
||||||
onDismissRequest = onDismissRequest,
|
Text(
|
||||||
modifier = modifier,
|
text = info.title,
|
||||||
) {
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (info.overview.isNotNullOrBlank()) {
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = info.title,
|
text = info.overview,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (info.overview.isNotNullOrBlank()) {
|
|
||||||
item {
|
|
||||||
Text(
|
|
||||||
text = info.overview,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (info.files.isNotEmpty()) {
|
|
||||||
item {
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items(info.files) { file ->
|
|
||||||
Text(
|
|
||||||
text = "- $file",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (info.files.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items(info.files) { file ->
|
||||||
|
Text(
|
||||||
|
text = "- $file",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,6 @@ fun SeriesDetails(
|
||||||
ItemDetailsDialog(
|
ItemDetailsDialog(
|
||||||
info = info,
|
info = info,
|
||||||
onDismissRequest = { overviewDialog = null },
|
onDismissRequest = { overviewDialog = null },
|
||||||
modifier = Modifier,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +201,7 @@ fun SeriesDetailsContent(
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = PaddingValues(bottom = 80.dp),
|
contentPadding = PaddingValues(bottom = 80.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
modifier = modifier,
|
modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
SeriesDetailsHeader(
|
SeriesDetailsHeader(
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,6 @@ fun MovieDetails(
|
||||||
ItemDetailsDialog(
|
ItemDetailsDialog(
|
||||||
info = info,
|
info = info,
|
||||||
onDismissRequest = { overviewDialog = null },
|
onDismissRequest = { overviewDialog = null },
|
||||||
modifier = Modifier,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
moreDialog?.let { params ->
|
moreDialog?.let { params ->
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,6 @@ fun SeriesOverview(
|
||||||
ItemDetailsDialog(
|
ItemDetailsDialog(
|
||||||
info = info,
|
info = info,
|
||||||
onDismissRequest = { overviewDialog = null },
|
onDismissRequest = { overviewDialog = null },
|
||||||
modifier = Modifier,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
moreDialog?.let { params ->
|
moreDialog?.let { params ->
|
||||||
|
|
|
||||||
|
|
@ -49,3 +49,7 @@ fun isForwardButton(event: KeyEvent): Boolean =
|
||||||
* Whether the [KeyEvent] is a key up event pressing media play or media play/pause
|
* Whether the [KeyEvent] is a key up event pressing media play or media play/pause
|
||||||
*/
|
*/
|
||||||
fun isPlayKeyUp(key: KeyEvent) = key.type == KeyEventType.KeyUp && (key.key == Key.MediaPlay || key.key == Key.MediaPlayPause)
|
fun isPlayKeyUp(key: KeyEvent) = key.type == KeyEventType.KeyUp && (key.key == Key.MediaPlay || key.key == Key.MediaPlayPause)
|
||||||
|
|
||||||
|
fun isUp(event: KeyEvent): Boolean = event.key == Key.DirectionUp || event.key == Key.SystemNavigationUp
|
||||||
|
|
||||||
|
fun isDown(event: KeyEvent): Boolean = event.key == Key.DirectionDown || event.key == Key.SystemNavigationDown
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package com.github.damontecres.dolphin.ui.playback
|
package com.github.damontecres.dolphin.ui.playback
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
|
|
@ -10,9 +13,7 @@ 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.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -24,12 +25,16 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import androidx.compose.ui.input.key.type
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
@ -42,9 +47,13 @@ import com.github.damontecres.dolphin.ui.cards.ChapterCard
|
||||||
import com.github.damontecres.dolphin.ui.ifElse
|
import com.github.damontecres.dolphin.ui.ifElse
|
||||||
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.dolphin.ui.letNotEmpty
|
import com.github.damontecres.dolphin.ui.letNotEmpty
|
||||||
|
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||||
import org.jellyfin.sdk.model.api.TrickplayInfo
|
import org.jellyfin.sdk.model.api.TrickplayInfo
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
private val titleTextSize = 28.sp
|
||||||
|
private val subtitleTextSize = 18.sp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PlaybackOverlay(
|
fun PlaybackOverlay(
|
||||||
title: String?,
|
title: String?,
|
||||||
|
|
@ -80,171 +89,144 @@ fun PlaybackOverlay(
|
||||||
|
|
||||||
val chapterInteractionSources =
|
val chapterInteractionSources =
|
||||||
remember(chapters.size) { List(chapters.size) { MutableInteractionSource() } }
|
remember(chapters.size) { List(chapters.size) { MutableInteractionSource() } }
|
||||||
// val chapterRowFocused = chapterInteractionSources.any { it.collectIsFocusedAsState().value }
|
|
||||||
var chapterRowFocused by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val titleTextSize = 28.sp
|
|
||||||
val subtitleTextSize = 18.sp
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
val titleHeight =
|
val titleHeight =
|
||||||
if (title.isNotNullOrBlank()) with(density) { titleTextSize.toDp() } else 0.dp
|
remember {
|
||||||
|
if (title.isNotNullOrBlank()) with(density) { titleTextSize.toDp() } else 0.dp
|
||||||
|
}
|
||||||
val subtitleHeight =
|
val subtitleHeight =
|
||||||
if (subtitle.isNotNullOrBlank()) with(density) { subtitleTextSize.toDp() } else 0.dp
|
remember {
|
||||||
|
if (subtitle.isNotNullOrBlank()) with(density) { subtitleTextSize.toDp() } else 0.dp
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate height based on content
|
// This will be calculated after composition
|
||||||
// Base height (with or w/o chapters) + title + subtitle
|
var controllerHeight by remember { mutableStateOf(0.dp) }
|
||||||
// The extra 8dp is for padding between title, subtitle, and playback controls
|
var state by remember { mutableStateOf(ViewState.CONTROLLER) }
|
||||||
// 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(
|
|
||||||
96.dp +
|
|
||||||
(if (chapters.isNotEmpty()) 40.dp else 0.dp) +
|
|
||||||
(if (chapterRowFocused) 80.dp else 0.dp) +
|
|
||||||
(
|
|
||||||
if (!chapterRowFocused && title.isNotNullOrBlank()) {
|
|
||||||
titleHeight + 12.dp
|
|
||||||
} else {
|
|
||||||
0.dp
|
|
||||||
}
|
|
||||||
) +
|
|
||||||
(
|
|
||||||
if (!chapterRowFocused && subtitle.isNotNullOrBlank()) {
|
|
||||||
subtitleHeight + 12.dp
|
|
||||||
} else {
|
|
||||||
0.dp
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
contentAlignment = Alignment.BottomCenter,
|
contentAlignment = Alignment.BottomCenter,
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
AnimatedVisibility(
|
||||||
contentPadding = PaddingValues(bottom = if (chapterRowFocused) 32.dp else 8.dp),
|
state == ViewState.CONTROLLER,
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
enter = slideInVertically() + fadeIn(),
|
||||||
modifier =
|
exit = slideOutVertically() + fadeOut(),
|
||||||
Modifier
|
|
||||||
.padding(bottom = 16.dp)
|
|
||||||
.height(height)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
) {
|
) {
|
||||||
item {
|
Controller(
|
||||||
|
title = title,
|
||||||
|
subtitleStreams = subtitleStreams,
|
||||||
|
chapters = chapters,
|
||||||
|
playerControls = playerControls,
|
||||||
|
controllerViewState = controllerViewState,
|
||||||
|
showPlay = showPlay,
|
||||||
|
previousEnabled = previousEnabled,
|
||||||
|
nextEnabled = nextEnabled,
|
||||||
|
seekEnabled = seekEnabled,
|
||||||
|
seekBack = seekBack,
|
||||||
|
skipBackOnResume = skipBackOnResume,
|
||||||
|
seekForward = seekForward,
|
||||||
|
onPlaybackActionClick = onPlaybackActionClick,
|
||||||
|
onSeekProgress = {
|
||||||
|
onSeekBarChange(it)
|
||||||
|
seekProgressMs = it
|
||||||
|
},
|
||||||
|
showDebugInfo = showDebugInfo,
|
||||||
|
scale = scale,
|
||||||
|
playbackSpeed = playbackSpeed,
|
||||||
|
moreButtonOptions = moreButtonOptions,
|
||||||
|
currentPlayback = currentPlayback,
|
||||||
|
audioStreams = audioStreams,
|
||||||
|
subtitle = subtitle,
|
||||||
|
seekBarInteractionSource = seekBarInteractionSource,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.onKeyEvent { e ->
|
||||||
|
if (chapters.isNotEmpty() &&
|
||||||
|
e.type == KeyEventType.KeyDown && isDown(e) &&
|
||||||
|
!seekBarFocused
|
||||||
|
) {
|
||||||
|
state = ViewState.CHAPTERS
|
||||||
|
true
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}.onGloballyPositioned {
|
||||||
|
controllerHeight = with(density) { it.size.height.toDp() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AnimatedVisibility(
|
||||||
|
state == ViewState.CHAPTERS,
|
||||||
|
enter = slideInVertically { it / 2 } + fadeIn(),
|
||||||
|
exit = slideOutVertically { it / 2 } + fadeOut(),
|
||||||
|
) {
|
||||||
|
if (chapters.isNotEmpty()) {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = Modifier,
|
modifier =
|
||||||
) {
|
Modifier
|
||||||
Column(
|
.fillMaxWidth()
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
.padding(8.dp)
|
||||||
modifier =
|
.onPreviewKeyEvent { e ->
|
||||||
Modifier
|
if (e.type == KeyEventType.KeyUp && isUp(e)) {
|
||||||
.padding(start = 16.dp)
|
state = ViewState.CONTROLLER
|
||||||
.alpha(if (chapterRowFocused) 0f else 1f),
|
true
|
||||||
) {
|
|
||||||
title?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontSize = titleTextSize,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
subtitle?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontSize = subtitleTextSize,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 = currentPlayback?.subtitleIndex,
|
|
||||||
audioIndex = currentPlayback?.audioIndex,
|
|
||||||
audioStreams = audioStreams,
|
|
||||||
playbackSpeed = playbackSpeed,
|
|
||||||
scale = scale,
|
|
||||||
seekBarIntervals = 16,
|
|
||||||
seekBack = seekBack,
|
|
||||||
seekForward = seekForward,
|
|
||||||
skipBackOnResume = skipBackOnResume,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
false
|
||||||
name = chapter.name,
|
},
|
||||||
position = chapter.position,
|
) {
|
||||||
imageUrl = chapter.imageUrl,
|
Text(
|
||||||
onClick = {
|
text = "Chapters",
|
||||||
playerControls.seekTo(chapter.position.inWholeMilliseconds)
|
style = MaterialTheme.typography.titleLarge,
|
||||||
controllerViewState.hideControls()
|
)
|
||||||
},
|
LazyRow(
|
||||||
interactionSource = interactionSource,
|
contentPadding = PaddingValues(16.dp),
|
||||||
modifier =
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
Modifier.ifElse(
|
modifier =
|
||||||
index == 0,
|
Modifier
|
||||||
Modifier.focusRequester(focusRequester),
|
.fillMaxWidth()
|
||||||
),
|
.focusRestorer(focusRequester)
|
||||||
)
|
.onFocusChanged {
|
||||||
|
if (it.hasFocus) {
|
||||||
|
controllerViewState.pulseControls()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
when (state) {
|
||||||
|
ViewState.CONTROLLER -> {}
|
||||||
|
|
||||||
|
ViewState.CHAPTERS -> {}
|
||||||
|
}
|
||||||
|
|
||||||
if (seekBarInteractionSource.collectIsFocusedAsState().value) {
|
if (seekBarInteractionSource.collectIsFocusedAsState().value) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
seekProgressPercent =
|
seekProgressPercent =
|
||||||
|
|
@ -273,7 +255,7 @@ fun PlaybackOverlay(
|
||||||
.align(Alignment.BottomStart)
|
.align(Alignment.BottomStart)
|
||||||
.offsetByPercent(
|
.offsetByPercent(
|
||||||
xPercentage = seekProgressPercent.coerceIn(0f, 1f),
|
xPercentage = seekProgressPercent.coerceIn(0f, 1f),
|
||||||
).padding(bottom = height - titleHeight - subtitleHeight),
|
).padding(bottom = controllerHeight - titleHeight - subtitleHeight),
|
||||||
previewImageUrl = imageUrl,
|
previewImageUrl = imageUrl,
|
||||||
duration = playerControls.duration,
|
duration = playerControls.duration,
|
||||||
seekProgressMs = seekProgressMs,
|
seekProgressMs = seekProgressMs,
|
||||||
|
|
@ -284,23 +266,113 @@ fun PlaybackOverlay(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
AnimatedVisibility(
|
||||||
AnimatedVisibility(
|
showDebugInfo && controllerViewState.controlsVisible,
|
||||||
showDebugInfo && controllerViewState.controlsVisible,
|
modifier =
|
||||||
modifier =
|
Modifier
|
||||||
Modifier
|
.align(Alignment.TopStart),
|
||||||
.align(Alignment.TopStart),
|
) {
|
||||||
) {
|
currentPlayback?.tracks?.letNotEmpty {
|
||||||
currentPlayback?.tracks?.letNotEmpty {
|
PlaybackTrackInfo(
|
||||||
PlaybackTrackInfo(
|
trackSupport = it,
|
||||||
trackSupport = it,
|
modifier =
|
||||||
modifier =
|
Modifier
|
||||||
Modifier
|
.align(Alignment.TopStart)
|
||||||
.align(Alignment.TopStart)
|
.padding(16.dp)
|
||||||
.padding(16.dp)
|
.background(AppColors.TransparentBlack50),
|
||||||
.background(AppColors.TransparentBlack50),
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class ViewState {
|
||||||
|
CONTROLLER,
|
||||||
|
CHAPTERS,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Controller(
|
||||||
|
title: String?,
|
||||||
|
subtitleStreams: List<SubtitleStream>,
|
||||||
|
chapters: List<Chapter>,
|
||||||
|
playerControls: Player,
|
||||||
|
controllerViewState: ControllerViewState,
|
||||||
|
showPlay: Boolean,
|
||||||
|
previousEnabled: Boolean,
|
||||||
|
nextEnabled: Boolean,
|
||||||
|
seekEnabled: Boolean,
|
||||||
|
seekBack: Duration,
|
||||||
|
skipBackOnResume: Duration?,
|
||||||
|
seekForward: Duration,
|
||||||
|
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
||||||
|
onSeekProgress: (Long) -> Unit,
|
||||||
|
showDebugInfo: Boolean,
|
||||||
|
scale: ContentScale,
|
||||||
|
playbackSpeed: Float,
|
||||||
|
moreButtonOptions: MoreButtonOptions,
|
||||||
|
currentPlayback: CurrentPlayback?,
|
||||||
|
audioStreams: List<AudioStream>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
subtitle: String? = null,
|
||||||
|
seekBarInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.padding(start = 16.dp),
|
||||||
|
) {
|
||||||
|
title?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontSize = titleTextSize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
subtitle?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontSize = subtitleTextSize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PlaybackControls(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
subtitleStreams = subtitleStreams,
|
||||||
|
playerControls = playerControls,
|
||||||
|
onPlaybackActionClick = onPlaybackActionClick,
|
||||||
|
controllerViewState = controllerViewState,
|
||||||
|
showDebugInfo = showDebugInfo,
|
||||||
|
onSeekProgress = {
|
||||||
|
onSeekProgress(it)
|
||||||
|
},
|
||||||
|
showPlay = showPlay,
|
||||||
|
previousEnabled = previousEnabled,
|
||||||
|
nextEnabled = nextEnabled,
|
||||||
|
seekEnabled = seekEnabled,
|
||||||
|
seekBarInteractionSource = seekBarInteractionSource,
|
||||||
|
moreButtonOptions = moreButtonOptions,
|
||||||
|
subtitleIndex = currentPlayback?.subtitleIndex,
|
||||||
|
audioIndex = currentPlayback?.audioIndex,
|
||||||
|
audioStreams = audioStreams,
|
||||||
|
playbackSpeed = playbackSpeed,
|
||||||
|
scale = scale,
|
||||||
|
seekBarIntervals = 16,
|
||||||
|
seekBack = seekBack,
|
||||||
|
seekForward = seekForward,
|
||||||
|
skipBackOnResume = skipBackOnResume,
|
||||||
|
)
|
||||||
|
if (chapters.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "Chapters",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(start = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
|
|
@ -113,7 +112,9 @@ fun IntervalSeekBarImpl(
|
||||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||||
var hasSeeked by remember { mutableStateOf(false) }
|
var hasSeeked by remember { mutableStateOf(false) }
|
||||||
var seekPositionMs by remember { mutableLongStateOf((progress * durationMs).toLong()) }
|
var seekPositionMs by remember { mutableLongStateOf((progress * durationMs).toLong()) }
|
||||||
val progressToUse by remember { derivedStateOf { if (isFocused && hasSeeked) seekPositionMs else (progress * durationMs).toLong() } }
|
// val progressToUse by remember { derivedStateOf { if (isFocused && hasSeeked) seekPositionMs else (progress * durationMs).toLong() } }
|
||||||
|
val progressToUse =
|
||||||
|
if (isFocused && hasSeeked) seekPositionMs else (progress * durationMs).toLong()
|
||||||
|
|
||||||
LaunchedEffect(isFocused) {
|
LaunchedEffect(isFocused) {
|
||||||
if (!isFocused) hasSeeked = false
|
if (!isFocused) hasSeeked = false
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue