mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +00:00
feat(tvseries): enhance episode focus handling and scrolling behavior
This commit is contained in:
@@ -81,6 +81,9 @@ internal fun TvSeriesScreenContent(
|
|||||||
val initialFocusSeason = series.seasons.firstOrNull { it.id == initialFocusSeasonId } ?: defaultSeason
|
val initialFocusSeason = series.seasons.firstOrNull { it.id == initialFocusSeasonId } ?: defaultSeason
|
||||||
val initialFocusedEpisodeId = initialFocusSeason.focusTargetEpisodeId(focusedEpisodeId)
|
val initialFocusedEpisodeId = initialFocusSeason.focusTargetEpisodeId(focusedEpisodeId)
|
||||||
val seasonTabFocusRequester = remember { FocusRequester() }
|
val seasonTabFocusRequester = remember { FocusRequester() }
|
||||||
|
var requestedInitialEpisodeFocus by remember(series.id, focusedSeasonId, focusedEpisodeId) {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
val waitingForInitialEpisodes = initialFocusedEpisodeId == null &&
|
val waitingForInitialEpisodes = initialFocusedEpisodeId == null &&
|
||||||
initialFocusSeason.episodes.isEmpty() &&
|
initialFocusSeason.episodes.isEmpty() &&
|
||||||
initialFocusSeason.episodeCount > 0
|
initialFocusSeason.episodeCount > 0
|
||||||
@@ -127,6 +130,9 @@ internal fun TvSeriesScreenContent(
|
|||||||
episodes = selectedSeason.episodes,
|
episodes = selectedSeason.episodes,
|
||||||
onPlayEpisode = { onPlayEpisode(it.id) },
|
onPlayEpisode = { onPlayEpisode(it.id) },
|
||||||
focusedEpisodeId = initialFocusedEpisodeId,
|
focusedEpisodeId = initialFocusedEpisodeId,
|
||||||
|
requestFocus = selectedSeason.id == initialFocusSeasonId && !requestedInitialEpisodeFocus,
|
||||||
|
onFocusRequested = { requestedInitialEpisodeFocus = true },
|
||||||
|
upFocusRequester = seasonTabFocusRequester,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -40,6 +41,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
@@ -91,7 +93,14 @@ internal fun TvSeasonTabs(
|
|||||||
TabRow(
|
TabRow(
|
||||||
selectedTabIndex = selectedSeasonIndex,
|
selectedTabIndex = selectedSeasonIndex,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.focusRestorer()
|
.then(
|
||||||
|
if (selectedItemFocusRequester != null) {
|
||||||
|
Modifier.focusRestorer(selectedItemFocusRequester)
|
||||||
|
} else {
|
||||||
|
Modifier.focusRestorer()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.focusGroup()
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentWidth(Alignment.Start),
|
.wrapContentWidth(Alignment.Start),
|
||||||
contentColor = MaterialTheme.colorScheme.onSurface
|
contentColor = MaterialTheme.colorScheme.onSurface
|
||||||
@@ -141,36 +150,60 @@ internal fun TvEpisodeCarousel(
|
|||||||
episodes: List<Episode>,
|
episodes: List<Episode>,
|
||||||
onPlayEpisode: (Episode) -> Unit,
|
onPlayEpisode: (Episode) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
focusedEpisodeId: UUID? = null
|
focusedEpisodeId: UUID? = null,
|
||||||
|
requestFocus: Boolean = false,
|
||||||
|
onFocusRequested: () -> Unit = {},
|
||||||
|
upFocusRequester: FocusRequester? = null
|
||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val focusedEpisodeFocusRequester = remember { FocusRequester() }
|
val rowScopedFocusRequester = remember { FocusRequester() }
|
||||||
|
var rowFocusedEpisodeId by remember { mutableStateOf<UUID?>(null) }
|
||||||
var requestedFocusEpisodeId by remember { mutableStateOf<UUID?>(null) }
|
var requestedFocusEpisodeId by remember { mutableStateOf<UUID?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(episodes, focusedEpisodeId) {
|
|
||||||
val focusedEpisodeIndex = focusedEpisodeId?.let { id ->
|
val focusedEpisodeIndex = focusedEpisodeId?.let { id ->
|
||||||
episodes.indexOfFirst { it.id == id }
|
episodes.indexOfFirst { it.id == id }
|
||||||
} ?: -1
|
} ?: -1
|
||||||
val firstUnwatchedIndex = episodes.indexOfFirst { !it.watched }.let { if (it == -1) 0 else it }
|
val firstUnwatchedIndex = episodes.indexOfFirst { !it.watched }
|
||||||
val targetIndex = focusedEpisodeIndex.takeIf { it >= 0 } ?: firstUnwatchedIndex
|
val targetEpisodeIndex = when {
|
||||||
|
focusedEpisodeIndex >= 0 -> focusedEpisodeIndex
|
||||||
|
firstUnwatchedIndex >= 0 -> firstUnwatchedIndex
|
||||||
|
episodes.isNotEmpty() -> 0
|
||||||
|
else -> -1
|
||||||
|
}
|
||||||
|
val targetEpisodeId = episodes.getOrNull(targetEpisodeIndex)?.id
|
||||||
|
|
||||||
if (targetIndex != 0) {
|
LaunchedEffect(episodes, targetEpisodeId) {
|
||||||
listState.scrollToItem(targetIndex)
|
if (episodes.none { it.id == rowFocusedEpisodeId }) {
|
||||||
|
rowFocusedEpisodeId = targetEpisodeId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetEpisodeIndex > 0) {
|
||||||
|
listState.scrollToItem(targetEpisodeIndex)
|
||||||
} else {
|
} else {
|
||||||
listState.scrollToItem(0)
|
listState.scrollToItem(0)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (focusedEpisodeIndex >= 0 && requestedFocusEpisodeId != focusedEpisodeId) {
|
LaunchedEffect(requestFocus, targetEpisodeId) {
|
||||||
|
if (requestFocus && targetEpisodeId != null && requestedFocusEpisodeId != targetEpisodeId) {
|
||||||
|
rowFocusedEpisodeId = targetEpisodeId
|
||||||
|
if (targetEpisodeIndex > 0) {
|
||||||
|
listState.scrollToItem(targetEpisodeIndex)
|
||||||
|
} else {
|
||||||
|
listState.scrollToItem(0)
|
||||||
|
}
|
||||||
withFrameNanos { }
|
withFrameNanos { }
|
||||||
focusedEpisodeFocusRequester.requestFocus()
|
rowScopedFocusRequester.requestFocus()
|
||||||
requestedFocusEpisodeId = focusedEpisodeId
|
requestedFocusEpisodeId = targetEpisodeId
|
||||||
|
onFocusRequested()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CompositionLocalProvider(LocalBringIntoViewSpec provides TvHomeRowBringIntoViewSpec) {
|
CompositionLocalProvider(LocalBringIntoViewSpec provides TvHomeRowBringIntoViewSpec) {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = modifier,
|
modifier = modifier
|
||||||
|
.focusRestorer(rowScopedFocusRequester)
|
||||||
|
.focusGroup(),
|
||||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
|
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
@@ -178,14 +211,28 @@ internal fun TvEpisodeCarousel(
|
|||||||
TvEpisodeCard(
|
TvEpisodeCard(
|
||||||
episode = episode,
|
episode = episode,
|
||||||
onPlayEpisode = { onPlayEpisode(episode) },
|
onPlayEpisode = { onPlayEpisode(episode) },
|
||||||
modifier = if (episode.id == focusedEpisodeId) {
|
onFocused = { rowFocusedEpisodeId = episode.id },
|
||||||
Modifier
|
modifier = Modifier
|
||||||
.focusRequester(focusedEpisodeFocusRequester)
|
.then(
|
||||||
.testTag(SeriesNextUpEpisodeCardTag)
|
if (episode.id == rowFocusedEpisodeId) {
|
||||||
|
Modifier.focusRequester(rowScopedFocusRequester)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.then(
|
||||||
|
upFocusRequester?.let { requester ->
|
||||||
|
Modifier.focusProperties { up = requester }
|
||||||
|
} ?: Modifier
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
if (episode.id == targetEpisodeId) {
|
||||||
|
Modifier.testTag(SeriesNextUpEpisodeCardTag)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,6 +286,7 @@ internal fun TvSeriesHeroSection(
|
|||||||
private fun TvEpisodeCard(
|
private fun TvEpisodeCard(
|
||||||
episode: Episode,
|
episode: Episode,
|
||||||
onPlayEpisode: () -> Unit,
|
onPlayEpisode: () -> Unit,
|
||||||
|
onFocused: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
@@ -250,7 +298,10 @@ private fun TvEpisodeCard(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.width(260.dp)
|
.width(260.dp)
|
||||||
.graphicsLayer { scaleX = scale; scaleY = scale }
|
.graphicsLayer { scaleX = scale; scaleY = scale }
|
||||||
.onFocusChanged { isFocused = it.isFocused }
|
.onFocusChanged {
|
||||||
|
isFocused = it.isFocused
|
||||||
|
if (it.isFocused) onFocused()
|
||||||
|
}
|
||||||
.clickable { onPlayEpisode() },
|
.clickable { onPlayEpisode() },
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user