feat: Implement auto-hide delay for TV player controls

This commit is contained in:
2026-04-08 20:19:04 +02:00
parent 98c936aea6
commit b41a9c24a2
2 changed files with 56 additions and 28 deletions

View File

@@ -45,6 +45,8 @@ import hu.bbara.purefin.tv.player.components.TvPlayerQueuePanel
import hu.bbara.purefin.tv.player.components.TvTrackPanelType import hu.bbara.purefin.tv.player.components.TvTrackPanelType
import hu.bbara.purefin.tv.player.components.TvTrackSelectionPanel import hu.bbara.purefin.tv.player.components.TvTrackSelectionPanel
private const val TV_CONTROLS_AUTO_HIDE_MS = 5_000L
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@Composable @Composable
fun TvPlayerScreen( fun TvPlayerScreen(
@@ -82,12 +84,36 @@ fun TvPlayerScreen(
LaunchedEffect(uiState.isPlaying) { LaunchedEffect(uiState.isPlaying) {
if (uiState.isPlaying && controlsVisible) { if (uiState.isPlaying && controlsVisible) {
viewModel.toggleControlsVisibility() viewModel.showControls(TV_CONTROLS_AUTO_HIDE_MS)
} }
} }
val hiddenControlFocusRequester = remember { FocusRequester() } val hiddenControlFocusRequester = remember { FocusRequester() }
val controlsFocusRequester = remember { FocusRequester() } val controlsFocusRequester = remember { FocusRequester() }
val showTvControls: () -> Unit = {
viewModel.showControls(TV_CONTROLS_AUTO_HIDE_MS)
}
val togglePlayPauseAndShowControls: () -> Unit = {
viewModel.togglePlayPause(TV_CONTROLS_AUTO_HIDE_MS)
}
val seekAndShowControls: (Long) -> Unit = { positionMs ->
viewModel.seekTo(positionMs)
showTvControls()
}
val seekByAndShowControls: (Long) -> Unit = { deltaMs ->
viewModel.seekBy(deltaMs)
showTvControls()
}
val seekToLiveEdgeAndShowControls: () -> Unit = {
viewModel.seekToLiveEdge()
showTvControls()
}
val nextAndShowControls: () -> Unit = {
viewModel.next(TV_CONTROLS_AUTO_HIDE_MS)
}
val previousAndShowControls: () -> Unit = {
viewModel.previous(TV_CONTROLS_AUTO_HIDE_MS)
}
LaunchedEffect(controlsVisible) { LaunchedEffect(controlsVisible) {
if (controlsVisible) { if (controlsVisible) {
@@ -106,23 +132,22 @@ fun TvPlayerScreen(
if (!controlsVisible && event.type == KeyEventType.KeyDown) { if (!controlsVisible && event.type == KeyEventType.KeyDown) {
when (event.key) { when (event.key) {
Key.DirectionLeft -> { Key.DirectionLeft -> {
viewModel.seekBy(-10_000) seekByAndShowControls(-10_000)
true true
} }
Key.DirectionRight -> { Key.DirectionRight -> {
viewModel.seekBy(10_000) seekByAndShowControls(10_000)
true true
} }
Key.DirectionUp, Key.DirectionDown -> { Key.DirectionUp, Key.DirectionDown -> {
viewModel.showControls() showTvControls()
true true
} }
Key.DirectionCenter, Key.Enter -> { Key.DirectionCenter, Key.Enter -> {
viewModel.togglePlayPause() togglePlayPauseAndShowControls()
viewModel.showControls()
true true
} }
@@ -157,12 +182,12 @@ fun TvPlayerScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
uiState = uiState, uiState = uiState,
focusRequester = controlsFocusRequester, focusRequester = controlsFocusRequester,
onPlayPause = { viewModel.togglePlayPause() }, onPlayPause = togglePlayPauseAndShowControls,
onSeek = { viewModel.seekTo(it) }, onSeek = seekAndShowControls,
onSeekRelative = { viewModel.seekBy(it) }, onSeekRelative = seekByAndShowControls,
onSeekLiveEdge = { viewModel.seekToLiveEdge() }, onSeekLiveEdge = seekToLiveEdgeAndShowControls,
onNext = { viewModel.next() }, onNext = nextAndShowControls,
onPrevious = { viewModel.previous() }, onPrevious = previousAndShowControls,
onOpenAudioPanel = { trackPanelType = TvTrackPanelType.AUDIO }, onOpenAudioPanel = { trackPanelType = TvTrackPanelType.AUDIO },
onOpenSubtitlesPanel = { trackPanelType = TvTrackPanelType.SUBTITLES }, onOpenSubtitlesPanel = { trackPanelType = TvTrackPanelType.SUBTITLES },
onOpenQualityPanel = { trackPanelType = TvTrackPanelType.QUALITY }, onOpenQualityPanel = { trackPanelType = TvTrackPanelType.QUALITY },
@@ -174,10 +199,10 @@ fun TvPlayerScreen(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
uiState = uiState, uiState = uiState,
onRetry = { viewModel.retry() }, onRetry = { viewModel.retry() },
onNext = { viewModel.next() }, onNext = nextAndShowControls,
onReplay = { onReplay = {
viewModel.seekTo(0L) viewModel.seekTo(0L)
viewModel.togglePlayPause() togglePlayPauseAndShowControls()
}, },
onDismissError = { viewModel.clearError() } onDismissError = { viewModel.clearError() }
) )
@@ -209,7 +234,7 @@ fun TvPlayerScreen(
TvPlayerQueuePanel( TvPlayerQueuePanel(
uiState = uiState, uiState = uiState,
onSelect = { id -> onSelect = { id ->
viewModel.playQueueItem(id) viewModel.playQueueItem(id, TV_CONTROLS_AUTO_HIDE_MS)
showQueuePanel = false showQueuePanel = false
}, },
onClose = { showQueuePanel = false }, onClose = { showQueuePanel = false },

View File

@@ -29,6 +29,9 @@ class PlayerViewModel @Inject constructor(
private val mediaRepository: MediaRepository, private val mediaRepository: MediaRepository,
private val progressManager: ProgressManager private val progressManager: ProgressManager
) : ViewModel() { ) : ViewModel() {
companion object {
private const val DEFAULT_CONTROLS_AUTO_HIDE_MS = 3_500L
}
val player get() = playerManager.player val player get() = playerManager.player
@@ -176,9 +179,9 @@ class PlayerViewModel @Inject constructor(
} }
} }
fun togglePlayPause() { fun togglePlayPause(autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) {
playerManager.togglePlayPause() playerManager.togglePlayPause()
showControls() showControls(autoHideDelayMs)
} }
fun seekTo(positionMs: Long) { fun seekTo(positionMs: Long) {
@@ -193,33 +196,33 @@ class PlayerViewModel @Inject constructor(
playerManager.seekToLiveEdge() playerManager.seekToLiveEdge()
} }
fun showControls() { fun showControls(autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) {
_controlsVisible.value = true _controlsVisible.value = true
scheduleAutoHide() scheduleAutoHide(autoHideDelayMs)
} }
fun toggleControlsVisibility() { fun toggleControlsVisibility() {
_controlsVisible.value = !_controlsVisible.value _controlsVisible.value = !_controlsVisible.value
if (_controlsVisible.value) scheduleAutoHide() if (_controlsVisible.value) scheduleAutoHide(DEFAULT_CONTROLS_AUTO_HIDE_MS)
} }
private fun scheduleAutoHide() { private fun scheduleAutoHide(autoHideDelayMs: Long) {
autoHideJob?.cancel() autoHideJob?.cancel()
if (!player.isPlaying) return if (!player.isPlaying) return
autoHideJob = viewModelScope.launch { autoHideJob = viewModelScope.launch {
delay(3500) delay(autoHideDelayMs)
_controlsVisible.value = false _controlsVisible.value = false
} }
} }
fun next() { fun next(autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) {
playerManager.next() playerManager.next()
showControls() showControls(autoHideDelayMs)
} }
fun previous() { fun previous(autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) {
playerManager.previous() playerManager.previous()
showControls() showControls(autoHideDelayMs)
} }
fun selectTrack(option: TrackOption) { fun selectTrack(option: TrackOption) {
@@ -235,9 +238,9 @@ class PlayerViewModel @Inject constructor(
playerManager.retry() playerManager.retry()
} }
fun playQueueItem(id: String) { fun playQueueItem(id: String, autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) {
playerManager.playQueueItem(id) playerManager.playQueueItem(id)
showControls() showControls(autoHideDelayMs)
} }
fun clearError() { fun clearError() {