mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
refactor: remove overcomplicated TvHomeHeroState
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user