Skip to content

Commit 01aed95

Browse files
UI: optimize tv control
1 parent 14b4658 commit 01aed95

3 files changed

Lines changed: 70 additions & 27 deletions

File tree

library/src/commonMain/kotlin/project/pipepipe/app/ui/component/player/PlayerControl.kt

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,32 @@ import androidx.compose.animation.fadeOut
88
import androidx.compose.foundation.background
99
import androidx.compose.foundation.basicMarquee
1010
import androidx.compose.foundation.layout.*
11+
import androidx.compose.foundation.shape.CircleShape
1112
import androidx.compose.material.icons.Icons
1213
import androidx.compose.material.icons.automirrored.filled.Undo
1314
import androidx.compose.material.icons.filled.*
14-
import androidx.compose.material3.*
15+
import androidx.compose.material3.FloatingActionButton
16+
import androidx.compose.material3.Icon
17+
import androidx.compose.material3.IconButton
18+
import androidx.compose.material3.IconButtonDefaults
19+
import androidx.compose.material3.MaterialTheme
20+
import androidx.compose.material3.Text
21+
import androidx.compose.material3.TextButton
1522
import androidx.compose.runtime.Composable
1623
import androidx.compose.runtime.collectAsState
1724
import androidx.compose.runtime.getValue
25+
import androidx.compose.runtime.mutableStateOf
26+
import androidx.compose.runtime.remember
27+
import androidx.compose.runtime.setValue
1828
import androidx.compose.ui.Alignment
1929
import androidx.compose.ui.Modifier
30+
import androidx.compose.ui.composed
2031
import androidx.compose.ui.draw.alpha
2132
import androidx.compose.ui.focus.FocusRequester
2233
import androidx.compose.ui.focus.focusRequester
34+
import androidx.compose.ui.focus.onFocusChanged
2335
import androidx.compose.ui.graphics.Color
36+
import androidx.compose.ui.graphics.Shape
2437
import androidx.compose.ui.layout.layout
2538
import androidx.compose.ui.text.PlatformTextStyle
2639
import androidx.compose.ui.text.TextStyle
@@ -102,6 +115,23 @@ fun Modifier.alphaHitTestPassThrough(alpha: Float): Modifier {
102115
}
103116
}
104117

118+
fun Modifier.focusedTVBackground(
119+
focusedColor: Color = Color.White.copy(alpha = 0.5f),
120+
shape: Shape = CircleShape
121+
): Modifier = composed {
122+
if (!SharedContext.isTv) return@composed this
123+
var isFocused by remember { mutableStateOf(false) }
124+
125+
this
126+
.onFocusChanged { isFocused = it.isFocused }
127+
.background(
128+
color = if (isFocused) focusedColor else Color.Transparent,
129+
shape = shape
130+
)
131+
}
132+
133+
134+
105135
@OptIn(ExperimentalLayoutApi::class)
106136
@Composable
107137
fun PlayerControl(
@@ -139,7 +169,14 @@ fun PlayerControl(
139169
label = "controlsAlpha"
140170
)
141171

142-
Box(modifier = Modifier.fillMaxSize()) {
172+
Box(modifier =
173+
Modifier.fillMaxSize().then(
174+
if (playPauseFocusRequester != null) {
175+
Modifier.focusRequester(playPauseFocusRequester)
176+
} else {
177+
Modifier
178+
}
179+
)) {
143180
// Background dim with alpha animation
144181
Box(
145182
modifier = Modifier
@@ -240,7 +277,10 @@ fun PlayerControl(
240277
verticalAlignment = Alignment.Top
241278
) {
242279
if (!isFullscreenMode) {
243-
IconButton(onClick = callbacks.onClose) {
280+
IconButton(
281+
onClick = callbacks.onClose,
282+
modifier = Modifier.focusedTVBackground()
283+
) {
244284
Icon(
245285
Icons.Default.Close,
246286
contentDescription = stringResource(MR.strings.close),
@@ -304,7 +344,7 @@ fun PlayerControl(
304344
}
305345

306346
// Speed button
307-
TextButton(onClick = callbacks.onSpeedPitchClick) {
347+
TextButton(onClick = callbacks.onSpeedPitchClick, modifier = Modifier.focusedTVBackground()) {
308348
Text(
309349
text = if (state.currentSpeed == 1f) "1x" else String.format(
310350
"%.1fx",
@@ -355,7 +395,7 @@ fun PlayerControl(
355395
if (state.timelineSize > 1 && state.currentTimelineIndex < state.timelineSize - 1) {
356396
IconButton(
357397
onClick = callbacks.onSeekToPrevious,
358-
modifier = Modifier.size(40.dp)
398+
modifier = Modifier.size(40.dp).focusedTVBackground(),
359399
) {
360400
Icon(
361401
Icons.Default.SkipPrevious,
@@ -368,14 +408,7 @@ fun PlayerControl(
368408
IconButton(
369409
onClick = callbacks.onPlayPauseClick,
370410
modifier = Modifier
371-
.size(60.dp)
372-
.then(
373-
if (playPauseFocusRequester != null) {
374-
Modifier.focusRequester(playPauseFocusRequester)
375-
} else {
376-
Modifier
377-
}
378-
)
411+
.size(60.dp).focusedTVBackground(),
379412
) {
380413
Icon(
381414
if (state.isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
@@ -389,7 +422,7 @@ fun PlayerControl(
389422
if (state.timelineSize > 1 && state.currentTimelineIndex < state.timelineSize - 1) {
390423
IconButton(
391424
onClick = callbacks.onSeekToNext,
392-
modifier = Modifier.size(40.dp)
425+
modifier = Modifier.size(40.dp).focusedTVBackground(),
393426
) {
394427
Icon(
395428
Icons.Default.SkipNext,
@@ -434,7 +467,10 @@ fun PlayerControl(
434467
color = Color.White,
435468
fontSize = 14.sp
436469
)
437-
IconButton(onClick = callbacks.onFullScreenClick) {
470+
IconButton(
471+
onClick = callbacks.onFullScreenClick,
472+
modifier = Modifier.focusedTVBackground()
473+
) {
438474
Icon(
439475
Icons.Default.Fullscreen,
440476
contentDescription = stringResource(MR.strings.player_rotate_screen),

library/src/commonMain/kotlin/project/pipepipe/app/ui/component/player/PlayerControlMenus.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ fun ResolutionMenu(
4646
fun hasVideoOverride(): Boolean = availableResolutions.count { it.isSelected } == 1
4747

4848
Box {
49-
TextButton(onClick = { onMenuChange(true) }) {
49+
TextButton(
50+
onClick = { onMenuChange(true) },
51+
modifier = Modifier.focusedTVBackground()
52+
) {
5053
Text(
5154
text = if (hasVideoOverride()) availableResolutions.first { it.isSelected }.displayLabel else stringResource(
5255
MR.strings.auto
@@ -116,7 +119,10 @@ fun MoreMenu(
116119
onPipClick: () -> Unit
117120
) {
118121
Box {
119-
TextButton(onClick = { onMenuChange(true) }) {
122+
TextButton(
123+
onClick = { onMenuChange(true) },
124+
modifier = Modifier.focusedTVBackground()
125+
) {
120126
Icon(
121127
imageVector = Icons.Default.MoreVert,
122128
contentDescription = "More options",

library/src/commonMain/kotlin/project/pipepipe/app/ui/component/player/VideoPlayer.kt

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,17 @@ fun VideoPlayer(
135135
val isFullscreenMode =
136136
SharedContext.sharedVideoDetailViewModel.uiState.value.pageState == VideoDetailPageState.FULLSCREEN_PLAYER
137137

138+
// TV support - Request focus to play/pause button when controls become visible
139+
LaunchedEffect(isControlsVisible) {
140+
if (!SharedContext.isTv ) return@LaunchedEffect
141+
if (isControlsVisible) {
142+
delay(400) // Wait for alpha animation to start and component to be placed
143+
playPauseFocusRequester.requestFocus()
144+
} else {
145+
playerFocusRequester.requestFocus()
146+
}
147+
}
148+
138149
// Volume state from PlatformActions
139150
val maxSystemVolume = remember { platformActions.getMaxVolume() }
140151
var volumeOverlayProgress by remember {
@@ -415,12 +426,6 @@ fun VideoPlayer(
415426
if (SharedContext.settingsManager.getBoolean("start_main_player_fullscreen_key")) {
416427
SharedContext.sharedVideoDetailViewModel.toggleFullscreenPlayer()
417428
}
418-
if (SharedContext.isTv) {
419-
gestureScope.launch {
420-
delay(100)
421-
runCatching { playerFocusRequester.requestFocus() }
422-
}
423-
}
424429
}
425430
)
426431
Icon(
@@ -451,10 +456,6 @@ fun VideoPlayer(
451456
Key.DirectionCenter, Key.Enter -> {
452457
if (!isControlsVisible) {
453458
isControlsVisible = true
454-
gestureScope.launch {
455-
delay(100)
456-
runCatching { playPauseFocusRequester.requestFocus() }
457-
}
458459
} else {
459460
// Toggle play/pause when controls visible
460461
if (isPlaying) mediaController.pause() else mediaController.play()

0 commit comments

Comments
 (0)