mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
feat: enhance player UI with hidden seek timeline and time display improvements
This commit is contained in:
@@ -12,11 +12,15 @@ import androidx.compose.animation.slideOutHorizontally
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
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.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Pause
|
import androidx.compose.material.icons.outlined.Pause
|
||||||
import androidx.compose.material.icons.outlined.SkipNext
|
import androidx.compose.material.icons.outlined.SkipNext
|
||||||
@@ -53,9 +57,12 @@ import androidx.media3.common.util.UnstableApi
|
|||||||
import androidx.media3.ui.AspectRatioFrameLayout
|
import androidx.media3.ui.AspectRatioFrameLayout
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import androidx.media3.ui.SubtitleView
|
import androidx.media3.ui.SubtitleView
|
||||||
|
import hu.bbara.purefin.core.player.model.TimedMarker
|
||||||
import hu.bbara.purefin.core.player.viewmodel.ControlsAutoHideBlocker
|
import hu.bbara.purefin.core.player.viewmodel.ControlsAutoHideBlocker
|
||||||
import hu.bbara.purefin.core.player.viewmodel.PlayerViewModel
|
import hu.bbara.purefin.core.player.viewmodel.PlayerViewModel
|
||||||
|
import hu.bbara.purefin.ui.screen.player.components.PlayerSeekBarTrack
|
||||||
import hu.bbara.purefin.ui.screen.player.components.TvIconButton
|
import hu.bbara.purefin.ui.screen.player.components.TvIconButton
|
||||||
|
import hu.bbara.purefin.ui.screen.player.components.TvPlayerTimeRow
|
||||||
import hu.bbara.purefin.ui.screen.player.components.TvPlayerControlsOverlay
|
import hu.bbara.purefin.ui.screen.player.components.TvPlayerControlsOverlay
|
||||||
import hu.bbara.purefin.ui.screen.player.components.TvPlayerLoadingErrorEndCard
|
import hu.bbara.purefin.ui.screen.player.components.TvPlayerLoadingErrorEndCard
|
||||||
import hu.bbara.purefin.ui.screen.player.components.TvTrackPanelType
|
import hu.bbara.purefin.ui.screen.player.components.TvTrackPanelType
|
||||||
@@ -85,6 +92,8 @@ fun TvPlayerScreen(
|
|||||||
var pendingTrackButtonFocus by remember { mutableStateOf<TvTrackPanelType?>(null) }
|
var pendingTrackButtonFocus by remember { mutableStateOf<TvTrackPanelType?>(null) }
|
||||||
var stopFeedbackVisible by remember { mutableStateOf(false) }
|
var stopFeedbackVisible by remember { mutableStateOf(false) }
|
||||||
var stopFeedbackRequestId by remember { mutableStateOf(0) }
|
var stopFeedbackRequestId by remember { mutableStateOf(0) }
|
||||||
|
var hiddenSeekPreviewPositionMs by remember { mutableStateOf<Long?>(null) }
|
||||||
|
var hiddenSeekRequestId by remember { mutableStateOf(0) }
|
||||||
val controlsAutoHideBlocked = isPlaylistExpanded || trackPanelType != null
|
val controlsAutoHideBlocked = isPlaylistExpanded || trackPanelType != null
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -162,6 +171,9 @@ fun TvPlayerScreen(
|
|||||||
showTvControls()
|
showTvControls()
|
||||||
}
|
}
|
||||||
val seekByWithoutShowingControls: (Long) -> Unit = { deltaMs ->
|
val seekByWithoutShowingControls: (Long) -> Unit = { deltaMs ->
|
||||||
|
val basePositionMs = hiddenSeekPreviewPositionMs ?: uiState.positionMs
|
||||||
|
hiddenSeekPreviewPositionMs = (basePositionMs + deltaMs).coerceSeekPosition(uiState.durationMs)
|
||||||
|
hiddenSeekRequestId += 1
|
||||||
viewModel.seekBy(deltaMs)
|
viewModel.seekBy(deltaMs)
|
||||||
stopFeedbackVisible = false
|
stopFeedbackVisible = false
|
||||||
}
|
}
|
||||||
@@ -221,9 +233,16 @@ fun TvPlayerScreen(
|
|||||||
stopFeedbackVisible = false
|
stopFeedbackVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(hiddenSeekRequestId) {
|
||||||
|
if (hiddenSeekRequestId == 0) return@LaunchedEffect
|
||||||
|
delay(TV_HIDDEN_STOP_FEEDBACK_MS)
|
||||||
|
hiddenSeekPreviewPositionMs = null
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(controlsVisible, isPlaylistExpanded, trackPanelType, uiState.isEnded, uiState.error) {
|
LaunchedEffect(controlsVisible, isPlaylistExpanded, trackPanelType, uiState.isEnded, uiState.error) {
|
||||||
if (controlsVisible || isPlaylistExpanded || trackPanelType != null || uiState.isEnded || uiState.error != null) {
|
if (controlsVisible || isPlaylistExpanded || trackPanelType != null || uiState.isEnded || uiState.error != null) {
|
||||||
stopFeedbackVisible = false
|
stopFeedbackVisible = false
|
||||||
|
hiddenSeekPreviewPositionMs = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +349,10 @@ fun TvPlayerScreen(
|
|||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(end = 24.dp, bottom = 24.dp)
|
.padding(
|
||||||
|
end = 24.dp,
|
||||||
|
bottom = if (hiddenSeekPreviewPositionMs != null) 104.dp else 24.dp
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
TvIconButton(
|
TvIconButton(
|
||||||
icon = Icons.Outlined.SkipNext,
|
icon = Icons.Outlined.SkipNext,
|
||||||
@@ -342,6 +364,23 @@ fun TvPlayerScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = !playerControlsVisible && hiddenSeekPreviewPositionMs != null,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(horizontal = 32.dp, vertical = 28.dp)
|
||||||
|
) {
|
||||||
|
HiddenTvSeekTimeline(
|
||||||
|
positionMs = hiddenSeekPreviewPositionMs ?: uiState.positionMs,
|
||||||
|
durationMs = uiState.durationMs,
|
||||||
|
bufferedMs = uiState.bufferedMs,
|
||||||
|
chapterMarkers = uiState.chapters,
|
||||||
|
adMarkers = uiState.ads
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
TvPlayerLoadingErrorEndCard(
|
TvPlayerLoadingErrorEndCard(
|
||||||
modifier = Modifier.align(Alignment.Center),
|
modifier = Modifier.align(Alignment.Center),
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
@@ -384,6 +423,53 @@ fun TvPlayerScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HiddenTvSeekTimeline(
|
||||||
|
positionMs: Long,
|
||||||
|
durationMs: Long,
|
||||||
|
bufferedMs: Long,
|
||||||
|
chapterMarkers: List<TimedMarker>,
|
||||||
|
adMarkers: List<TimedMarker>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val safeDuration = durationMs.takeIf { it > 0 } ?: 1L
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(Color.Transparent)
|
||||||
|
.padding(horizontal = 20.dp, vertical = 12.dp)
|
||||||
|
) {
|
||||||
|
TvPlayerTimeRow(
|
||||||
|
positionMs = positionMs,
|
||||||
|
durationMs = durationMs,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
PlayerSeekBarTrack(
|
||||||
|
positionMs = positionMs,
|
||||||
|
durationMs = safeDuration,
|
||||||
|
bufferedMs = bufferedMs,
|
||||||
|
chapterMarkers = chapterMarkers,
|
||||||
|
adMarkers = adMarkers,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(32.dp),
|
||||||
|
isFocused = false,
|
||||||
|
thumbRadius = 7.dp,
|
||||||
|
focusedThumbRadius = 9.dp,
|
||||||
|
focusedThumbHaloRadiusDelta = 0.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Long.coerceSeekPosition(durationMs: Long): Long =
|
||||||
|
if (durationMs > 0L) {
|
||||||
|
coerceIn(0L, durationMs)
|
||||||
|
} else {
|
||||||
|
coerceAtLeast(0L)
|
||||||
|
}
|
||||||
|
|
||||||
internal fun handleTvPlayerRootKeyEvent(
|
internal fun handleTvPlayerRootKeyEvent(
|
||||||
event: androidx.compose.ui.input.key.KeyEvent,
|
event: androidx.compose.ui.input.key.KeyEvent,
|
||||||
isPlaying: Boolean,
|
isPlaying: Boolean,
|
||||||
|
|||||||
@@ -187,7 +187,6 @@ private fun TvPlayerBottomSection(
|
|||||||
subtitlesButtonEnabled: Boolean,
|
subtitlesButtonEnabled: Boolean,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
val playlistFocusRequester = remember { FocusRequester() }
|
val playlistFocusRequester = remember { FocusRequester() }
|
||||||
var wasPlaylistExpanded by remember { mutableStateOf(isPlaylistExpanded) }
|
var wasPlaylistExpanded by remember { mutableStateOf(isPlaylistExpanded) }
|
||||||
|
|
||||||
@@ -210,24 +209,11 @@ private fun TvPlayerBottomSection(
|
|||||||
.testTag(TvPlayerPlaylistStateTag)
|
.testTag(TvPlayerPlaylistStateTag)
|
||||||
.semantics { stateDescription = playlistExpandState }
|
.semantics { stateDescription = playlistExpandState }
|
||||||
) {
|
) {
|
||||||
Row(
|
TvPlayerTimeRow(
|
||||||
modifier = Modifier
|
positionMs = uiState.positionMs,
|
||||||
.fillMaxWidth()
|
durationMs = uiState.durationMs,
|
||||||
.padding(horizontal = 4.dp),
|
modifier = Modifier.padding(horizontal = 4.dp)
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
)
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = formatTime(uiState.positionMs),
|
|
||||||
color = scheme.onSurface,
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = formatTime(uiState.durationMs),
|
|
||||||
color = scheme.onSurface,
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
TvPlayerSeekBar(
|
TvPlayerSeekBar(
|
||||||
positionMs = uiState.positionMs,
|
positionMs = uiState.positionMs,
|
||||||
durationMs = uiState.durationMs,
|
durationMs = uiState.durationMs,
|
||||||
@@ -355,6 +341,32 @@ private fun TvPlayerBottomSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun TvPlayerTimeRow(
|
||||||
|
positionMs: Long,
|
||||||
|
durationMs: Long,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = formatTime(positionMs),
|
||||||
|
color = scheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = formatTime(durationMs),
|
||||||
|
color = scheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleExpandPlaylistKey(
|
private fun handleExpandPlaylistKey(
|
||||||
event: androidx.compose.ui.input.key.KeyEvent,
|
event: androidx.compose.ui.input.key.KeyEvent,
|
||||||
onExpand: () -> Unit
|
onExpand: () -> Unit
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ import androidx.compose.animation.slideInHorizontally
|
|||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
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.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -40,6 +43,7 @@ import androidx.media3.common.util.UnstableApi
|
|||||||
import androidx.media3.ui.AspectRatioFrameLayout
|
import androidx.media3.ui.AspectRatioFrameLayout
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import androidx.media3.ui.SubtitleView
|
import androidx.media3.ui.SubtitleView
|
||||||
|
import hu.bbara.purefin.core.player.model.TimedMarker
|
||||||
import hu.bbara.purefin.core.player.viewmodel.PlayerViewModel
|
import hu.bbara.purefin.core.player.viewmodel.PlayerViewModel
|
||||||
import hu.bbara.purefin.ui.common.visual.EmptyValueTimedVisibility
|
import hu.bbara.purefin.ui.common.visual.EmptyValueTimedVisibility
|
||||||
import hu.bbara.purefin.ui.common.visual.ValueChangeTimedVisibility
|
import hu.bbara.purefin.ui.common.visual.ValueChangeTimedVisibility
|
||||||
@@ -49,6 +53,8 @@ import hu.bbara.purefin.ui.screen.player.components.PlayerControlsOverlay
|
|||||||
import hu.bbara.purefin.ui.screen.player.components.PlayerGesturesLayer
|
import hu.bbara.purefin.ui.screen.player.components.PlayerGesturesLayer
|
||||||
import hu.bbara.purefin.ui.screen.player.components.PlayerLoadingErrorEndCard
|
import hu.bbara.purefin.ui.screen.player.components.PlayerLoadingErrorEndCard
|
||||||
import hu.bbara.purefin.ui.screen.player.components.PlayerQueuePanel
|
import hu.bbara.purefin.ui.screen.player.components.PlayerQueuePanel
|
||||||
|
import hu.bbara.purefin.ui.screen.player.components.PlayerSeekBarTrack
|
||||||
|
import hu.bbara.purefin.ui.screen.player.components.PlayerTimeRow
|
||||||
import hu.bbara.purefin.ui.screen.player.components.SkipSegmentButton
|
import hu.bbara.purefin.ui.screen.player.components.SkipSegmentButton
|
||||||
import hu.bbara.purefin.ui.screen.player.components.rememberPersistentOverlayController
|
import hu.bbara.purefin.ui.screen.player.components.rememberPersistentOverlayController
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
@@ -73,6 +79,7 @@ fun PlayerScreen(
|
|||||||
var brightness by remember { mutableStateOf(readCurrentBrightness(activity)) }
|
var brightness by remember { mutableStateOf(readCurrentBrightness(activity)) }
|
||||||
var showQueuePanel by remember { mutableStateOf(false) }
|
var showQueuePanel by remember { mutableStateOf(false) }
|
||||||
var horizontalSeekFeedback by remember { mutableStateOf<Long?>(null) }
|
var horizontalSeekFeedback by remember { mutableStateOf<Long?>(null) }
|
||||||
|
var horizontalSeekPreviewPositionMs by remember { mutableStateOf<Long?>(null) }
|
||||||
val overlayController = rememberPersistentOverlayController()
|
val overlayController = rememberPersistentOverlayController()
|
||||||
|
|
||||||
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
|
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
|
||||||
@@ -136,8 +143,15 @@ fun PlayerScreen(
|
|||||||
0
|
0
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onHorizontalDragPreview = {
|
onHorizontalDragPreview = { deltaMs, previewPositionMs ->
|
||||||
horizontalSeekFeedback = it
|
horizontalSeekFeedback = deltaMs
|
||||||
|
horizontalSeekPreviewPositionMs = previewPositionMs?.let {
|
||||||
|
if (uiState.durationMs > 0L) {
|
||||||
|
it.coerceIn(0L, uiState.durationMs)
|
||||||
|
} else {
|
||||||
|
it.coerceAtLeast(0L)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onHorizontalDragSeekTo = {
|
onHorizontalDragSeekTo = {
|
||||||
viewModel.seekTo(it)
|
viewModel.seekTo(it)
|
||||||
@@ -156,6 +170,25 @@ fun PlayerScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EmptyValueTimedVisibility(
|
||||||
|
value = horizontalSeekPreviewPositionMs,
|
||||||
|
hideAfterMillis = 1_000
|
||||||
|
) { previewPositionMs ->
|
||||||
|
if (!playerControlsVisible) {
|
||||||
|
HiddenSeekTimeline(
|
||||||
|
positionMs = previewPositionMs,
|
||||||
|
durationMs = uiState.durationMs,
|
||||||
|
bufferedMs = uiState.bufferedMs,
|
||||||
|
chapterMarkers = uiState.chapters,
|
||||||
|
adMarkers = uiState.ads,
|
||||||
|
isLive = uiState.isLive,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(horizontal = 24.dp, vertical = 28.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ValueChangeTimedVisibility(
|
ValueChangeTimedVisibility(
|
||||||
value = brightness,
|
value = brightness,
|
||||||
hideAfterMillis = 800,
|
hideAfterMillis = 800,
|
||||||
@@ -218,7 +251,10 @@ fun PlayerScreen(
|
|||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(end = 24.dp, bottom = 24.dp)
|
.padding(
|
||||||
|
end = 24.dp,
|
||||||
|
bottom = if (horizontalSeekPreviewPositionMs != null) 96.dp else 24.dp
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
SkipSegmentButton(onClick = { viewModel.skipActiveSegment() })
|
SkipSegmentButton(onClick = { viewModel.skipActiveSegment() })
|
||||||
}
|
}
|
||||||
@@ -258,6 +294,48 @@ fun PlayerScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HiddenSeekTimeline(
|
||||||
|
positionMs: Long,
|
||||||
|
durationMs: Long,
|
||||||
|
bufferedMs: Long,
|
||||||
|
chapterMarkers: List<TimedMarker>,
|
||||||
|
adMarkers: List<TimedMarker>,
|
||||||
|
isLive: Boolean,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val safeDuration = durationMs.takeIf { it > 0 } ?: 1L
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(Color.Transparent)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 10.dp)
|
||||||
|
) {
|
||||||
|
PlayerTimeRow(
|
||||||
|
positionMs = positionMs,
|
||||||
|
durationMs = durationMs,
|
||||||
|
isLive = isLive,
|
||||||
|
onSeekLiveEdge = null
|
||||||
|
)
|
||||||
|
PlayerSeekBarTrack(
|
||||||
|
positionMs = positionMs,
|
||||||
|
durationMs = safeDuration,
|
||||||
|
bufferedMs = bufferedMs,
|
||||||
|
chapterMarkers = chapterMarkers,
|
||||||
|
adMarkers = adMarkers,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(28.dp),
|
||||||
|
isFocused = false,
|
||||||
|
focusedTrackHeight = 4.dp,
|
||||||
|
focusedThumbRadius = 6.dp,
|
||||||
|
focusedThumbHaloRadiusDelta = 0.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SeekAmountIndicator(deltaMs: Long, modifier: Modifier = Modifier) {
|
private fun SeekAmountIndicator(deltaMs: Long, modifier: Modifier = Modifier) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ fun PlayerControlsOverlay(
|
|||||||
onOpenQueue: () -> Unit,
|
onOpenQueue: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
var scrubbing by remember { mutableStateOf(false) }
|
var scrubbing by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
@@ -177,40 +176,14 @@ private fun BottomSection(
|
|||||||
onQueueSelected: (String) -> Unit,
|
onQueueSelected: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
Row(
|
PlayerTimeRow(
|
||||||
modifier = Modifier
|
positionMs = uiState.positionMs,
|
||||||
.fillMaxWidth()
|
durationMs = uiState.durationMs,
|
||||||
.padding(horizontal = 8.dp),
|
isLive = uiState.isLive,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
onSeekLiveEdge = onSeekLiveEdge,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
modifier = Modifier.padding(horizontal = 8.dp)
|
||||||
) {
|
)
|
||||||
Text(
|
|
||||||
text = formatTime(uiState.positionMs),
|
|
||||||
color = scheme.onSurface,
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
if (uiState.isLive) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(text = "LIVE", color = scheme.primary, fontWeight = FontWeight.Bold)
|
|
||||||
Text(
|
|
||||||
text = "Catch up",
|
|
||||||
color = scheme.onSurface,
|
|
||||||
modifier = Modifier.clickable { onSeekLiveEdge() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text(
|
|
||||||
text = formatTime(uiState.durationMs),
|
|
||||||
color = scheme.onSurface,
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PlayerSeekBar(
|
PlayerSeekBar(
|
||||||
positionMs = uiState.positionMs,
|
positionMs = uiState.positionMs,
|
||||||
durationMs = uiState.durationMs,
|
durationMs = uiState.durationMs,
|
||||||
@@ -329,6 +302,50 @@ fun SkipSegmentButton(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun PlayerTimeRow(
|
||||||
|
positionMs: Long,
|
||||||
|
durationMs: Long,
|
||||||
|
isLive: Boolean,
|
||||||
|
onSeekLiveEdge: (() -> Unit)?,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = formatTime(positionMs),
|
||||||
|
color = scheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
if (isLive) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(text = "LIVE", color = scheme.primary, fontWeight = FontWeight.Bold)
|
||||||
|
if (onSeekLiveEdge != null) {
|
||||||
|
Text(
|
||||||
|
text = "Catch up",
|
||||||
|
color = scheme.onSurface,
|
||||||
|
modifier = Modifier.clickable { onSeekLiveEdge() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = formatTime(durationMs),
|
||||||
|
color = scheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun formatTime(positionMs: Long): String {
|
private fun formatTime(positionMs: Long): String {
|
||||||
val totalSeconds = positionMs / 1000
|
val totalSeconds = positionMs / 1000
|
||||||
val seconds = (totalSeconds % 60).toInt()
|
val seconds = (totalSeconds % 60).toInt()
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ fun PlayerGesturesLayer(
|
|||||||
onDoubleTapLeft: () -> Unit,
|
onDoubleTapLeft: () -> Unit,
|
||||||
onVerticalDragLeft: (delta: Float) -> Unit,
|
onVerticalDragLeft: (delta: Float) -> Unit,
|
||||||
onVerticalDragRight: (delta: Float) -> Unit,
|
onVerticalDragRight: (delta: Float) -> Unit,
|
||||||
onHorizontalDragPreview: (deltaMs: Long?) -> Unit = {},
|
onHorizontalDragPreview: (deltaMs: Long?, previewPositionMs: Long?) -> Unit = { _, _ -> },
|
||||||
onHorizontalDragSeekTo: (positionMs: Long) -> Unit,
|
onHorizontalDragSeekTo: (positionMs: Long) -> Unit,
|
||||||
currentPositionProvider: () -> Long,
|
currentPositionProvider: () -> Long,
|
||||||
) {
|
) {
|
||||||
@@ -95,7 +95,10 @@ fun PlayerGesturesLayer(
|
|||||||
val deltaMs = HorizontalSeekGestureHelper.deltaMs(accumulatedHorizontalDrag)
|
val deltaMs = HorizontalSeekGestureHelper.deltaMs(accumulatedHorizontalDrag)
|
||||||
if (deltaMs != 0L && deltaMs != lastPreviewDelta) {
|
if (deltaMs != 0L && deltaMs != lastPreviewDelta) {
|
||||||
lastPreviewDelta = deltaMs
|
lastPreviewDelta = deltaMs
|
||||||
onHorizontalDragPreview(deltaMs)
|
onHorizontalDragPreview(
|
||||||
|
deltaMs,
|
||||||
|
(startPositionMs + deltaMs).coerceAtLeast(0L)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,10 +120,10 @@ fun PlayerGesturesLayer(
|
|||||||
if (deltaMs != 0L) {
|
if (deltaMs != 0L) {
|
||||||
val targetMs = (startPositionMs + deltaMs).coerceAtLeast(0L)
|
val targetMs = (startPositionMs + deltaMs).coerceAtLeast(0L)
|
||||||
onHorizontalDragSeekTo(targetMs)
|
onHorizontalDragSeekTo(targetMs)
|
||||||
onHorizontalDragPreview(deltaMs)
|
onHorizontalDragPreview(deltaMs, targetMs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onHorizontalDragPreview(null)
|
onHorizontalDragPreview(null, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user