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.Modifier
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.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
@@ -42,9 +39,6 @@ import org.jellyfin.sdk.model.api.BaseItemKind
fun PosterCard(
item: PosterItem,
modifier: Modifier = Modifier,
focusRequester: FocusRequester? = null,
upFocusRequester: FocusRequester? = null,
downFocusRequester: FocusRequester? = null,
onMovieSelected: (UUID) -> Unit,
onSeriesSelected: (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(
modifier = modifier
.width(posterWidth)
@@ -93,7 +74,7 @@ fun PosterCard(
PurefinAsyncImage(
model = item.imageUrl,
contentDescription = null,
modifier = imageFocusModifier
modifier = Modifier
.aspectRatio(2f / 3f)
.clip(RoundedCornerShape(14.dp))
.border(

View File

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

View File

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