Fix TV detail playback single-press behavior

This commit is contained in:
2026-04-06 20:38:05 +02:00
parent 9e8772467c
commit b91a8a81e8
7 changed files with 99 additions and 2 deletions

View File

@@ -7,11 +7,16 @@ import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performKeyInput
import androidx.compose.ui.test.pressKey
import androidx.compose.ui.input.key.Key
import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
import org.junit.Assert.assertEquals
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.util.UUID import java.util.UUID
@@ -66,6 +71,32 @@ class EpisodeScreenContentTest {
composeRule.onAllNodesWithText("Series").assertCountEquals(0) composeRule.onAllNodesWithText("Series").assertCountEquals(0)
} }
@Test
fun episodeScreenContent_startsPlaybackOnFirstCenterPress() {
var playCount = 0
composeRule.setContent {
AppTheme {
EpisodeScreenContent(
episode = sampleEpisode(progress = 63.0),
seriesTitle = "Severance",
onPlay = { playCount++ }
)
}
}
composeRule.waitForIdle()
composeRule.onNodeWithTag(EpisodePlayButtonTag).assertIsFocused()
composeRule.onRoot().performKeyInput {
pressKey(Key.DirectionCenter)
}
composeRule.waitForIdle()
assertEquals(1, playCount)
}
private fun sampleEpisode(progress: Double?): Episode { private fun sampleEpisode(progress: Double?): Episode {
val seriesId = UUID.fromString("11111111-1111-1111-1111-111111111111") val seriesId = UUID.fromString("11111111-1111-1111-1111-111111111111")
val seasonId = UUID.fromString("22222222-2222-2222-2222-222222222222") val seasonId = UUID.fromString("22222222-2222-2222-2222-222222222222")

View File

@@ -7,11 +7,16 @@ import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performKeyInput
import androidx.compose.ui.test.pressKey
import androidx.compose.ui.input.key.Key
import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Movie import hu.bbara.purefin.core.model.Movie
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
import org.junit.Assert.assertEquals
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.util.UUID import java.util.UUID
@@ -43,6 +48,31 @@ class MovieScreenContentTest {
.assertIsFocused() .assertIsFocused()
} }
@Test
fun movieScreenContent_startsPlaybackOnFirstCenterPress() {
var playCount = 0
composeRule.setContent {
AppTheme {
MovieScreenContent(
movie = sampleMovie(progress = 42.0),
onPlay = { playCount++ }
)
}
}
composeRule.waitForIdle()
composeRule.onNodeWithTag(MoviePlayButtonTag).assertIsFocused()
composeRule.onRoot().performKeyInput {
pressKey(Key.DirectionCenter)
}
composeRule.waitForIdle()
assertEquals(1, playCount)
}
private fun sampleMovie(progress: Double?): Movie { private fun sampleMovie(progress: Double?): Movie {
return Movie( return Movie(
id = UUID.fromString("11111111-1111-1111-1111-111111111111"), id = UUID.fromString("11111111-1111-1111-1111-111111111111"),

View File

@@ -5,13 +5,18 @@ import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performKeyInput
import androidx.compose.ui.test.pressKey
import androidx.compose.ui.input.key.Key
import hu.bbara.purefin.core.model.CastMember import hu.bbara.purefin.core.model.CastMember
import hu.bbara.purefin.core.model.Episode import hu.bbara.purefin.core.model.Episode
import hu.bbara.purefin.core.model.Season import hu.bbara.purefin.core.model.Season
import hu.bbara.purefin.core.model.Series import hu.bbara.purefin.core.model.Series
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
import org.junit.Assert.assertEquals
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.util.UUID import java.util.UUID
@@ -65,6 +70,31 @@ class SeriesScreenContentTest {
.assertIsFocused() .assertIsFocused()
} }
@Test
fun seriesScreenContent_startsPlaybackOnFirstCenterPress() {
var playCount = 0
composeRule.setContent {
AppTheme {
SeriesScreenContent(
series = sampleSeriesWithEpisodes(),
onPlayEpisode = { playCount++ }
)
}
}
composeRule.waitForIdle()
composeRule.onNodeWithTag(SeriesPlayButtonTag).assertIsFocused()
composeRule.onRoot().performKeyInput {
pressKey(Key.DirectionCenter)
}
composeRule.waitForIdle()
assertEquals(1, playCount)
}
private fun sampleSeriesWithEpisodes(): Series { private fun sampleSeriesWithEpisodes(): Series {
val seriesId = UUID.fromString("11111111-1111-1111-1111-111111111111") val seriesId = UUID.fromString("11111111-1111-1111-1111-111111111111")
val seasonId = UUID.fromString("22222222-2222-2222-2222-222222222222") val seasonId = UUID.fromString("22222222-2222-2222-2222-222222222222")

View File

@@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -75,6 +76,7 @@ internal fun EpisodeScreenContent(
val playFocusRequester = remember { FocusRequester() } val playFocusRequester = remember { FocusRequester() }
LaunchedEffect(episode.id) { LaunchedEffect(episode.id) {
withFrameNanos { }
playFocusRequester.requestFocus() playFocusRequester.requestFocus()
} }

View File

@@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -54,6 +55,7 @@ internal fun MovieScreenContent(
val playFocusRequester = remember { FocusRequester() } val playFocusRequester = remember { FocusRequester() }
LaunchedEffect(movie.id) { LaunchedEffect(movie.id) {
withFrameNanos { }
playFocusRequester.requestFocus() playFocusRequester.requestFocus()
} }

View File

@@ -7,10 +7,11 @@ import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -63,6 +64,7 @@ internal fun SeriesScreenContent(
val firstContentFocusRequester = remember { FocusRequester() } val firstContentFocusRequester = remember { FocusRequester() }
LaunchedEffect(series.id, nextUpEpisode?.id) { LaunchedEffect(series.id, nextUpEpisode?.id) {
withFrameNanos { }
if (nextUpEpisode != null) { if (nextUpEpisode != null) {
playFocusRequester.requestFocus() playFocusRequester.requestFocus()
} else { } else {

View File

@@ -68,8 +68,8 @@ fun MediaResumeButton(
.border(3.dp, focusBorderColor, focusShape) .border(3.dp, focusBorderColor, focusShape)
.clip(focusShape) .clip(focusShape)
.onFocusChanged { isFocused = it.isFocused || it.hasFocus } .onFocusChanged { isFocused = it.isFocused || it.hasFocus }
.focusable()
.clickable(onClick = onClick) .clickable(onClick = onClick)
.focusable()
) { ) {
// Bottom layer: inverted colors (visible for the remaining %) // Bottom layer: inverted colors (visible for the remaining %)
Box( Box(