mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +00:00
feat: player lock
This commit is contained in:
parent
2f032bae81
commit
4a04e12e42
2 changed files with 265 additions and 28 deletions
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
|
@ -21,6 +22,8 @@ import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Forward10
|
import androidx.compose.material.icons.rounded.Forward10
|
||||||
|
import androidx.compose.material.icons.rounded.Lock
|
||||||
|
import androidx.compose.material.icons.rounded.LockOpen
|
||||||
import androidx.compose.material.icons.rounded.Replay10
|
import androidx.compose.material.icons.rounded.Replay10
|
||||||
import androidx.compose.material.icons.rounded.Speed
|
import androidx.compose.material.icons.rounded.Speed
|
||||||
import androidx.compose.material.icons.rounded.SwapHoriz
|
import androidx.compose.material.icons.rounded.SwapHoriz
|
||||||
|
|
@ -29,6 +32,7 @@ import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.SliderDefaults
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -61,6 +65,8 @@ internal fun PlayerControlsShell(
|
||||||
displayedPositionMs: Long,
|
displayedPositionMs: Long,
|
||||||
metrics: PlayerLayoutMetrics,
|
metrics: PlayerLayoutMetrics,
|
||||||
resizeMode: PlayerResizeMode,
|
resizeMode: PlayerResizeMode,
|
||||||
|
isLocked: Boolean,
|
||||||
|
onLockToggle: () -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onTogglePlayback: () -> Unit,
|
onTogglePlayback: () -> Unit,
|
||||||
onSeekBack: () -> Unit,
|
onSeekBack: () -> Unit,
|
||||||
|
|
@ -120,6 +126,8 @@ internal fun PlayerControlsShell(
|
||||||
episodeNumber = episodeNumber,
|
episodeNumber = episodeNumber,
|
||||||
episodeTitle = episodeTitle,
|
episodeTitle = episodeTitle,
|
||||||
metrics = metrics,
|
metrics = metrics,
|
||||||
|
isLocked = isLocked,
|
||||||
|
onLockToggle = onLockToggle,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopStart)
|
.align(Alignment.TopStart)
|
||||||
|
|
@ -175,6 +183,8 @@ private fun PlayerHeader(
|
||||||
episodeNumber: Int?,
|
episodeNumber: Int?,
|
||||||
episodeTitle: String?,
|
episodeTitle: String?,
|
||||||
metrics: PlayerLayoutMetrics,
|
metrics: PlayerLayoutMetrics,
|
||||||
|
isLocked: Boolean,
|
||||||
|
onLockToggle: () -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -240,18 +250,55 @@ private fun PlayerHeader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NuvioBackButton(
|
Row(
|
||||||
onClick = onBack,
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
containerColor = Color.Black.copy(alpha = 0.35f),
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
contentColor = Color.White,
|
) {
|
||||||
buttonSize = metrics.headerIconSize + 16.dp,
|
PlayerHeaderIconButton(
|
||||||
iconSize = metrics.headerIconSize,
|
icon = if (isLocked) Icons.Rounded.LockOpen else Icons.Rounded.Lock,
|
||||||
contentDescription = "Close player",
|
contentDescription = if (isLocked) "Unlock player controls" else "Lock player controls",
|
||||||
)
|
buttonSize = metrics.headerIconSize + 16.dp,
|
||||||
|
iconSize = metrics.headerIconSize,
|
||||||
|
onClick = onLockToggle,
|
||||||
|
)
|
||||||
|
NuvioBackButton(
|
||||||
|
onClick = onBack,
|
||||||
|
containerColor = Color.Black.copy(alpha = 0.35f),
|
||||||
|
contentColor = Color.White,
|
||||||
|
buttonSize = metrics.headerIconSize + 16.dp,
|
||||||
|
iconSize = metrics.headerIconSize,
|
||||||
|
contentDescription = "Close player",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PlayerHeaderIconButton(
|
||||||
|
icon: ImageVector,
|
||||||
|
contentDescription: String,
|
||||||
|
buttonSize: androidx.compose.ui.unit.Dp,
|
||||||
|
iconSize: androidx.compose.ui.unit.Dp,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(buttonSize)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color.Black.copy(alpha = 0.35f))
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(iconSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CenterControls(
|
private fun CenterControls(
|
||||||
snapshot: PlayerPlaybackSnapshot,
|
snapshot: PlayerPlaybackSnapshot,
|
||||||
|
|
@ -446,6 +493,105 @@ private fun ProgressControls(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun LockedPlayerOverlay(
|
||||||
|
playbackSnapshot: PlayerPlaybackSnapshot,
|
||||||
|
displayedPositionMs: Long,
|
||||||
|
metrics: PlayerLayoutMetrics,
|
||||||
|
horizontalSafePadding: androidx.compose.ui.unit.Dp,
|
||||||
|
onUnlock: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val durationMs = playbackSnapshot.durationMs.coerceAtLeast(1L)
|
||||||
|
val sliderColors = SliderDefaults.colors(
|
||||||
|
thumbColor = Color.White,
|
||||||
|
activeTrackColor = Color.White,
|
||||||
|
inactiveTrackColor = Color.White.copy(alpha = 0.28f),
|
||||||
|
disabledThumbColor = Color.White,
|
||||||
|
disabledActiveTrackColor = Color.White,
|
||||||
|
disabledInactiveTrackColor = Color.White.copy(alpha = 0.28f),
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(220.dp)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black.copy(alpha = 0.72f),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.padding(horizontal = 24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(78.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color.Black.copy(alpha = 0.52f))
|
||||||
|
.border(1.dp, Color.White.copy(alpha = 0.18f), CircleShape)
|
||||||
|
.clickable(onClick = onUnlock),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Lock,
|
||||||
|
contentDescription = "Unlock player controls",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(34.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Text(
|
||||||
|
text = "Tap to unlock",
|
||||||
|
style = MaterialTheme.nuvioTypeScale.bodyMd.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
color = Color.White.copy(alpha = 0.92f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = horizontalSafePadding + metrics.horizontalPadding)
|
||||||
|
.padding(bottom = metrics.sliderBottomOffset),
|
||||||
|
) {
|
||||||
|
Slider(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(metrics.sliderTouchHeight)
|
||||||
|
.graphicsLayer(scaleY = metrics.sliderScaleY),
|
||||||
|
value = displayedPositionMs.coerceIn(0L, durationMs).toFloat(),
|
||||||
|
onValueChange = {},
|
||||||
|
onValueChangeFinished = {},
|
||||||
|
valueRange = 0f..durationMs.toFloat(),
|
||||||
|
enabled = false,
|
||||||
|
colors = sliderColors,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 14.dp)
|
||||||
|
.padding(top = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
TimePill(text = formatPlaybackTime(displayedPositionMs), fontSize = metrics.timeSize)
|
||||||
|
TimePill(text = formatPlaybackTime(durationMs), fontSize = metrics.timeSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TimePill(
|
private fun TimePill(
|
||||||
text: String,
|
text: String,
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ import kotlin.math.roundToInt
|
||||||
private const val PlaybackProgressPersistIntervalMs = 60_000L
|
private const val PlaybackProgressPersistIntervalMs = 60_000L
|
||||||
private const val PlayerDoubleTapSeekStepMs = 10_000L
|
private const val PlayerDoubleTapSeekStepMs = 10_000L
|
||||||
private const val PlayerDoubleTapSeekResetDelayMs = 800L
|
private const val PlayerDoubleTapSeekResetDelayMs = 800L
|
||||||
|
private const val PlayerLockedOverlayDurationMs = 2_000L
|
||||||
private const val PlayerLeftGestureBoundary = 0.4f
|
private const val PlayerLeftGestureBoundary = 0.4f
|
||||||
private const val PlayerRightGestureBoundary = 0.6f
|
private const val PlayerRightGestureBoundary = 0.6f
|
||||||
private const val PlayerVerticalGestureSensitivity = 1f
|
private const val PlayerVerticalGestureSensitivity = 1f
|
||||||
|
|
@ -154,6 +155,7 @@ fun PlayerScreen(
|
||||||
val hapticFeedback = LocalHapticFeedback.current
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
val gestureController = rememberPlayerGestureController()
|
val gestureController = rememberPlayerGestureController()
|
||||||
var controlsVisible by rememberSaveable { mutableStateOf(true) }
|
var controlsVisible by rememberSaveable { mutableStateOf(true) }
|
||||||
|
var playerControlsLocked by rememberSaveable { mutableStateOf(false) }
|
||||||
// Active playback state (mutable to support source/episode switching)
|
// Active playback state (mutable to support source/episode switching)
|
||||||
var activeSourceUrl by rememberSaveable { mutableStateOf(sourceUrl) }
|
var activeSourceUrl by rememberSaveable { mutableStateOf(sourceUrl) }
|
||||||
var activeSourceAudioUrl by rememberSaveable { mutableStateOf(sourceAudioUrl) }
|
var activeSourceAudioUrl by rememberSaveable { mutableStateOf(sourceAudioUrl) }
|
||||||
|
|
@ -189,6 +191,7 @@ fun PlayerScreen(
|
||||||
var gestureFeedback by remember { mutableStateOf<GestureFeedbackState?>(null) }
|
var gestureFeedback by remember { mutableStateOf<GestureFeedbackState?>(null) }
|
||||||
var liveGestureFeedback by remember { mutableStateOf<GestureFeedbackState?>(null) }
|
var liveGestureFeedback by remember { mutableStateOf<GestureFeedbackState?>(null) }
|
||||||
var renderedGestureFeedback by remember { mutableStateOf<GestureFeedbackState?>(null) }
|
var renderedGestureFeedback by remember { mutableStateOf<GestureFeedbackState?>(null) }
|
||||||
|
var lockedOverlayVisible by remember { mutableStateOf(false) }
|
||||||
var gestureMessageJob by remember { mutableStateOf<Job?>(null) }
|
var gestureMessageJob by remember { mutableStateOf<Job?>(null) }
|
||||||
var accumulatedSeekResetJob by remember { mutableStateOf<Job?>(null) }
|
var accumulatedSeekResetJob by remember { mutableStateOf<Job?>(null) }
|
||||||
var accumulatedSeekState by remember { mutableStateOf<PlayerAccumulatedSeekState?>(null) }
|
var accumulatedSeekState by remember { mutableStateOf<PlayerAccumulatedSeekState?>(null) }
|
||||||
|
|
@ -497,6 +500,35 @@ fun PlayerScreen(
|
||||||
liveGestureFeedback = null
|
liveGestureFeedback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun revealLockedOverlay() {
|
||||||
|
controlsVisible = false
|
||||||
|
lockedOverlayVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun lockPlayerControls() {
|
||||||
|
playerControlsLocked = true
|
||||||
|
controlsVisible = false
|
||||||
|
lockedOverlayVisible = false
|
||||||
|
pausedOverlayVisible = false
|
||||||
|
scrubbingPositionMs = null
|
||||||
|
gestureMessageJob?.cancel()
|
||||||
|
gestureFeedback = null
|
||||||
|
liveGestureFeedback = null
|
||||||
|
renderedGestureFeedback = null
|
||||||
|
showAudioModal = false
|
||||||
|
showSubtitleModal = false
|
||||||
|
showSourcesPanel = false
|
||||||
|
showEpisodesPanel = false
|
||||||
|
episodeStreamsPanelState = EpisodeStreamsPanelState()
|
||||||
|
PlayerStreamsRepository.clearEpisodeStreams()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unlockPlayerControls() {
|
||||||
|
playerControlsLocked = false
|
||||||
|
lockedOverlayVisible = false
|
||||||
|
controlsVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
fun showSeekFeedback(direction: PlayerSeekDirection, amountMs: Long) {
|
fun showSeekFeedback(direction: PlayerSeekDirection, amountMs: Long) {
|
||||||
val seconds = amountMs / 1000L
|
val seconds = amountMs / 1000L
|
||||||
if (seconds <= 0L) return
|
if (seconds <= 0L) return
|
||||||
|
|
@ -659,6 +691,10 @@ fun PlayerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
val onSurfaceTap = rememberUpdatedState { offset: Offset ->
|
val onSurfaceTap = rememberUpdatedState { offset: Offset ->
|
||||||
|
if (playerControlsLocked) {
|
||||||
|
revealLockedOverlay()
|
||||||
|
return@rememberUpdatedState
|
||||||
|
}
|
||||||
val centerStart = layoutSize.width * PlayerLeftGestureBoundary
|
val centerStart = layoutSize.width * PlayerLeftGestureBoundary
|
||||||
val centerEnd = layoutSize.width * PlayerRightGestureBoundary
|
val centerEnd = layoutSize.width * PlayerRightGestureBoundary
|
||||||
if (controlsVisible && offset.x in centerStart..centerEnd) {
|
if (controlsVisible && offset.x in centerStart..centerEnd) {
|
||||||
|
|
@ -668,6 +704,10 @@ fun PlayerScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val onSurfaceDoubleTap = rememberUpdatedState { offset: Offset ->
|
val onSurfaceDoubleTap = rememberUpdatedState { offset: Offset ->
|
||||||
|
if (playerControlsLocked) {
|
||||||
|
revealLockedOverlay()
|
||||||
|
return@rememberUpdatedState
|
||||||
|
}
|
||||||
when {
|
when {
|
||||||
offset.x < layoutSize.width * PlayerLeftGestureBoundary -> {
|
offset.x < layoutSize.width * PlayerLeftGestureBoundary -> {
|
||||||
handleDoubleTapSeek(PlayerSeekDirection.Backward)
|
handleDoubleTapSeek(PlayerSeekDirection.Backward)
|
||||||
|
|
@ -686,7 +726,9 @@ fun PlayerScreen(
|
||||||
val showBrightnessFeedbackState = rememberUpdatedState(::showBrightnessFeedback)
|
val showBrightnessFeedbackState = rememberUpdatedState(::showBrightnessFeedback)
|
||||||
val showVolumeFeedbackState = rememberUpdatedState(::showVolumeFeedback)
|
val showVolumeFeedbackState = rememberUpdatedState(::showVolumeFeedback)
|
||||||
val clearLiveGestureFeedbackState = rememberUpdatedState(::clearLiveGestureFeedback)
|
val clearLiveGestureFeedbackState = rememberUpdatedState(::clearLiveGestureFeedback)
|
||||||
|
val revealLockedOverlayState = rememberUpdatedState(::revealLockedOverlay)
|
||||||
val isHoldToSpeedGestureActiveState = rememberUpdatedState(isHoldToSpeedGestureActive)
|
val isHoldToSpeedGestureActiveState = rememberUpdatedState(isHoldToSpeedGestureActive)
|
||||||
|
val playerControlsLockedState = rememberUpdatedState(playerControlsLocked)
|
||||||
val currentPositionMsState = rememberUpdatedState(playbackSnapshot.positionMs.coerceAtLeast(0L))
|
val currentPositionMsState = rememberUpdatedState(playbackSnapshot.positionMs.coerceAtLeast(0L))
|
||||||
val currentDurationMsState = rememberUpdatedState(playbackSnapshot.durationMs)
|
val currentDurationMsState = rememberUpdatedState(playbackSnapshot.durationMs)
|
||||||
val commitHorizontalSeekState = rememberUpdatedState { targetPositionMs: Long ->
|
val commitHorizontalSeekState = rememberUpdatedState { targetPositionMs: Long ->
|
||||||
|
|
@ -1002,6 +1044,7 @@ fun PlayerScreen(
|
||||||
scrubbingPositionMs = null
|
scrubbingPositionMs = null
|
||||||
liveGestureFeedback = null
|
liveGestureFeedback = null
|
||||||
renderedGestureFeedback = null
|
renderedGestureFeedback = null
|
||||||
|
lockedOverlayVisible = false
|
||||||
initialLoadCompleted = false
|
initialLoadCompleted = false
|
||||||
lastProgressPersistEpochMs = 0L
|
lastProgressPersistEpochMs = 0L
|
||||||
previousIsPlaying = false
|
previousIsPlaying = false
|
||||||
|
|
@ -1096,6 +1139,14 @@ fun PlayerScreen(
|
||||||
controlsVisible = false
|
controlsVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(playerControlsLocked, lockedOverlayVisible) {
|
||||||
|
if (!playerControlsLocked || !lockedOverlayVisible) {
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
delay(PlayerLockedOverlayDurationMs)
|
||||||
|
lockedOverlayVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(playbackSnapshot.isPlaying, playbackSnapshot.isLoading, playbackSnapshot.durationMs, errorMessage) {
|
LaunchedEffect(playbackSnapshot.isPlaying, playbackSnapshot.isLoading, playbackSnapshot.durationMs, errorMessage) {
|
||||||
pausedOverlayVisible = false
|
pausedOverlayVisible = false
|
||||||
if (playbackSnapshot.isPlaying || playbackSnapshot.isLoading || playbackSnapshot.durationMs <= 0L || errorMessage != null) {
|
if (playbackSnapshot.isPlaying || playbackSnapshot.isLoading || playbackSnapshot.durationMs <= 0L || errorMessage != null) {
|
||||||
|
|
@ -1277,12 +1328,27 @@ fun PlayerScreen(
|
||||||
},
|
},
|
||||||
onTap = { offset -> onSurfaceTap.value(offset) },
|
onTap = { offset -> onSurfaceTap.value(offset) },
|
||||||
onDoubleTap = { offset -> onSurfaceDoubleTap.value(offset) },
|
onDoubleTap = { offset -> onSurfaceDoubleTap.value(offset) },
|
||||||
onLongPress = { activateHoldToSpeedState.value() },
|
onLongPress = {
|
||||||
|
if (playerControlsLockedState.value) {
|
||||||
|
revealLockedOverlayState.value()
|
||||||
|
} else {
|
||||||
|
activateHoldToSpeedState.value()
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.pointerInput(gestureController, layoutSize) {
|
.pointerInput(gestureController, layoutSize) {
|
||||||
awaitEachGesture {
|
awaitEachGesture {
|
||||||
val down = awaitFirstDown()
|
val down = awaitFirstDown()
|
||||||
|
if (playerControlsLockedState.value) {
|
||||||
|
while (true) {
|
||||||
|
val event = awaitPointerEvent()
|
||||||
|
val change = event.changes.firstOrNull { it.id == down.id } ?: break
|
||||||
|
if (!change.pressed) break
|
||||||
|
change.consume()
|
||||||
|
}
|
||||||
|
return@awaitEachGesture
|
||||||
|
}
|
||||||
val controller = gestureController
|
val controller = gestureController
|
||||||
val width = size.width.toFloat().takeIf { it > 0f } ?: return@awaitEachGesture
|
val width = size.width.toFloat().takeIf { it > 0f } ?: return@awaitEachGesture
|
||||||
val height = size.height.toFloat().takeIf { it > 0f } ?: return@awaitEachGesture
|
val height = size.height.toFloat().takeIf { it > 0f } ?: return@awaitEachGesture
|
||||||
|
|
@ -1415,18 +1481,18 @@ fun PlayerScreen(
|
||||||
}
|
}
|
||||||
if (snapshot.isEnded) {
|
if (snapshot.isEnded) {
|
||||||
shouldPlay = false
|
shouldPlay = false
|
||||||
controlsVisible = true
|
controlsVisible = !playerControlsLocked
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError = { message ->
|
onError = { message ->
|
||||||
errorMessage = message
|
errorMessage = message
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
controlsVisible = true
|
controlsVisible = !playerControlsLocked
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if (pausedOverlayVisible && !controlsVisible) {
|
if (pausedOverlayVisible && !controlsVisible && !playerControlsLocked) {
|
||||||
PauseMetadataOverlay(
|
PauseMetadataOverlay(
|
||||||
title = title,
|
title = title,
|
||||||
logo = logo,
|
logo = logo,
|
||||||
|
|
@ -1443,7 +1509,7 @@ fun PlayerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = controlsVisible,
|
visible = controlsVisible && !playerControlsLocked,
|
||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
) {
|
) {
|
||||||
|
|
@ -1458,6 +1524,14 @@ fun PlayerScreen(
|
||||||
displayedPositionMs = displayedPositionMs,
|
displayedPositionMs = displayedPositionMs,
|
||||||
metrics = metrics,
|
metrics = metrics,
|
||||||
resizeMode = resizeMode,
|
resizeMode = resizeMode,
|
||||||
|
isLocked = playerControlsLocked,
|
||||||
|
onLockToggle = {
|
||||||
|
if (playerControlsLocked) {
|
||||||
|
unlockPlayerControls()
|
||||||
|
} else {
|
||||||
|
lockPlayerControls()
|
||||||
|
}
|
||||||
|
},
|
||||||
onBack = onBackWithProgress,
|
onBack = onBackWithProgress,
|
||||||
onTogglePlayback = ::togglePlayback,
|
onTogglePlayback = ::togglePlayback,
|
||||||
onSeekBack = { seekBy(-10_000L) },
|
onSeekBack = { seekBy(-10_000L) },
|
||||||
|
|
@ -1484,6 +1558,21 @@ fun PlayerScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = playerControlsLocked && lockedOverlayVisible,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
) {
|
||||||
|
LockedPlayerOverlay(
|
||||||
|
playbackSnapshot = playbackSnapshot,
|
||||||
|
displayedPositionMs = displayedPositionMs,
|
||||||
|
metrics = metrics,
|
||||||
|
horizontalSafePadding = horizontalSafePadding,
|
||||||
|
onUnlock = ::unlockPlayerControls,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = playerSettingsUiState.showLoadingOverlay && !initialLoadCompleted && errorMessage == null,
|
visible = playerSettingsUiState.showLoadingOverlay && !initialLoadCompleted && errorMessage == null,
|
||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
|
|
@ -1521,23 +1610,25 @@ fun PlayerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip intro/recap/outro button
|
// Skip intro/recap/outro button
|
||||||
SkipIntroButton(
|
if (!playerControlsLocked) {
|
||||||
interval = activeSkipInterval,
|
SkipIntroButton(
|
||||||
dismissed = skipIntervalDismissed,
|
interval = activeSkipInterval,
|
||||||
controlsVisible = controlsVisible,
|
dismissed = skipIntervalDismissed,
|
||||||
onSkip = {
|
controlsVisible = controlsVisible,
|
||||||
val interval = activeSkipInterval ?: return@SkipIntroButton
|
onSkip = {
|
||||||
playerController?.seekTo((interval.endTime * 1000).toLong())
|
val interval = activeSkipInterval ?: return@SkipIntroButton
|
||||||
skipIntervalDismissed = true
|
playerController?.seekTo((interval.endTime * 1000).toLong())
|
||||||
},
|
skipIntervalDismissed = true
|
||||||
onDismiss = { skipIntervalDismissed = true },
|
},
|
||||||
modifier = Modifier
|
onDismiss = { skipIntervalDismissed = true },
|
||||||
.align(Alignment.BottomStart)
|
modifier = Modifier
|
||||||
.padding(start = sliderEdgePadding, bottom = overlayBottomPadding),
|
.align(Alignment.BottomStart)
|
||||||
)
|
.padding(start = sliderEdgePadding, bottom = overlayBottomPadding),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Next episode card
|
// Next episode card
|
||||||
if (isSeries) {
|
if (isSeries && !playerControlsLocked) {
|
||||||
NextEpisodeCard(
|
NextEpisodeCard(
|
||||||
nextEpisode = nextEpisodeInfo,
|
nextEpisode = nextEpisodeInfo,
|
||||||
visible = showNextEpisodeCard,
|
visible = showNextEpisodeCard,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue