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

View File

@@ -3,7 +3,6 @@
package hu.bbara.purefin.ui.screen.home.components package hu.bbara.purefin.ui.screen.home.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize 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.foundation.lazy.itemsIndexed
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable 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.remember
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.platform.LocalDensity
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import hu.bbara.purefin.core.ui.model.MediaUiModel import hu.bbara.purefin.core.ui.model.MediaUiModel
@@ -45,35 +37,12 @@ fun TvHomeContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
val itemRegistry = rememberTvHomeItemRegistry(
libraries = libraries,
libraryContent = libraryContent,
continueWatching = continueWatching,
nextUp = nextUp
)
val visibleLibraries = itemRegistry.visibleLibraries
val hasContinueWatching = continueWatching.isNotEmpty() val hasContinueWatching = continueWatching.isNotEmpty()
val hasNextUp = nextUp.isNotEmpty() val hasNextUp = nextUp.isNotEmpty()
val hasLibraries = visibleLibraries.isNotEmpty() val hasVisibleContent = hasContinueWatching || hasNextUp
val hasVisibleContent = hasContinueWatching || hasNextUp || hasLibraries
val firstVisibleLibraryId = visibleLibraries.firstOrNull()?.id
val initialFocusRequester = remember { FocusRequester() } 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( LazyColumn(
modifier = modifier modifier = modifier
.fillMaxSize() .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) { if (hasNextUp) {
item(key = "tv-home-next-up") { item(key = "tv-home-next-up") {
TvNextUpSection( 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( itemsIndexed(
items = visibleLibraries, items = libraries,
key = { _, library -> library.id } key = { _, library -> library.id }
) { index, library -> ) { index, library ->
TvLibraryPosterSection( TvLibraryPosterSection(
title = library.name, title = library.name,
items = libraryContent[library.id].orEmpty(), items = libraryContent[library.id].orEmpty(),
onFocusedItem = onMediaFocused, onFocusedItem = onMediaFocused,
firstItemFocusRequester = initialFocusRequester.takeIf { firstItemFocusRequester = initialFocusRequester,
!hasContinueWatching &&
!hasNextUp &&
library.id == firstVisibleLibraryId
},
firstItemTestTag = TvHomeInitialFocusTag.takeIf {
!hasContinueWatching &&
!hasNextUp &&
library.id == firstVisibleLibraryId
},
rowTestTag = tvHomeLibraryRowTag(library.id),
onMovieSelected = onMovieSelected, onMovieSelected = onMovieSelected,
onSeriesSelected = onSeriesSelected, onSeriesSelected = onSeriesSelected,
onEpisodeSelected = onEpisodeSelected onEpisodeSelected = onEpisodeSelected
) )
if (index < visibleLibraries.lastIndex) {
Spacer(modifier = Modifier.height(20.dp))
}
} }
if (hasVisibleContent) { 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 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 { class SeriesUiModel : MediaUiModel {

View File

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

View File

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