From 76eebc1139370d409965409cd1b165afa1f2c640 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Sun, 19 Apr 2026 19:49:12 +0200 Subject: [PATCH] refactor: remove overcomplicated TvHomeHeroState --- .../purefin/ui/screen/home/TvHomeScreen.kt | 38 ++--- .../screen/home/components/TvHomeContent.kt | 160 ++++++------------ .../screen/home/components/TvHomeHeroState.kt | 111 ------------ .../purefin/core/ui/model/MediaUiModel.kt | 23 +++ data/jellyfin/build.gradle.kts | 1 + gradle/libs.versions.toml | 4 +- 6 files changed, 91 insertions(+), 246 deletions(-) delete mode 100644 app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeHeroState.kt diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/TvHomeScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/TvHomeScreen.kt index 2c99f84f..1d3e16d0 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/TvHomeScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/TvHomeScreen.kt @@ -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, @@ -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(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 - ) - } + TvFocusedItemHero( + 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, diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeContent.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeContent.kt index 8df0134b..43e01061 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeContent.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeContent.kt @@ -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,117 +37,67 @@ 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 + + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(scheme.background) + .testTag(TvHomeContentViewportTag), + contentPadding = contentPadding + ) { + item(key = "tv-home-top-spacer") { + Spacer(modifier = Modifier.height(8.dp)) } - } - - CompositionLocalProvider(LocalBringIntoViewSpec provides columnBringIntoViewSpec) { - LazyColumn( - modifier = modifier - .fillMaxSize() - .background(scheme.background) - .testTag(TvHomeContentViewportTag), - contentPadding = contentPadding - ) { - item(key = "tv-home-top-spacer") { - Spacer(modifier = Modifier.height(8.dp)) - } - if (hasContinueWatching) { - item(key = "tv-home-continue-watching") { - TvContinueWatchingSection( - items = continueWatching, - onFocusedItem = onMediaFocused, - onMovieSelected = onMovieSelected, - onEpisodeSelected = onEpisodeSelected, - firstItemFocusRequester = initialFocusRequester, - firstItemTestTag = TvHomeInitialFocusTag, - rowTestTag = TvHomeContinueWatchingRowTag - ) - } - } - - 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( - items = nextUp, - onFocusedItem = onMediaFocused, - onEpisodeSelected = onEpisodeSelected, - firstItemFocusRequester = initialFocusRequester.takeIf { !hasContinueWatching }, - firstItemTestTag = TvHomeInitialFocusTag.takeIf { !hasContinueWatching }, - rowTestTag = TvHomeNextUpRowTag - ) - } - } - - if (hasLibraries && (hasContinueWatching || hasNextUp)) { - item(key = "tv-home-post-next-up-gap") { - Spacer(modifier = Modifier.height(20.dp)) - } - } - - itemsIndexed( - items = visibleLibraries, - key = { _, library -> library.id } - ) { index, library -> - TvLibraryPosterSection( - title = library.name, - items = libraryContent[library.id].orEmpty(), + if (hasContinueWatching) { + item(key = "tv-home-continue-watching") { + TvContinueWatchingSection( + items = continueWatching, onFocusedItem = onMediaFocused, - firstItemFocusRequester = initialFocusRequester.takeIf { - !hasContinueWatching && - !hasNextUp && - library.id == firstVisibleLibraryId - }, - firstItemTestTag = TvHomeInitialFocusTag.takeIf { - !hasContinueWatching && - !hasNextUp && - library.id == firstVisibleLibraryId - }, - rowTestTag = tvHomeLibraryRowTag(library.id), onMovieSelected = onMovieSelected, - onSeriesSelected = onSeriesSelected, - onEpisodeSelected = onEpisodeSelected + onEpisodeSelected = onEpisodeSelected, + firstItemFocusRequester = initialFocusRequester, + firstItemTestTag = TvHomeInitialFocusTag, + rowTestTag = TvHomeContinueWatchingRowTag ) - - if (index < visibleLibraries.lastIndex) { - Spacer(modifier = Modifier.height(20.dp)) - } } + } - if (hasVisibleContent) { - item(key = "tv-home-trailing-space") { - Spacer(modifier = Modifier.height(TvHomeBringIntoViewTrailingSpace)) - } + if (hasNextUp) { + item(key = "tv-home-next-up") { + TvNextUpSection( + items = nextUp, + onFocusedItem = onMediaFocused, + onEpisodeSelected = onEpisodeSelected, + firstItemFocusRequester = initialFocusRequester.takeIf { !hasContinueWatching }, + firstItemTestTag = TvHomeInitialFocusTag.takeIf { !hasContinueWatching }, + rowTestTag = TvHomeNextUpRowTag + ) + } + } + + itemsIndexed( + items = libraries, + key = { _, library -> library.id } + ) { index, library -> + TvLibraryPosterSection( + title = library.name, + items = libraryContent[library.id].orEmpty(), + onFocusedItem = onMediaFocused, + firstItemFocusRequester = initialFocusRequester, + onMovieSelected = onMovieSelected, + onSeriesSelected = onSeriesSelected, + onEpisodeSelected = onEpisodeSelected + ) + } + + if (hasVisibleContent) { + item(key = "tv-home-trailing-space") { + Spacer(modifier = Modifier.height(TvHomeBringIntoViewTrailingSpace)) } } } diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeHeroState.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeHeroState.kt deleted file mode 100644 index 1c70d8f5..00000000 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/home/components/TvHomeHeroState.kt +++ /dev/null @@ -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, - private val libraryContent: Map>, - private val continueWatching: List, - private val nextUp: List, - 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, - libraryContent: Map>, - continueWatching: List, - nextUp: List, -): TvHomeItemRegistry { - return remember(libraries, libraryContent, continueWatching, nextUp) { - createTvHomeItemRegistry( - libraries = libraries, - libraryContent = libraryContent, - continueWatching = continueWatching, - nextUp = nextUp - ) - } -} - -internal fun createTvHomeItemRegistry( - libraries: List, - libraryContent: Map>, - continueWatching: List, - nextUp: List, -): 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, - libraryContent: Map>, - continueWatching: List, - nextUp: List, -): TvHomeHeroState { - val itemRegistry = rememberTvHomeItemRegistry( - libraries = libraries, - libraryContent = libraryContent, - continueWatching = continueWatching, - nextUp = nextUp - ) - var focusedItemId by remember { mutableStateOf(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 - ) - } -} diff --git a/core/model/src/main/java/hu/bbara/purefin/core/ui/model/MediaUiModel.kt b/core/model/src/main/java/hu/bbara/purefin/core/ui/model/MediaUiModel.kt index 8e38023c..b0f4ff77 100644 --- a/core/model/src/main/java/hu/bbara/purefin/core/ui/model/MediaUiModel.kt +++ b/core/model/src/main/java/hu/bbara/purefin/core/ui/model/MediaUiModel.kt @@ -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 { diff --git a/data/jellyfin/build.gradle.kts b/data/jellyfin/build.gradle.kts index 4819e491..45caefaf 100644 --- a/data/jellyfin/build.gradle.kts +++ b/data/jellyfin/build.gradle.kts @@ -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) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6e4ae6cc..8ea8f1d1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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]