refactor: remove overcomplicated TvHomeHeroState

This commit is contained in:
2026-04-19 19:49:12 +02:00
parent 632dda52d6
commit 76eebc1139
6 changed files with 91 additions and 246 deletions

View File

@@ -1,25 +1,23 @@
package hu.bbara.purefin.ui.screen.home
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.ui.model.MediaUiModel
import hu.bbara.purefin.core.ui.model.MovieUiModel
import hu.bbara.purefin.feature.browse.home.LibraryItem
import hu.bbara.purefin.ui.screen.home.components.TvFocusedItemHero
import hu.bbara.purefin.ui.screen.home.components.TvHomeContent
import hu.bbara.purefin.ui.screen.home.components.rememberTvHomeHeroState
import java.util.UUID
private const val TvHomeHeroHeightFraction = 0.32f
private val TvHomeMinHeroHeight = 160.dp
private val TvHomeMaxHeroHeight = 200.dp
@Composable
fun TvHomeScreen(
libraries: List<LibraryItem>,
@@ -32,39 +30,29 @@ fun TvHomeScreen(
modifier: Modifier = Modifier,
) {
val scheme = MaterialTheme.colorScheme
val heroState = rememberTvHomeHeroState(
libraries = libraries,
libraryContent = libraryContent,
continueWatching = continueWatching,
nextUp = nextUp
)
val focusedMediaUiModel = remember { mutableStateOf<MediaUiModel>(MovieUiModel.createPlaceholder()) }
BoxWithConstraints(
Surface(
modifier = modifier
.fillMaxSize()
.background(scheme.background)
) {
val heroHeight = heroState.focusedHero?.let {
(maxHeight * TvHomeHeroHeightFraction)
.coerceIn(TvHomeMinHeroHeight, TvHomeMaxHeroHeight)
}
Column(
modifier = Modifier.fillMaxSize()
) {
heroState.focusedHero?.let { hero ->
TvFocusedItemHero(
item = hero,
height = heroHeight ?: TvHomeMinHeroHeight
item = focusedMediaUiModel.value,
height = 250.dp
)
}
TvHomeContent(
libraries = libraries,
libraryContent = libraryContent,
continueWatching = continueWatching,
nextUp = nextUp,
//TODO implement it by throwing out complex TvHomeHeroState shit ass code. Fuck ai btw
onMediaFocused = {},
onMediaFocused = {
focusedMediaUiModel.value = it
},
onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected,

View File

@@ -3,7 +3,6 @@
package hu.bbara.purefin.ui.screen.home.components
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -12,16 +11,9 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.ui.model.MediaUiModel
@@ -45,35 +37,12 @@ fun TvHomeContent(
modifier: Modifier = Modifier,
) {
val scheme = MaterialTheme.colorScheme
val itemRegistry = rememberTvHomeItemRegistry(
libraries = libraries,
libraryContent = libraryContent,
continueWatching = continueWatching,
nextUp = nextUp
)
val visibleLibraries = itemRegistry.visibleLibraries
val hasContinueWatching = continueWatching.isNotEmpty()
val hasNextUp = nextUp.isNotEmpty()
val hasLibraries = visibleLibraries.isNotEmpty()
val hasVisibleContent = hasContinueWatching || hasNextUp || hasLibraries
val firstVisibleLibraryId = visibleLibraries.firstOrNull()?.id
val hasVisibleContent = hasContinueWatching || hasNextUp
val initialFocusRequester = remember { FocusRequester() }
val firstAvailableItemKey = itemRegistry.firstAvailableItemId
var initialFocusApplied by remember { mutableStateOf(false) }
val topOffsetPx = with(LocalDensity.current) { TvHomeFocusedItemTopOffset.toPx() }
val columnBringIntoViewSpec = remember(topOffsetPx) {
tvHomeColumnBringIntoViewSpec(topOffsetPx = topOffsetPx)
}
LaunchedEffect(firstAvailableItemKey, initialFocusApplied) {
if (!initialFocusApplied && firstAvailableItemKey != null) {
withFrameNanos { }
initialFocusRequester.requestFocus()
initialFocusApplied = true
}
}
CompositionLocalProvider(LocalBringIntoViewSpec provides columnBringIntoViewSpec) {
LazyColumn(
modifier = modifier
.fillMaxSize()
@@ -98,12 +67,6 @@ fun TvHomeContent(
}
}
if (hasContinueWatching && (hasNextUp || hasLibraries)) {
item(key = "tv-home-post-continue-gap") {
Spacer(modifier = Modifier.height(20.dp))
}
}
if (hasNextUp) {
item(key = "tv-home-next-up") {
TvNextUpSection(
@@ -117,39 +80,19 @@ fun TvHomeContent(
}
}
if (hasLibraries && (hasContinueWatching || hasNextUp)) {
item(key = "tv-home-post-next-up-gap") {
Spacer(modifier = Modifier.height(20.dp))
}
}
itemsIndexed(
items = visibleLibraries,
items = libraries,
key = { _, library -> library.id }
) { index, library ->
TvLibraryPosterSection(
title = library.name,
items = libraryContent[library.id].orEmpty(),
onFocusedItem = onMediaFocused,
firstItemFocusRequester = initialFocusRequester.takeIf {
!hasContinueWatching &&
!hasNextUp &&
library.id == firstVisibleLibraryId
},
firstItemTestTag = TvHomeInitialFocusTag.takeIf {
!hasContinueWatching &&
!hasNextUp &&
library.id == firstVisibleLibraryId
},
rowTestTag = tvHomeLibraryRowTag(library.id),
firstItemFocusRequester = initialFocusRequester,
onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected
)
if (index < visibleLibraries.lastIndex) {
Spacer(modifier = Modifier.height(20.dp))
}
}
if (hasVisibleContent) {
@@ -158,5 +101,4 @@ fun TvHomeContent(
}
}
}
}
}

View File

@@ -1,111 +0,0 @@
package hu.bbara.purefin.ui.screen.home.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import hu.bbara.purefin.core.ui.model.MediaUiModel
import hu.bbara.purefin.feature.browse.home.FocusableItem
import hu.bbara.purefin.feature.browse.home.LibraryItem
import java.util.UUID
//TODO throw this out and simplify it.
internal data class TvHomeItemRegistry(
val visibleLibraries: List<LibraryItem>,
private val libraryContent: Map<UUID, List<MediaUiModel>>,
private val continueWatching: List<MediaUiModel>,
private val nextUp: List<MediaUiModel>,
val firstAvailableItemId: UUID?,
) {
val firstAvailableItem: MediaUiModel?
get() = firstAvailableItemId?.let(::itemById)
fun itemById(id: UUID): MediaUiModel? {
return continueWatching.firstOrNull { it.id == id }
?: nextUp.firstOrNull { it.id == id }
?: visibleLibraries.asSequence()
.mapNotNull { library -> libraryContent[library.id] }
.flatten()
.firstOrNull { it.id == id }
}
}
internal class TvHomeHeroState(
val focusedHero: MediaUiModel?,
val onMediaFocused: (FocusableItem) -> Unit,
)
@Composable
internal fun rememberTvHomeItemRegistry(
libraries: List<LibraryItem>,
libraryContent: Map<UUID, List<MediaUiModel>>,
continueWatching: List<MediaUiModel>,
nextUp: List<MediaUiModel>,
): TvHomeItemRegistry {
return remember(libraries, libraryContent, continueWatching, nextUp) {
createTvHomeItemRegistry(
libraries = libraries,
libraryContent = libraryContent,
continueWatching = continueWatching,
nextUp = nextUp
)
}
}
internal fun createTvHomeItemRegistry(
libraries: List<LibraryItem>,
libraryContent: Map<UUID, List<MediaUiModel>>,
continueWatching: List<MediaUiModel>,
nextUp: List<MediaUiModel>,
): TvHomeItemRegistry {
val visibleLibraries = libraries.filter { libraryContent[it.id]?.isEmpty() != true }
val firstAvailableItemId = continueWatching.firstOrNull()?.id
?: nextUp.firstOrNull()?.id
?: visibleLibraries.firstOrNull()?.id?.let { libraryId ->
libraryContent[libraryId]?.firstOrNull()?.id
}
return TvHomeItemRegistry(
visibleLibraries = visibleLibraries,
libraryContent = libraryContent,
continueWatching = continueWatching,
nextUp = nextUp,
firstAvailableItemId = firstAvailableItemId
)
}
@Composable
internal fun rememberTvHomeHeroState(
libraries: List<LibraryItem>,
libraryContent: Map<UUID, List<MediaUiModel>>,
continueWatching: List<MediaUiModel>,
nextUp: List<MediaUiModel>,
): TvHomeHeroState {
val itemRegistry = rememberTvHomeItemRegistry(
libraries = libraries,
libraryContent = libraryContent,
continueWatching = continueWatching,
nextUp = nextUp
)
var focusedItemId by remember { mutableStateOf<UUID?>(null) }
val focusedItem = remember(focusedItemId, itemRegistry) {
focusedItemId?.let(itemRegistry::itemById)
}
val focusedHero = remember(focusedItem, itemRegistry) {
(focusedItem ?: itemRegistry.firstAvailableItem)
}
val onMediaFocused: (FocusableItem) -> Unit = remember {
{ item ->
focusedItemId = item.id
}
}
return remember(focusedHero, onMediaFocused) {
TvHomeHeroState(
focusedHero = focusedHero,
onMediaFocused = onMediaFocused
)
}
}

View File

@@ -38,6 +38,29 @@ class MovieUiModel: MediaUiModel {
)
progress = (movie.progress?.toFloat() ?: 0f) / 100f
}
companion object {
fun createPlaceholder() : MovieUiModel {
return MovieUiModel(
Movie(
id = UUID.randomUUID(),
libraryId = UUID.randomUUID(),
title = "Loading...",
progress = 0.0,
watched = false,
year = "",
rating = "",
runtime = "",
format = "",
synopsis = "",
imageUrlPrefix = "",
audioTrack = "",
subtitles = "",
cast = emptyList()
)
)
}
}
}
class SeriesUiModel : MediaUiModel {

View File

@@ -36,6 +36,7 @@ dependencies {
implementation(libs.jellyfin.core)
implementation(libs.media3.common)
implementation(libs.kotlinx.serialization.json)
implementation(libs.slf4j.api)
implementation(libs.hilt)
ksp(libs.hilt.compiler)
implementation(libs.datastore)

View File

@@ -8,7 +8,7 @@ lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.9.3"
kotlin = "2.2.10"
composeBom = "2025.02.00"
jellyfin-core = "1.8.5"
jellyfin-core = "1.8.8"
hilt = "2.57.2"
hiltNavigationCompose = "1.2.0"
ksp = "2.3.2"
@@ -23,6 +23,7 @@ media3 = "1.9.0"
media3FfmpegDecoder = "1.9.0+1"
nav3Core = "1.0.0"
room = "2.7.0"
slf4j = "2.0.17"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -67,6 +68,7 @@ androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runt
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
[plugins]