mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
Compare commits
2 Commits
360cd51e23
...
a0af187e8f
| Author | SHA1 | Date | |
|---|---|---|---|
| a0af187e8f | |||
| 16839942b1 |
@@ -1,6 +1,8 @@
|
||||
package hu.bbara.purefin.tv.player.components
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -10,12 +12,17 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.SemanticsActions
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.stateDescription
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.test.assert
|
||||
@@ -37,17 +44,48 @@ import hu.bbara.purefin.core.player.model.PlayerUiState
|
||||
import hu.bbara.purefin.core.player.model.QueueItemUi
|
||||
import hu.bbara.purefin.core.player.model.TrackOption
|
||||
import hu.bbara.purefin.core.player.model.TrackType
|
||||
import hu.bbara.purefin.tv.player.TV_HIDDEN_STOP_FEEDBACK_MS
|
||||
import hu.bbara.purefin.tv.player.TvPlayerHiddenStopFeedback
|
||||
import hu.bbara.purefin.tv.player.TvPlayerHiddenStopFeedbackTag
|
||||
import hu.bbara.purefin.tv.player.handleTvPlayerRootKeyEvent
|
||||
import hu.bbara.purefin.ui.theme.AppTheme
|
||||
import kotlinx.coroutines.delay
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
private const val TvPlayerRootTag = "tv_player_root"
|
||||
private const val TvPlayerHiddenPauseCountTag = "tv_player_hidden_pause_count"
|
||||
private const val TvPlayerHiddenResumeCountTag = "tv_player_hidden_resume_count"
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class TvPlayerControlsOverlayTest {
|
||||
|
||||
@get:Rule
|
||||
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun requestingControlsFocus_focusesPlayPauseInsteadOfSeekBar() {
|
||||
composeRule.setContent {
|
||||
AppTheme {
|
||||
OverlayHost(
|
||||
uiState = samplePlayerState(),
|
||||
requestFocus = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeRule.waitForIdle()
|
||||
|
||||
composeRule.onNodeWithTag(TvPlayerPlayPauseButtonTag).assertIsFocused()
|
||||
composeRule.onNodeWithTag(TvPlayerSeekBarTag)
|
||||
.assert(
|
||||
SemanticsMatcher.expectValue(
|
||||
SemanticsProperties.Focused,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downFromSeekBar_movesFocusToControlButtonsBeforePlaylist() {
|
||||
composeRule.setContent {
|
||||
@@ -323,6 +361,137 @@ class TvPlayerControlsOverlayTest {
|
||||
composeRule.onAllNodesWithTag(TvPlayerTrackPanelTag).assertCountEquals(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hiddenControls_centerWhilePlaying_pausesWithoutShowingControlsAndShowsStopFeedback() {
|
||||
composeRule.setContent {
|
||||
AppTheme {
|
||||
TrackPanelHost(
|
||||
uiState = samplePlayerState(),
|
||||
initialControlsVisible = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeRule.onNodeWithTag(TvPlayerRootTag)
|
||||
.performSemanticsAction(SemanticsActions.RequestFocus)
|
||||
.performKeyInput {
|
||||
pressKey(Key.DirectionCenter)
|
||||
}
|
||||
|
||||
composeRule.waitForIdle()
|
||||
|
||||
composeRule.onAllNodesWithTag(TvPlayerPlayPauseButtonTag).assertCountEquals(0)
|
||||
composeRule.onNodeWithTag(TvPlayerHiddenStopFeedbackTag).assertIsDisplayed()
|
||||
composeRule.onNodeWithTag(TvPlayerHiddenPauseCountTag)
|
||||
.assert(
|
||||
SemanticsMatcher.expectValue(
|
||||
SemanticsProperties.StateDescription,
|
||||
"1"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hiddenControls_enterWhilePlaying_pausesWithoutShowingControlsAndShowsStopFeedback() {
|
||||
composeRule.setContent {
|
||||
AppTheme {
|
||||
TrackPanelHost(
|
||||
uiState = samplePlayerState(),
|
||||
initialControlsVisible = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeRule.onNodeWithTag(TvPlayerRootTag)
|
||||
.performSemanticsAction(SemanticsActions.RequestFocus)
|
||||
.performKeyInput {
|
||||
pressKey(Key.Enter)
|
||||
}
|
||||
|
||||
composeRule.waitForIdle()
|
||||
|
||||
composeRule.onAllNodesWithTag(TvPlayerPlayPauseButtonTag).assertCountEquals(0)
|
||||
composeRule.onNodeWithTag(TvPlayerHiddenStopFeedbackTag).assertIsDisplayed()
|
||||
composeRule.onNodeWithTag(TvPlayerHiddenPauseCountTag)
|
||||
.assert(
|
||||
SemanticsMatcher.expectValue(
|
||||
SemanticsProperties.StateDescription,
|
||||
"1"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hiddenControls_stopFeedbackAutoHidesAfterTimeout() {
|
||||
composeRule.mainClock.autoAdvance = false
|
||||
|
||||
composeRule.setContent {
|
||||
AppTheme {
|
||||
TrackPanelHost(
|
||||
uiState = samplePlayerState(),
|
||||
initialControlsVisible = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeRule.onNodeWithTag(TvPlayerRootTag)
|
||||
.performSemanticsAction(SemanticsActions.RequestFocus)
|
||||
.performKeyInput {
|
||||
pressKey(Key.DirectionCenter)
|
||||
}
|
||||
|
||||
composeRule.mainClock.advanceTimeByFrame()
|
||||
composeRule.onNodeWithTag(TvPlayerHiddenStopFeedbackTag).assertIsDisplayed()
|
||||
|
||||
composeRule.mainClock.advanceTimeBy(TV_HIDDEN_STOP_FEEDBACK_MS - 1)
|
||||
composeRule.mainClock.advanceTimeByFrame()
|
||||
composeRule.onNodeWithTag(TvPlayerHiddenStopFeedbackTag).assertIsDisplayed()
|
||||
|
||||
composeRule.mainClock.advanceTimeBy(1)
|
||||
composeRule.mainClock.advanceTimeBy(300)
|
||||
composeRule.mainClock.advanceTimeByFrame()
|
||||
composeRule.onAllNodesWithTag(TvPlayerHiddenStopFeedbackTag).assertCountEquals(0)
|
||||
|
||||
composeRule.mainClock.autoAdvance = true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hiddenControls_centerWhilePaused_resumesWithoutShowingControls() {
|
||||
composeRule.setContent {
|
||||
AppTheme {
|
||||
TrackPanelHost(
|
||||
uiState = samplePlayerState().copy(isPlaying = false),
|
||||
initialControlsVisible = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
composeRule.onNodeWithTag(TvPlayerRootTag)
|
||||
.performSemanticsAction(SemanticsActions.RequestFocus)
|
||||
.performKeyInput {
|
||||
pressKey(Key.DirectionCenter)
|
||||
}
|
||||
|
||||
composeRule.waitForIdle()
|
||||
|
||||
composeRule.onAllNodesWithTag(TvPlayerPlayPauseButtonTag).assertCountEquals(0)
|
||||
composeRule.onAllNodesWithTag(TvPlayerHiddenStopFeedbackTag).assertCountEquals(0)
|
||||
composeRule.onNodeWithTag(TvPlayerHiddenPauseCountTag)
|
||||
.assert(
|
||||
SemanticsMatcher.expectValue(
|
||||
SemanticsProperties.StateDescription,
|
||||
"0"
|
||||
)
|
||||
)
|
||||
composeRule.onNodeWithTag(TvPlayerHiddenResumeCountTag)
|
||||
.assert(
|
||||
SemanticsMatcher.expectValue(
|
||||
SemanticsProperties.StateDescription,
|
||||
"1"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun backOnTrackPanel_closesItAndRestoresFocusToOpeningButton() {
|
||||
composeRule.setContent {
|
||||
@@ -433,7 +602,8 @@ class TvPlayerControlsOverlayTest {
|
||||
@Composable
|
||||
private fun OverlayHost(
|
||||
uiState: PlayerUiState,
|
||||
initiallyExpanded: Boolean = false
|
||||
initiallyExpanded: Boolean = false,
|
||||
requestFocus: Boolean = false
|
||||
) {
|
||||
var isPlaylistExpanded by remember { mutableStateOf(initiallyExpanded) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@@ -441,6 +611,12 @@ private fun OverlayHost(
|
||||
val audioButtonFocusRequester = remember { FocusRequester() }
|
||||
val subtitlesButtonFocusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(requestFocus) {
|
||||
if (requestFocus) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.size(width = 960.dp, height = 540.dp)) {
|
||||
TvPlayerControlsOverlay(
|
||||
uiState = uiState,
|
||||
@@ -470,11 +646,17 @@ private fun OverlayHost(
|
||||
|
||||
@Composable
|
||||
private fun TrackPanelHost(
|
||||
uiState: PlayerUiState
|
||||
uiState: PlayerUiState,
|
||||
initialControlsVisible: Boolean = true
|
||||
) {
|
||||
var controlsVisible by remember { mutableStateOf(true) }
|
||||
var controlsVisible by remember { mutableStateOf(initialControlsVisible) }
|
||||
var trackPanelType by remember { mutableStateOf<TvTrackPanelType?>(null) }
|
||||
var pendingTrackButtonFocus by remember { mutableStateOf<TvTrackPanelType?>(null) }
|
||||
var stopFeedbackVisible by remember { mutableStateOf(false) }
|
||||
var stopFeedbackRequestId by remember { mutableStateOf(0) }
|
||||
var pauseWithoutControlsCount by remember { mutableStateOf(0) }
|
||||
var resumeWithoutControlsCount by remember { mutableStateOf(0) }
|
||||
val rootFocusRequester = remember { FocusRequester() }
|
||||
val controlsFocusRequester = remember { FocusRequester() }
|
||||
val qualityButtonFocusRequester = remember { FocusRequester() }
|
||||
val audioButtonFocusRequester = remember { FocusRequester() }
|
||||
@@ -486,6 +668,15 @@ private fun TrackPanelHost(
|
||||
controlsVisible = true
|
||||
}
|
||||
}
|
||||
val pausePlaybackWithoutShowingControls: () -> Unit = {
|
||||
pauseWithoutControlsCount += 1
|
||||
stopFeedbackVisible = true
|
||||
stopFeedbackRequestId += 1
|
||||
}
|
||||
val resumePlaybackWithoutShowingControls: () -> Unit = {
|
||||
resumeWithoutControlsCount += 1
|
||||
stopFeedbackVisible = false
|
||||
}
|
||||
|
||||
LaunchedEffect(trackPanelType, pendingTrackButtonFocus) {
|
||||
val pendingFocus = pendingTrackButtonFocus ?: return@LaunchedEffect
|
||||
@@ -498,18 +689,38 @@ private fun TrackPanelHost(
|
||||
pendingTrackButtonFocus = null
|
||||
}
|
||||
|
||||
LaunchedEffect(stopFeedbackRequestId) {
|
||||
if (stopFeedbackRequestId == 0) return@LaunchedEffect
|
||||
delay(TV_HIDDEN_STOP_FEEDBACK_MS)
|
||||
stopFeedbackVisible = false
|
||||
}
|
||||
|
||||
LaunchedEffect(controlsVisible, trackPanelType) {
|
||||
if (controlsVisible || trackPanelType != null) {
|
||||
stopFeedbackVisible = false
|
||||
} else {
|
||||
rootFocusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(width = 960.dp, height = 540.dp)
|
||||
.focusRequester(rootFocusRequester)
|
||||
.focusable()
|
||||
.testTag(TvPlayerRootTag)
|
||||
.onPreviewKeyEvent { event ->
|
||||
handleTvPlayerRootKeyEvent(
|
||||
event = event,
|
||||
isPlaying = uiState.isPlaying,
|
||||
controlsVisible = controlsVisible,
|
||||
isPlaylistExpanded = false,
|
||||
trackPanelType = trackPanelType,
|
||||
onCloseTrackPanel = closeTrackPanel,
|
||||
onCollapsePlaylist = { controlsVisible = true },
|
||||
onHideControls = { controlsVisible = false },
|
||||
onPausePlaybackWithoutShowingControls = pausePlaybackWithoutShowingControls,
|
||||
onResumePlaybackWithoutShowingControls = resumePlaybackWithoutShowingControls,
|
||||
onSeekRelative = { _ -> controlsVisible = true },
|
||||
onShowControls = { controlsVisible = true },
|
||||
onTogglePlayPause = { controlsVisible = true }
|
||||
@@ -551,6 +762,25 @@ private fun TrackPanelHost(
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = stopFeedbackVisible,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
) {
|
||||
TvPlayerHiddenStopFeedback()
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.testTag(TvPlayerHiddenPauseCountTag)
|
||||
.semantics { stateDescription = pauseWithoutControlsCount.toString() }
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.testTag(TvPlayerHiddenResumeCountTag)
|
||||
.semantics { stateDescription = resumeWithoutControlsCount.toString() }
|
||||
)
|
||||
|
||||
trackPanelType?.let { panelType ->
|
||||
TvTrackSelectionPanel(
|
||||
panelType = panelType,
|
||||
|
||||
@@ -14,6 +14,13 @@ import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Pause
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -24,6 +31,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -33,6 +41,8 @@ import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
@@ -44,8 +54,11 @@ import hu.bbara.purefin.tv.player.components.TvPlayerControlsOverlay
|
||||
import hu.bbara.purefin.tv.player.components.TvPlayerLoadingErrorEndCard
|
||||
import hu.bbara.purefin.tv.player.components.TvTrackPanelType
|
||||
import hu.bbara.purefin.tv.player.components.TvTrackSelectionPanel
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
private const val TV_CONTROLS_AUTO_HIDE_MS = 5_000L
|
||||
internal const val TV_HIDDEN_STOP_FEEDBACK_MS = 1_200L
|
||||
internal const val TvPlayerHiddenStopFeedbackTag = "tv_player_hidden_stop_feedback"
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
@@ -63,6 +76,8 @@ fun TvPlayerScreen(
|
||||
var isPlaylistExpanded by remember { mutableStateOf(false) }
|
||||
var trackPanelType by remember { mutableStateOf<TvTrackPanelType?>(null) }
|
||||
var pendingTrackButtonFocus by remember { mutableStateOf<TvTrackPanelType?>(null) }
|
||||
var stopFeedbackVisible by remember { mutableStateOf(false) }
|
||||
var stopFeedbackRequestId by remember { mutableStateOf(0) }
|
||||
val controlsAutoHideBlocked = isPlaylistExpanded || trackPanelType != null
|
||||
|
||||
val context = LocalContext.current
|
||||
@@ -118,6 +133,15 @@ fun TvPlayerScreen(
|
||||
val togglePlayPauseAndShowControls: () -> Unit = {
|
||||
viewModel.togglePlayPause(TV_CONTROLS_AUTO_HIDE_MS)
|
||||
}
|
||||
val pausePlaybackWithoutShowingControls: () -> Unit = {
|
||||
viewModel.pausePlayback()
|
||||
stopFeedbackVisible = true
|
||||
stopFeedbackRequestId += 1
|
||||
}
|
||||
val resumePlaybackWithoutShowingControls: () -> Unit = {
|
||||
viewModel.resumePlayback()
|
||||
stopFeedbackVisible = false
|
||||
}
|
||||
val seekAndShowControls: (Long) -> Unit = { positionMs ->
|
||||
viewModel.seekTo(positionMs)
|
||||
showTvControls()
|
||||
@@ -164,6 +188,18 @@ fun TvPlayerScreen(
|
||||
pendingTrackButtonFocus = null
|
||||
}
|
||||
|
||||
LaunchedEffect(stopFeedbackRequestId) {
|
||||
if (stopFeedbackRequestId == 0) return@LaunchedEffect
|
||||
delay(TV_HIDDEN_STOP_FEEDBACK_MS)
|
||||
stopFeedbackVisible = false
|
||||
}
|
||||
|
||||
LaunchedEffect(controlsVisible, isPlaylistExpanded, trackPanelType, uiState.isEnded, uiState.error) {
|
||||
if (controlsVisible || isPlaylistExpanded || trackPanelType != null || uiState.isEnded || uiState.error != null) {
|
||||
stopFeedbackVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(enabled = true) {
|
||||
when {
|
||||
trackPanelType != null -> closeTrackPanel()
|
||||
@@ -181,12 +217,15 @@ fun TvPlayerScreen(
|
||||
.onPreviewKeyEvent { event ->
|
||||
handleTvPlayerRootKeyEvent(
|
||||
event = event,
|
||||
isPlaying = uiState.isPlaying,
|
||||
controlsVisible = controlsVisible,
|
||||
isPlaylistExpanded = isPlaylistExpanded,
|
||||
trackPanelType = trackPanelType,
|
||||
onCloseTrackPanel = closeTrackPanel,
|
||||
onCollapsePlaylist = collapsePlaylistToControls,
|
||||
onHideControls = { viewModel.toggleControlsVisibility() },
|
||||
onPausePlaybackWithoutShowingControls = pausePlaybackWithoutShowingControls,
|
||||
onResumePlaybackWithoutShowingControls = resumePlaybackWithoutShowingControls,
|
||||
onSeekRelative = seekByAndShowControls,
|
||||
onShowControls = showTvControls,
|
||||
onTogglePlayPause = togglePlayPauseAndShowControls
|
||||
@@ -254,6 +293,15 @@ fun TvPlayerScreen(
|
||||
onDismissError = { viewModel.clearError() }
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = stopFeedbackVisible,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
) {
|
||||
TvPlayerHiddenStopFeedback()
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = trackPanelType != null,
|
||||
enter = slideInHorizontally { it },
|
||||
@@ -277,12 +325,15 @@ fun TvPlayerScreen(
|
||||
|
||||
internal fun handleTvPlayerRootKeyEvent(
|
||||
event: androidx.compose.ui.input.key.KeyEvent,
|
||||
isPlaying: Boolean,
|
||||
controlsVisible: Boolean,
|
||||
isPlaylistExpanded: Boolean,
|
||||
trackPanelType: TvTrackPanelType?,
|
||||
onCloseTrackPanel: () -> Unit,
|
||||
onCollapsePlaylist: () -> Unit,
|
||||
onHideControls: () -> Unit,
|
||||
onPausePlaybackWithoutShowingControls: () -> Unit,
|
||||
onResumePlaybackWithoutShowingControls: () -> Unit,
|
||||
onSeekRelative: (Long) -> Unit,
|
||||
onShowControls: () -> Unit,
|
||||
onTogglePlayPause: () -> Unit
|
||||
@@ -328,7 +379,11 @@ internal fun handleTvPlayerRootKeyEvent(
|
||||
}
|
||||
|
||||
Key.DirectionCenter, Key.Enter -> {
|
||||
onTogglePlayPause()
|
||||
if (isPlaying) {
|
||||
onPausePlaybackWithoutShowingControls()
|
||||
} else {
|
||||
onResumePlaybackWithoutShowingControls()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -338,3 +393,24 @@ internal fun handleTvPlayerRootKeyEvent(
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TvPlayerHiddenStopFeedback(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.testTag(TvPlayerHiddenStopFeedbackTag)
|
||||
.clip(CircleShape)
|
||||
.background(Color.Black.copy(alpha = 0.72f))
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Pause,
|
||||
contentDescription = "Pause playback",
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.size(72.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@ private fun TvPlayerBottomSection(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val playPauseFocusRequester = remember { FocusRequester() }
|
||||
val playlistFocusRequester = remember { FocusRequester() }
|
||||
var wasPlaylistExpanded by remember { mutableStateOf(isPlaylistExpanded) }
|
||||
|
||||
@@ -194,7 +193,7 @@ private fun TvPlayerBottomSection(
|
||||
if (isPlaylistExpanded && uiState.queue.isNotEmpty()) {
|
||||
playlistFocusRequester.requestFocus()
|
||||
} else if (wasPlaylistExpanded) {
|
||||
playPauseFocusRequester.requestFocus()
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
wasPlaylistExpanded = isPlaylistExpanded
|
||||
}
|
||||
@@ -255,10 +254,9 @@ private fun TvPlayerBottomSection(
|
||||
onSeekRelative = onSeekRelative,
|
||||
togglePlayState = onPlayPause,
|
||||
onMoveDown = {
|
||||
playPauseFocusRequester.requestFocus()
|
||||
focusRequester.requestFocus()
|
||||
true
|
||||
},
|
||||
focusRequester = focusRequester,
|
||||
modifier = Modifier.testTag(TvPlayerSeekBarTag)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
@@ -292,7 +290,7 @@ private fun TvPlayerBottomSection(
|
||||
onClick = onPlayPause,
|
||||
size = 72,
|
||||
modifier = expandPlaylistModifier
|
||||
.focusRequester(playPauseFocusRequester)
|
||||
.focusRequester(focusRequester)
|
||||
.testTag(TvPlayerPlayPauseButtonTag)
|
||||
)
|
||||
TvIconButton(
|
||||
|
||||
@@ -139,6 +139,18 @@ class PlayerManager @Inject constructor(
|
||||
if (player.isPlaying) player.pause() else player.play()
|
||||
}
|
||||
|
||||
fun pausePlayback() {
|
||||
if (player.isPlaying) {
|
||||
player.pause()
|
||||
}
|
||||
}
|
||||
|
||||
fun resumePlayback() {
|
||||
if (!player.isPlaying) {
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
|
||||
fun seekTo(positionMs: Long) {
|
||||
val target = clampSeekPosition(positionMs)
|
||||
pendingSeekPositionMs = target
|
||||
|
||||
@@ -188,6 +188,14 @@ class PlayerViewModel @Inject constructor(
|
||||
showControls(autoHideDelayMs)
|
||||
}
|
||||
|
||||
fun pausePlayback() {
|
||||
playerManager.pausePlayback()
|
||||
}
|
||||
|
||||
fun resumePlayback() {
|
||||
playerManager.resumePlayback()
|
||||
}
|
||||
|
||||
fun seekTo(positionMs: Long) {
|
||||
playerManager.seekTo(positionMs)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user