mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
Fix TV player track selector focus
This commit is contained in:
@@ -40,4 +40,5 @@ dependencies {
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.jellyfin.core)
|
||||
implementation(libs.okhttp)
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package hu.bbara.purefin.core.player.viewmodel
|
||||
|
||||
enum class ControlsAutoHideBlocker {
|
||||
PLAYLIST,
|
||||
TRACK_PANEL
|
||||
}
|
||||
|
||||
internal sealed interface ControlsAutoHideCommand {
|
||||
data object Cancel : ControlsAutoHideCommand
|
||||
data class Schedule(val delayMs: Long) : ControlsAutoHideCommand
|
||||
}
|
||||
|
||||
internal class ControlsAutoHidePolicy(
|
||||
private val defaultDelayMs: Long
|
||||
) {
|
||||
private val blockers = mutableSetOf<ControlsAutoHideBlocker>()
|
||||
private var isPlaying = false
|
||||
|
||||
var controlsVisible: Boolean = true
|
||||
private set
|
||||
|
||||
var lastAutoHideDelayMs: Long = defaultDelayMs
|
||||
private set
|
||||
|
||||
fun onPlaybackChanged(isPlaying: Boolean): ControlsAutoHideCommand {
|
||||
this.isPlaying = isPlaying
|
||||
return nextCommand()
|
||||
}
|
||||
|
||||
fun setAutoHideDelay(delayMs: Long): ControlsAutoHideCommand {
|
||||
lastAutoHideDelayMs = delayMs
|
||||
return nextCommand()
|
||||
}
|
||||
|
||||
fun showControls(delayMs: Long? = null): ControlsAutoHideCommand {
|
||||
delayMs?.let { lastAutoHideDelayMs = it }
|
||||
controlsVisible = true
|
||||
return nextCommand()
|
||||
}
|
||||
|
||||
fun toggleControlsVisibility(): ControlsAutoHideCommand {
|
||||
controlsVisible = !controlsVisible
|
||||
return nextCommand()
|
||||
}
|
||||
|
||||
fun hideControls(): ControlsAutoHideCommand {
|
||||
controlsVisible = false
|
||||
return ControlsAutoHideCommand.Cancel
|
||||
}
|
||||
|
||||
fun setBlocked(
|
||||
blocker: ControlsAutoHideBlocker,
|
||||
blocked: Boolean
|
||||
): ControlsAutoHideCommand {
|
||||
if (blocked) {
|
||||
blockers += blocker
|
||||
controlsVisible = true
|
||||
} else {
|
||||
blockers -= blocker
|
||||
}
|
||||
return nextCommand()
|
||||
}
|
||||
|
||||
private fun nextCommand(): ControlsAutoHideCommand {
|
||||
return if (!controlsVisible || !isPlaying || blockers.isNotEmpty()) {
|
||||
ControlsAutoHideCommand.Cancel
|
||||
} else {
|
||||
ControlsAutoHideCommand.Schedule(lastAutoHideDelayMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ class PlayerViewModel @Inject constructor(
|
||||
private val _controlsVisible = MutableStateFlow(true)
|
||||
val controlsVisible: StateFlow<Boolean> = _controlsVisible.asStateFlow()
|
||||
|
||||
private val controlsAutoHidePolicy = ControlsAutoHidePolicy(DEFAULT_CONTROLS_AUTO_HIDE_MS)
|
||||
private var autoHideJob: Job? = null
|
||||
private var lastNextUpMediaId: String? = null
|
||||
private var dataErrorMessage: String? = null
|
||||
@@ -68,6 +69,9 @@ class PlayerViewModel @Inject constructor(
|
||||
error = state.error ?: dataErrorMessage
|
||||
)
|
||||
}
|
||||
applyControlsAutoHideCommand(
|
||||
controlsAutoHidePolicy.onPlaybackChanged(state.isPlaying)
|
||||
)
|
||||
if (state.isEnded) {
|
||||
showControls()
|
||||
}
|
||||
@@ -196,26 +200,42 @@ class PlayerViewModel @Inject constructor(
|
||||
playerManager.seekToLiveEdge()
|
||||
}
|
||||
|
||||
fun setControlsAutoHideDelay(autoHideDelayMs: Long) {
|
||||
applyControlsAutoHideCommand(
|
||||
controlsAutoHidePolicy.setAutoHideDelay(autoHideDelayMs)
|
||||
)
|
||||
}
|
||||
|
||||
fun setControlsAutoHideBlocked(
|
||||
blocker: ControlsAutoHideBlocker,
|
||||
blocked: Boolean
|
||||
) {
|
||||
applyControlsAutoHideCommand(
|
||||
controlsAutoHidePolicy.setBlocked(blocker, blocked)
|
||||
)
|
||||
}
|
||||
|
||||
fun showControls(autoHideDelayMs: Long = DEFAULT_CONTROLS_AUTO_HIDE_MS) {
|
||||
_controlsVisible.value = true
|
||||
scheduleAutoHide(autoHideDelayMs)
|
||||
applyControlsAutoHideCommand(
|
||||
controlsAutoHidePolicy.showControls(autoHideDelayMs)
|
||||
)
|
||||
}
|
||||
|
||||
fun toggleControlsVisibility() {
|
||||
_controlsVisible.value = !_controlsVisible.value
|
||||
if (_controlsVisible.value) {
|
||||
scheduleAutoHide(DEFAULT_CONTROLS_AUTO_HIDE_MS)
|
||||
} else {
|
||||
autoHideJob?.cancel()
|
||||
}
|
||||
applyControlsAutoHideCommand(
|
||||
controlsAutoHidePolicy.toggleControlsVisibility()
|
||||
)
|
||||
}
|
||||
|
||||
private fun scheduleAutoHide(autoHideDelayMs: Long) {
|
||||
private fun applyControlsAutoHideCommand(command: ControlsAutoHideCommand) {
|
||||
_controlsVisible.value = controlsAutoHidePolicy.controlsVisible
|
||||
autoHideJob?.cancel()
|
||||
if (!player.isPlaying) return
|
||||
autoHideJob = null
|
||||
if (command !is ControlsAutoHideCommand.Schedule) return
|
||||
autoHideJob = viewModelScope.launch {
|
||||
delay(autoHideDelayMs)
|
||||
_controlsVisible.value = false
|
||||
delay(command.delayMs)
|
||||
controlsAutoHidePolicy.hideControls()
|
||||
_controlsVisible.value = controlsAutoHidePolicy.controlsVisible
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package hu.bbara.purefin.core.player.viewmodel
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class ControlsAutoHidePolicyTest {
|
||||
|
||||
@Test
|
||||
fun activeBlockerCancelsScheduledAutoHide() {
|
||||
val policy = ControlsAutoHidePolicy(defaultDelayMs = 3_500L)
|
||||
|
||||
assertEquals(
|
||||
ControlsAutoHideCommand.Schedule(3_500L),
|
||||
policy.onPlaybackChanged(isPlaying = true)
|
||||
)
|
||||
assertEquals(
|
||||
ControlsAutoHideCommand.Cancel,
|
||||
policy.setBlocked(ControlsAutoHideBlocker.TRACK_PANEL, blocked = true)
|
||||
)
|
||||
assertTrue(policy.controlsVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun playlistBlockerPreventsAutoHideUntilCleared() {
|
||||
val policy = ControlsAutoHidePolicy(defaultDelayMs = 3_500L)
|
||||
|
||||
policy.onPlaybackChanged(isPlaying = true)
|
||||
policy.showControls(delayMs = 5_000L)
|
||||
|
||||
assertEquals(
|
||||
ControlsAutoHideCommand.Cancel,
|
||||
policy.setBlocked(ControlsAutoHideBlocker.PLAYLIST, blocked = true)
|
||||
)
|
||||
assertEquals(
|
||||
ControlsAutoHideCommand.Schedule(5_000L),
|
||||
policy.setBlocked(ControlsAutoHideBlocker.PLAYLIST, blocked = false)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun trackPanelBlockerUsesRememberedDelayWhenRemoved() {
|
||||
val policy = ControlsAutoHidePolicy(defaultDelayMs = 3_500L)
|
||||
|
||||
policy.onPlaybackChanged(isPlaying = true)
|
||||
assertEquals(
|
||||
ControlsAutoHideCommand.Cancel,
|
||||
policy.setBlocked(ControlsAutoHideBlocker.TRACK_PANEL, blocked = true)
|
||||
)
|
||||
policy.showControls(delayMs = 5_000L)
|
||||
|
||||
assertEquals(
|
||||
ControlsAutoHideCommand.Schedule(5_000L),
|
||||
policy.setBlocked(ControlsAutoHideBlocker.TRACK_PANEL, blocked = false)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun autoHideResumesOnlyAfterLastBlockerIsCleared() {
|
||||
val policy = ControlsAutoHidePolicy(defaultDelayMs = 3_500L)
|
||||
|
||||
policy.onPlaybackChanged(isPlaying = true)
|
||||
policy.showControls(delayMs = 5_000L)
|
||||
policy.setBlocked(ControlsAutoHideBlocker.PLAYLIST, blocked = true)
|
||||
policy.setBlocked(ControlsAutoHideBlocker.TRACK_PANEL, blocked = true)
|
||||
|
||||
assertEquals(
|
||||
ControlsAutoHideCommand.Cancel,
|
||||
policy.setBlocked(ControlsAutoHideBlocker.PLAYLIST, blocked = false)
|
||||
)
|
||||
assertEquals(
|
||||
ControlsAutoHideCommand.Schedule(5_000L),
|
||||
policy.setBlocked(ControlsAutoHideBlocker.TRACK_PANEL, blocked = false)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun blockingStateMakesHiddenControlsVisibleAgain() {
|
||||
val policy = ControlsAutoHidePolicy(defaultDelayMs = 3_500L)
|
||||
|
||||
policy.onPlaybackChanged(isPlaying = true)
|
||||
policy.toggleControlsVisibility()
|
||||
|
||||
assertEquals(
|
||||
ControlsAutoHideCommand.Cancel,
|
||||
policy.setBlocked(ControlsAutoHideBlocker.PLAYLIST, blocked = true)
|
||||
)
|
||||
assertTrue(policy.controlsVisible)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user