refactor: simplify navigation

This commit is contained in:
2026-04-01 21:48:58 +02:00
parent cdb9dc3403
commit b3d3f60d78
3 changed files with 34 additions and 174 deletions

View File

@@ -20,9 +20,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment 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.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
@@ -42,9 +39,6 @@ import org.jellyfin.sdk.model.api.BaseItemKind
fun PosterCard( fun PosterCard(
item: PosterItem, item: PosterItem,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
focusRequester: FocusRequester? = null,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
onMovieSelected: (UUID) -> Unit, onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit, onSeriesSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
@@ -67,19 +61,6 @@ fun PosterCard(
} }
} }
val imageFocusModifier = Modifier
.then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier)
.then(
if (upFocusRequester != null || downFocusRequester != null) {
Modifier.focusProperties {
upFocusRequester?.let { up = it }
downFocusRequester?.let { down = it }
}
} else {
Modifier
}
)
Column( Column(
modifier = modifier modifier = modifier
.width(posterWidth) .width(posterWidth)
@@ -93,7 +74,7 @@ fun PosterCard(
PurefinAsyncImage( PurefinAsyncImage(
model = item.imageUrl, model = item.imageUrl,
contentDescription = null, contentDescription = null,
modifier = imageFocusModifier modifier = Modifier
.aspectRatio(2f / 3f) .aspectRatio(2f / 3f)
.clip(RoundedCornerShape(14.dp)) .clip(RoundedCornerShape(14.dp))
.border( .border(

View File

@@ -1,5 +1,6 @@
package hu.bbara.purefin.tv.home.ui package hu.bbara.purefin.tv.home.ui
import android.annotation.SuppressLint
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -8,10 +9,13 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
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.focus.FocusRequester.Companion.FocusRequesterFactory.component1
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem import hu.bbara.purefin.feature.shared.home.ContinueWatchingItem
import hu.bbara.purefin.feature.shared.home.LibraryItem import hu.bbara.purefin.feature.shared.home.LibraryItem
@@ -19,6 +23,7 @@ import hu.bbara.purefin.feature.shared.home.NextUpItem
import hu.bbara.purefin.feature.shared.home.PosterItem import hu.bbara.purefin.feature.shared.home.PosterItem
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
@SuppressLint("RememberInComposition")
@Composable @Composable
fun TvHomeContent( fun TvHomeContent(
libraries: List<LibraryItem>, libraries: List<LibraryItem>,
@@ -33,36 +38,12 @@ fun TvHomeContent(
val visibleLibraries = remember(libraries, libraryContent) { val visibleLibraries = remember(libraries, libraryContent) {
libraries.filter { libraryContent[it.id]?.isEmpty() != true } libraries.filter { libraryContent[it.id]?.isEmpty() != true }
} }
val continueWatchingSectionFocusRequester = remember { FocusRequester() }
val continueWatchingFirstItemFocusRequester = remember { FocusRequester() } val ( nextUpRef, continueWatchingRef ) = remember { FocusRequester.createRefs() }
val nextUpSectionFocusRequester = remember { FocusRequester() } val libraryRefs = remember(libraryContent) {
val nextUpFirstItemFocusRequester = remember { FocusRequester() } libraryContent.keys.associateWith { FocusRequester() }
val librarySectionFocusRequesters = remember(visibleLibraries.map { it.id }) {
visibleLibraries.associate { library -> library.id to FocusRequester() }
}
val libraryFirstItemFocusRequesters = remember(visibleLibraries.map { it.id }) {
visibleLibraries.associate { library -> library.id to FocusRequester() }
}
val firstSectionFocusRequester = when {
continueWatching.isNotEmpty() -> continueWatchingSectionFocusRequester
nextUp.isNotEmpty() -> nextUpSectionFocusRequester
else -> visibleLibraries.firstOrNull()?.let { librarySectionFocusRequesters[it.id] }
}
val firstVisibleLibrarySectionFocusRequester = visibleLibraries.firstOrNull()
?.let { librarySectionFocusRequesters[it.id] }
val firstHomeRowBelowContinueWatching = when {
nextUp.isNotEmpty() -> nextUpSectionFocusRequester
else -> firstVisibleLibrarySectionFocusRequester
}
val firstHomeRowAboveLibraries = when {
nextUp.isNotEmpty() -> nextUpSectionFocusRequester
continueWatching.isNotEmpty() -> continueWatchingSectionFocusRequester
else -> null
} }
LaunchedEffect(firstSectionFocusRequester) {
firstSectionFocusRequester?.requestFocus()
}
LazyColumn( LazyColumn(
modifier = modifier modifier = modifier
@@ -75,11 +56,12 @@ fun TvHomeContent(
item { item {
TvContinueWatchingSection( TvContinueWatchingSection(
items = continueWatching, items = continueWatching,
sectionFocusRequester = continueWatchingSectionFocusRequester,
firstItemFocusRequester = continueWatchingFirstItemFocusRequester,
downFocusRequester = firstHomeRowBelowContinueWatching,
onMovieSelected = onMovieSelected, onMovieSelected = onMovieSelected,
onEpisodeSelected = onEpisodeSelected onEpisodeSelected = onEpisodeSelected,
modifier = Modifier.focusRequester(continueWatchingRef)
.focusProperties{
down = nextUpRef
}
) )
} }
item { item {
@@ -88,12 +70,12 @@ fun TvHomeContent(
item { item {
TvNextUpSection( TvNextUpSection(
items = nextUp, items = nextUp,
sectionFocusRequester = nextUpSectionFocusRequester, onEpisodeSelected = onEpisodeSelected,
firstItemFocusRequester = nextUpFirstItemFocusRequester, modifier = Modifier.focusRequester(nextUpRef)
upFocusRequester = continueWatching.takeIf { it.isNotEmpty() } .focusProperties{
?.let { continueWatchingSectionFocusRequester }, up = continueWatchingRef
downFocusRequester = firstVisibleLibrarySectionFocusRequester, libraryRefs.values.firstOrNull()?.let { down = it }
onEpisodeSelected = onEpisodeSelected }
) )
} }
item { item {
@@ -103,29 +85,19 @@ fun TvHomeContent(
items = visibleLibraries, items = visibleLibraries,
key = { it.id } key = { it.id }
) { item -> ) { item ->
val libraryIndex = visibleLibraries.indexOfFirst { it.id == item.id } val ref = libraryRefs[item.id]
val previousLibrarySectionFocusRequester = visibleLibraries
.getOrNull(libraryIndex - 1)
?.let { librarySectionFocusRequesters[it.id] }
val nextLibrarySectionFocusRequester = visibleLibraries
.getOrNull(libraryIndex + 1)
?.let { librarySectionFocusRequesters[it.id] }
TvLibraryPosterSection( TvLibraryPosterSection(
title = item.name, title = item.name,
items = libraryContent[item.id] ?: emptyList(), items = libraryContent[item.id] ?: emptyList(),
action = "See All", action = "See All",
sectionFocusRequester = librarySectionFocusRequesters.getValue(item.id),
firstItemFocusRequester = libraryFirstItemFocusRequesters.getValue(item.id),
upFocusRequester = if (libraryIndex == 0) {
firstHomeRowAboveLibraries
} else {
previousLibrarySectionFocusRequester
},
downFocusRequester = nextLibrarySectionFocusRequester,
onMovieSelected = onMovieSelected, onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected, onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected onEpisodeSelected = onEpisodeSelected,
modifier = if (ref != null) Modifier.focusRequester(ref) else Modifier
.focusProperties {
up = nextUpRef
libraryRefs.values.dropWhile { it != ref }.drop(1).firstOrNull()?.let { down = it }
}
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }

View File

@@ -28,16 +28,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment 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.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -55,10 +49,6 @@ import kotlin.math.nextUp
@Composable @Composable
fun TvContinueWatchingSection( fun TvContinueWatchingSection(
items: List<ContinueWatchingItem>, items: List<ContinueWatchingItem>,
sectionFocusRequester: FocusRequester,
firstItemFocusRequester: FocusRequester? = null,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
onMovieSelected: (UUID) -> Unit, onMovieSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@@ -70,20 +60,15 @@ fun TvContinueWatchingSection(
) )
LazyRow( LazyRow(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth(),
.focusRequester(sectionFocusRequester)
.focusRestorer(firstItemFocusRequester ?: FocusRequester.Default),
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
itemsIndexed(items = items) { index, item -> itemsIndexed(items = items) { index, item ->
TvContinueWatchingCard( TvContinueWatchingCard(
item = item, item = item,
focusRequester = if (index == 0) firstItemFocusRequester else null,
isFirstItem = index == 0, isFirstItem = index == 0,
isLastItem = index == items.lastIndex, isLastItem = index == items.lastIndex,
upFocusRequester = upFocusRequester,
downFocusRequester = downFocusRequester,
onMovieSelected = onMovieSelected, onMovieSelected = onMovieSelected,
onEpisodeSelected = onEpisodeSelected onEpisodeSelected = onEpisodeSelected
) )
@@ -95,18 +80,13 @@ fun TvContinueWatchingSection(
fun TvContinueWatchingCard( fun TvContinueWatchingCard(
item: ContinueWatchingItem, item: ContinueWatchingItem,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
focusRequester: FocusRequester? = null,
isFirstItem: Boolean = false, isFirstItem: Boolean = false,
isLastItem: Boolean = false, isLastItem: Boolean = false,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
onMovieSelected: (UUID) -> Unit, onMovieSelected: (UUID) -> Unit,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
) { ) {
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
val context = LocalContext.current
val density = LocalDensity.current
var isFocused by remember { mutableStateOf(false) } var isFocused by remember { mutableStateOf(false) }
val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale") val scale by animateFloatAsState(targetValue = if (isFocused) 1.07f else 1.0f, label = "scale")
@@ -130,32 +110,6 @@ fun TvContinueWatchingCard(
} }
} }
val imageFocusModifier = Modifier
.then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier)
.then(
if (upFocusRequester != null || downFocusRequester != null) {
Modifier.focusProperties {
upFocusRequester?.let { up = it }
downFocusRequester?.let { down = it }
if (isFirstItem) {
left = FocusRequester.Cancel
}
if (isLastItem) {
right = FocusRequester.Cancel
}
}
} else {
Modifier.focusProperties {
if (isFirstItem) {
left = FocusRequester.Cancel
}
if (isLastItem) {
right = FocusRequester.Cancel
}
}
}
)
Column( Column(
modifier = modifier modifier = modifier
.width(cardWidth) .width(cardWidth)
@@ -181,7 +135,7 @@ fun TvContinueWatchingCard(
PurefinAsyncImage( PurefinAsyncImage(
model = imageUrl, model = imageUrl,
contentDescription = null, contentDescription = null,
modifier = imageFocusModifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.onFocusChanged { isFocused = it.isFocused } .onFocusChanged { isFocused = it.isFocused }
.clickable { .clickable {
@@ -220,10 +174,6 @@ fun TvContinueWatchingCard(
@Composable @Composable
fun TvNextUpSection( fun TvNextUpSection(
items: List<NextUpItem>, items: List<NextUpItem>,
sectionFocusRequester: FocusRequester,
firstItemFocusRequester: FocusRequester? = null,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@@ -234,9 +184,7 @@ fun TvNextUpSection(
) )
LazyRow( LazyRow(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth(),
.focusRequester(sectionFocusRequester)
.focusRestorer(firstItemFocusRequester ?: FocusRequester.Default),
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
@@ -246,11 +194,8 @@ fun TvNextUpSection(
) { index, item -> ) { index, item ->
TvNextUpCard( TvNextUpCard(
item = item, item = item,
focusRequester = if (index == 0) firstItemFocusRequester else null,
isFirstItem = index == 0, isFirstItem = index == 0,
isLastItem = index == items.lastIndex, isLastItem = index == items.lastIndex,
upFocusRequester = upFocusRequester,
downFocusRequester = downFocusRequester,
onEpisodeSelected = onEpisodeSelected onEpisodeSelected = onEpisodeSelected
) )
} }
@@ -261,11 +206,8 @@ fun TvNextUpSection(
fun TvNextUpCard( fun TvNextUpCard(
item: NextUpItem, item: NextUpItem,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
focusRequester: FocusRequester? = null,
isFirstItem: Boolean = false, isFirstItem: Boolean = false,
isLastItem: Boolean = false, isLastItem: Boolean = false,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
onEpisodeSelected: (UUID, UUID, UUID) -> Unit, onEpisodeSelected: (UUID, UUID, UUID) -> Unit,
) { ) {
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
@@ -282,32 +224,6 @@ fun TvNextUpCard(
onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id) onEpisodeSelected(episode.seriesId, episode.seasonId, episode.id)
} }
val imageFocusModifier = Modifier
.then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier)
.then(
if (upFocusRequester != null || downFocusRequester != null) {
Modifier.focusProperties {
upFocusRequester?.let { up = it }
downFocusRequester?.let { down = it }
if (isFirstItem) {
left = FocusRequester.Cancel
}
if (isLastItem) {
right = FocusRequester.Cancel
}
}
} else {
Modifier.focusProperties {
if (isFirstItem) {
left = FocusRequester.Cancel
}
if (isLastItem) {
right = FocusRequester.Cancel
}
}
}
)
Column( Column(
modifier = modifier modifier = modifier
.width(cardWidth) .width(cardWidth)
@@ -333,7 +249,7 @@ fun TvNextUpCard(
PurefinAsyncImage( PurefinAsyncImage(
model = imageUrl, model = imageUrl,
contentDescription = null, contentDescription = null,
modifier = imageFocusModifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.onFocusChanged { isFocused = it.isFocused } .onFocusChanged { isFocused = it.isFocused }
.clickable { .clickable {
@@ -367,10 +283,6 @@ fun TvLibraryPosterSection(
title: String, title: String,
items: List<PosterItem>, items: List<PosterItem>,
action: String?, action: String?,
sectionFocusRequester: FocusRequester,
firstItemFocusRequester: FocusRequester? = null,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onMovieSelected: (UUID) -> Unit, onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (UUID) -> Unit, onSeriesSelected: (UUID) -> Unit,
@@ -382,9 +294,7 @@ fun TvLibraryPosterSection(
) )
LazyRow( LazyRow(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth(),
.focusRequester(sectionFocusRequester)
.focusRestorer(firstItemFocusRequester ?: FocusRequester.Default),
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
@@ -394,9 +304,6 @@ fun TvLibraryPosterSection(
) { index, item -> ) { index, item ->
PosterCard( PosterCard(
item = item, item = item,
focusRequester = if (index == 0) firstItemFocusRequester else null,
upFocusRequester = upFocusRequester,
downFocusRequester = downFocusRequester,
onMovieSelected = onMovieSelected, onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected, onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected onEpisodeSelected = onEpisodeSelected