mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
feat: Add D-pad seeking and auto-focus seek bar in TV player
D-pad left/right now seeks -10s/+10s both when controls are hidden and when the seek bar is focused. Controls auto-focus the seek bar when shown, removing the need to press OK first to interact.
This commit is contained in:
@@ -52,8 +52,6 @@ import androidx.compose.material3.ButtonDefaults
|
|||||||
import androidx.compose.material3.CircularProgressIndicator
|
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.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
|
||||||
@@ -137,9 +135,17 @@ fun TvPlayerScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val seekBarFocusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
|
LaunchedEffect(controlsVisible) {
|
||||||
|
if (controlsVisible) {
|
||||||
|
seekBarFocusRequester.requestFocus()
|
||||||
|
} else {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -149,8 +155,17 @@ fun TvPlayerScreen(
|
|||||||
.onPreviewKeyEvent { event ->
|
.onPreviewKeyEvent { event ->
|
||||||
if (!controlsVisible && event.type == KeyEventType.KeyDown) {
|
if (!controlsVisible && event.type == KeyEventType.KeyDown) {
|
||||||
when (event.key) {
|
when (event.key) {
|
||||||
|
Key.DirectionLeft -> {
|
||||||
|
viewModel.seekBy(-10_000)
|
||||||
|
viewModel.showControls()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Key.DirectionRight -> {
|
||||||
|
viewModel.seekBy(10_000)
|
||||||
|
viewModel.showControls()
|
||||||
|
true
|
||||||
|
}
|
||||||
Key.DirectionUp, Key.DirectionDown,
|
Key.DirectionUp, Key.DirectionDown,
|
||||||
Key.DirectionLeft, Key.DirectionRight,
|
|
||||||
Key.DirectionCenter, Key.Enter -> {
|
Key.DirectionCenter, Key.Enter -> {
|
||||||
viewModel.showControls()
|
viewModel.showControls()
|
||||||
true
|
true
|
||||||
@@ -185,6 +200,7 @@ fun TvPlayerScreen(
|
|||||||
TvPlayerControlsOverlay(
|
TvPlayerControlsOverlay(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
|
seekBarFocusRequester = seekBarFocusRequester,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onPlayPause = { viewModel.togglePlayPause() },
|
onPlayPause = { viewModel.togglePlayPause() },
|
||||||
onSeek = { viewModel.seekTo(it) },
|
onSeek = { viewModel.seekTo(it) },
|
||||||
@@ -250,6 +266,7 @@ private enum class TrackPanelType { AUDIO, SUBTITLES, QUALITY }
|
|||||||
@Composable
|
@Composable
|
||||||
private fun TvPlayerControlsOverlay(
|
private fun TvPlayerControlsOverlay(
|
||||||
uiState: PlayerUiState,
|
uiState: PlayerUiState,
|
||||||
|
seekBarFocusRequester: FocusRequester,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onPlayPause: () -> Unit,
|
onPlayPause: () -> Unit,
|
||||||
onSeek: (Long) -> Unit,
|
onSeek: (Long) -> Unit,
|
||||||
@@ -288,6 +305,7 @@ private fun TvPlayerControlsOverlay(
|
|||||||
)
|
)
|
||||||
TvPlayerBottomSection(
|
TvPlayerBottomSection(
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
|
seekBarFocusRequester = seekBarFocusRequester,
|
||||||
onPlayPause = onPlayPause,
|
onPlayPause = onPlayPause,
|
||||||
onSeek = onSeek,
|
onSeek = onSeek,
|
||||||
onSeekRelative = onSeekRelative,
|
onSeekRelative = onSeekRelative,
|
||||||
@@ -355,6 +373,7 @@ private fun TvPlayerTopBar(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun TvPlayerBottomSection(
|
private fun TvPlayerBottomSection(
|
||||||
uiState: PlayerUiState,
|
uiState: PlayerUiState,
|
||||||
|
seekBarFocusRequester: FocusRequester,
|
||||||
onPlayPause: () -> Unit,
|
onPlayPause: () -> Unit,
|
||||||
onSeek: (Long) -> Unit,
|
onSeek: (Long) -> Unit,
|
||||||
onSeekRelative: (Long) -> Unit,
|
onSeekRelative: (Long) -> Unit,
|
||||||
@@ -406,7 +425,9 @@ private fun TvPlayerBottomSection(
|
|||||||
bufferedMs = uiState.bufferedMs,
|
bufferedMs = uiState.bufferedMs,
|
||||||
chapterMarkers = uiState.chapters,
|
chapterMarkers = uiState.chapters,
|
||||||
adMarkers = uiState.ads,
|
adMarkers = uiState.ads,
|
||||||
onSeek = onSeek
|
onSeek = onSeek,
|
||||||
|
onSeekRelative = onSeekRelative,
|
||||||
|
focusRequester = seekBarFocusRequester
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Box(
|
Box(
|
||||||
@@ -484,6 +505,8 @@ private fun TvPlayerSeekBar(
|
|||||||
chapterMarkers: List<TimedMarker>,
|
chapterMarkers: List<TimedMarker>,
|
||||||
adMarkers: List<TimedMarker>,
|
adMarkers: List<TimedMarker>,
|
||||||
onSeek: (Long) -> Unit,
|
onSeek: (Long) -> Unit,
|
||||||
|
onSeekRelative: (Long) -> Unit,
|
||||||
|
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
@@ -493,12 +516,33 @@ private fun TvPlayerSeekBar(
|
|||||||
val progressRatio = (position.toFloat() / safeDuration).coerceIn(0f, 1f)
|
val progressRatio = (position.toFloat() / safeDuration).coerceIn(0f, 1f)
|
||||||
val combinedMarkers = chapterMarkers.map { it.copy(type = MarkerType.CHAPTER) } +
|
val combinedMarkers = chapterMarkers.map { it.copy(type = MarkerType.CHAPTER) } +
|
||||||
adMarkers.map { it.copy(type = MarkerType.AD) }
|
adMarkers.map { it.copy(type = MarkerType.AD) }
|
||||||
|
var isFocused by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 4.dp)
|
.padding(horizontal = 4.dp)
|
||||||
.height(32.dp),
|
.height(32.dp)
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.onFocusChanged { isFocused = it.isFocused }
|
||||||
|
.onPreviewKeyEvent { event ->
|
||||||
|
if (event.type == KeyEventType.KeyDown) {
|
||||||
|
when (event.key) {
|
||||||
|
Key.DirectionLeft -> {
|
||||||
|
onSeekRelative(-10_000)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Key.DirectionRight -> {
|
||||||
|
onSeekRelative(10_000)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focusable(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Canvas(
|
Canvas(
|
||||||
@@ -506,7 +550,7 @@ private fun TvPlayerSeekBar(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 2.dp, vertical = 10.dp)
|
.padding(horizontal = 2.dp, vertical = 10.dp)
|
||||||
) {
|
) {
|
||||||
val trackHeight = 4f
|
val trackHeight = if (isFocused) 6f else 4f
|
||||||
val trackTop = size.height / 2 - trackHeight / 2
|
val trackTop = size.height / 2 - trackHeight / 2
|
||||||
drawRect(
|
drawRect(
|
||||||
color = scheme.onSurface.copy(alpha = 0.2f),
|
color = scheme.onSurface.copy(alpha = 0.2f),
|
||||||
@@ -524,12 +568,19 @@ private fun TvPlayerSeekBar(
|
|||||||
size = Size(width = progressWidth, height = trackHeight),
|
size = Size(width = progressWidth, height = trackHeight),
|
||||||
topLeft = Offset(0f, trackTop)
|
topLeft = Offset(0f, trackTop)
|
||||||
)
|
)
|
||||||
val thumbRadius = 7.dp.toPx()
|
val thumbRadius = if (isFocused) 9.dp.toPx() else 7.dp.toPx()
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = scheme.primary,
|
color = if (isFocused) scheme.primary else scheme.primary,
|
||||||
radius = thumbRadius,
|
radius = thumbRadius,
|
||||||
center = Offset(progressWidth.coerceIn(0f, size.width), size.height / 2)
|
center = Offset(progressWidth.coerceIn(0f, size.width), size.height / 2)
|
||||||
)
|
)
|
||||||
|
if (isFocused) {
|
||||||
|
drawCircle(
|
||||||
|
color = scheme.primary.copy(alpha = 0.3f),
|
||||||
|
radius = thumbRadius + 4.dp.toPx(),
|
||||||
|
center = Offset(progressWidth.coerceIn(0f, size.width), size.height / 2)
|
||||||
|
)
|
||||||
|
}
|
||||||
combinedMarkers.forEach { marker ->
|
combinedMarkers.forEach { marker ->
|
||||||
val x = (marker.positionMs.toFloat() / safeDuration) * size.width
|
val x = (marker.positionMs.toFloat() / safeDuration) * size.width
|
||||||
val color = if (marker.type == MarkerType.AD) scheme.secondary else scheme.primary
|
val color = if (marker.type == MarkerType.AD) scheme.secondary else scheme.primary
|
||||||
@@ -540,17 +591,6 @@ private fun TvPlayerSeekBar(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Slider(
|
|
||||||
value = position.toFloat(),
|
|
||||||
onValueChange = { onSeek(it.toLong()) },
|
|
||||||
valueRange = 0f..safeDuration.toFloat(),
|
|
||||||
colors = SliderDefaults.colors(
|
|
||||||
thumbColor = Color.Transparent,
|
|
||||||
activeTrackColor = Color.Transparent,
|
|
||||||
inactiveTrackColor = Color.Transparent
|
|
||||||
),
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user