feature: add resume/pause functionality to PlayerGesturesLayer and update seek duration

This commit is contained in:
2026-01-26 20:27:58 +01:00
parent 616aa302ad
commit f2158820ac
2 changed files with 57 additions and 117 deletions

View File

@@ -19,13 +19,14 @@ fun PurefinIconButton(
icon: ImageVector, icon: ImageVector,
contentDescription: String, contentDescription: String,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
size: Int = 52
) { ) {
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
Box( Box(
modifier = modifier modifier = modifier
.size(52.dp) .size(size.dp)
.clip(CircleShape) .clip(CircleShape)
.background(scheme.secondary) .background(scheme.secondary)
.clickable { onClick() }, .clickable { onClick() },

View File

@@ -11,24 +11,19 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Cast import androidx.compose.material.icons.outlined.Cast
import androidx.compose.material.icons.outlined.Forward10
import androidx.compose.material.icons.outlined.Forward30 import androidx.compose.material.icons.outlined.Forward30
import androidx.compose.material.icons.outlined.LiveTv
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Pause import androidx.compose.material.icons.outlined.Pause
import androidx.compose.material.icons.outlined.PlayArrow import androidx.compose.material.icons.outlined.PlayArrow
import androidx.compose.material.icons.outlined.PlaylistPlay import androidx.compose.material.icons.outlined.PlaylistPlay
import androidx.compose.material.icons.outlined.Replay10 import androidx.compose.material.icons.outlined.Replay10
import androidx.compose.material.icons.outlined.Replay30
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SkipNext import androidx.compose.material.icons.outlined.SkipNext
import androidx.compose.material.icons.outlined.SkipPrevious import androidx.compose.material.icons.outlined.SkipPrevious
import androidx.compose.material.icons.outlined.Subtitles import androidx.compose.material.icons.outlined.Subtitles
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -38,7 +33,6 @@ 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.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -94,15 +88,6 @@ fun PlayerControlsOverlay(
onOpenQueue = onOpenQueue, onOpenQueue = onOpenQueue,
modifier = Modifier.align(Alignment.TopCenter) modifier = Modifier.align(Alignment.TopCenter)
) )
CenterControls(
isPlaying = uiState.isPlaying,
isLive = uiState.isLive,
onPlayPause = onPlayPause,
onSeekForward = { onSeekRelative(30_000) },
onSeekBackward = { onSeekRelative(-10_000) },
onSeekLiveEdge = onSeekLiveEdge,
modifier = Modifier.align(Alignment.Center)
)
BottomSection( BottomSection(
uiState = uiState, uiState = uiState,
scrubbing = scrubbing, scrubbing = scrubbing,
@@ -111,6 +96,10 @@ fun PlayerControlsOverlay(
onScrubFinished = { scrubbing = false }, onScrubFinished = { scrubbing = false },
onNext = onNext, onNext = onNext,
onPrevious = onPrevious, onPrevious = onPrevious,
onPlayPause = onPlayPause,
onSeekForward = { onSeekRelative(30_000) },
onSeekBackward = { onSeekRelative(-10_000) },
onSeekLiveEdge = onSeekLiveEdge,
onToggleCaptions = onToggleCaptions, onToggleCaptions = onToggleCaptions,
onShowSettings = onShowSettings, onShowSettings = onShowSettings,
onQueueSelected = onQueueSelected, onQueueSelected = onQueueSelected,
@@ -161,80 +150,6 @@ private fun TopBar(
} }
} }
@Composable
private fun CenterControls(
isPlaying: Boolean,
isLive: Boolean,
onPlayPause: () -> Unit,
onSeekForward: () -> Unit,
onSeekBackward: () -> Unit,
onSeekLiveEdge: () -> Unit,
modifier: Modifier = Modifier
) {
val scheme = MaterialTheme.colorScheme
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
OverlayActionButton(
icon = Icons.Outlined.Replay10,
label = "-10",
onClick = onSeekBackward
)
Box(
modifier = Modifier
.clip(CircleShape)
.background(scheme.primary.copy(alpha = 0.9f))
) {
val icon = if (isPlaying) Icons.Outlined.Pause else Icons.Outlined.PlayArrow
GhostIconButton(
icon = icon,
contentDescription = "Play/Pause",
onClick = onPlayPause,
modifier = Modifier.padding(6.dp)
)
}
OverlayActionButton(
icon = Icons.Outlined.Forward30,
label = "+30",
onClick = onSeekForward
)
}
Spacer(modifier = Modifier.height(8.dp))
if (isLive) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(Icons.Outlined.LiveTv, contentDescription = null, tint = scheme.primary)
Text(
text = "Live",
color = scheme.primary,
fontWeight = FontWeight.Bold,
modifier = Modifier
.clip(CircleShape)
.background(scheme.primary.copy(alpha = 0.15f))
.padding(horizontal = 10.dp, vertical = 6.dp)
)
Text(
text = "Catch up",
color = scheme.onSurface,
modifier = Modifier
.clip(CircleShape)
.background(scheme.surfaceVariant.copy(alpha = 0.7f))
.clickable { onSeekLiveEdge() }
.padding(horizontal = 12.dp, vertical = 6.dp)
)
}
}
}
}
@Composable @Composable
private fun BottomSection( private fun BottomSection(
uiState: PlayerUiState, uiState: PlayerUiState,
@@ -244,6 +159,10 @@ private fun BottomSection(
onScrubFinished: () -> Unit, onScrubFinished: () -> Unit,
onNext: () -> Unit, onNext: () -> Unit,
onPrevious: () -> Unit, onPrevious: () -> Unit,
onPlayPause: () -> Unit,
onSeekForward: () -> Unit,
onSeekBackward: () -> Unit,
onSeekLiveEdge: () -> Unit,
onToggleCaptions: () -> Unit, onToggleCaptions: () -> Unit,
onShowSettings: () -> Unit, onShowSettings: () -> Unit,
onQueueSelected: (String) -> Unit, onQueueSelected: (String) -> Unit,
@@ -255,7 +174,8 @@ private fun BottomSection(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = formatTime(uiState.positionMs), text = formatTime(uiState.positionMs),
@@ -263,7 +183,17 @@ private fun BottomSection(
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
if (uiState.isLive) { if (uiState.isLive) {
Text(text = "LIVE", color = scheme.primary, fontWeight = FontWeight.Bold) 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 { } else {
Text( Text(
text = formatTime(uiState.durationMs), text = formatTime(uiState.durationMs),
@@ -283,26 +213,52 @@ private fun BottomSection(
onScrubFinished = onScrubFinished onScrubFinished = onScrubFinished
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Row( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 4.dp), .padding(horizontal = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) { ) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { Row(
modifier = Modifier.align(Alignment.Center),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
PurefinIconButton( PurefinIconButton(
icon = Icons.Outlined.SkipPrevious, icon = Icons.Outlined.SkipPrevious,
contentDescription = "Previous", contentDescription = "Previous",
onClick = onPrevious onClick = onPrevious,
size = 64
)
PurefinIconButton(
icon = Icons.Outlined.Replay10,
contentDescription = "Seek backward",
onClick = onSeekBackward,
size = 64
)
PurefinIconButton(
icon = if (uiState.isPlaying) Icons.Outlined.Pause else Icons.Outlined.PlayArrow,
contentDescription = "Play/Pause",
onClick = onPlayPause,
size = 64
)
PurefinIconButton(
icon = Icons.Outlined.Forward30,
contentDescription = "Seek forward",
onClick = onSeekForward,
size = 64
) )
PurefinIconButton( PurefinIconButton(
icon = Icons.Outlined.SkipNext, icon = Icons.Outlined.SkipNext,
contentDescription = "Next", contentDescription = "Next",
onClick = onNext onClick = onNext,
size = 64
) )
} }
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { Row(
modifier = Modifier.align(Alignment.CenterEnd),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
PurefinIconButton( PurefinIconButton(
icon = Icons.Outlined.Subtitles, icon = Icons.Outlined.Subtitles,
contentDescription = "Captions", contentDescription = "Captions",
@@ -319,23 +275,6 @@ private fun BottomSection(
} }
} }
@Composable
private fun OverlayActionButton(
icon: androidx.compose.ui.graphics.vector.ImageVector,
label: String,
onClick: () -> Unit
) {
val scheme = MaterialTheme.colorScheme
Column(horizontalAlignment = Alignment.CenterHorizontally) {
GhostIconButton(
icon = icon,
contentDescription = label,
onClick = onClick
)
Text(text = label, color = scheme.onSurface, style = MaterialTheme.typography.labelSmall)
}
}
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()