From 1ce380f3b59533b3d3c7864799fb0cb1bed53b46 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Fri, 23 Jan 2026 12:44:20 -0500 Subject: [PATCH] Apply view options to preview --- .../wholphin/data/model/HomeRowConfig.kt | 123 +++++++++++++++++- .../wholphin/ui/cards/BannerCard.kt | 8 +- .../damontecres/wholphin/ui/main/HomePage.kt | 10 +- .../ui/main/settings/HomePageRowList.kt | 26 ++-- .../ui/main/settings/HomePageRowSettings.kt | 56 ++++++++ .../ui/main/settings/HomePageSettings.kt | 71 +++++++++- .../settings/HomePageSettingsDestination.kt | 4 +- .../damontecres/wholphin/util/LoadingState.kt | 2 + app/src/main/res/values/strings.xml | 1 + 9 files changed, 275 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageRowSettings.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/HomeRowConfig.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/HomeRowConfig.kt index 00dfd1aa..e15df65e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/HomeRowConfig.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/HomeRowConfig.kt @@ -2,10 +2,16 @@ package com.github.damontecres.wholphin.data.model +import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.preferences.AppChoicePreference +import com.github.damontecres.wholphin.preferences.AppClickablePreference +import com.github.damontecres.wholphin.preferences.AppSliderPreference +import com.github.damontecres.wholphin.preferences.AppSwitchPreference import com.github.damontecres.wholphin.preferences.PrefContentScale import com.github.damontecres.wholphin.ui.AspectRatio import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.components.ViewOptionImageType +import com.github.damontecres.wholphin.ui.components.ViewOptions import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import org.jellyfin.sdk.model.serializer.UUIDSerializer @@ -16,33 +22,43 @@ sealed class HomeRowConfig { abstract val id: UUID abstract val viewOptions: HomeRowViewOptions + abstract fun updateViewOptions(viewOptions: HomeRowViewOptions): HomeRowConfig + @Serializable data class ContinueWatching( override val id: UUID, val combined: Boolean, override val viewOptions: HomeRowViewOptions, - ) : HomeRowConfig() + ) : HomeRowConfig() { + override fun updateViewOptions(viewOptions: HomeRowViewOptions): HomeRowConfig = this.copy(viewOptions = viewOptions) + } @Serializable data class RecentlyAdded( override val id: UUID, val parentId: UUID, override val viewOptions: HomeRowViewOptions, - ) : HomeRowConfig() + ) : HomeRowConfig() { + override fun updateViewOptions(viewOptions: HomeRowViewOptions): HomeRowConfig = this.copy(viewOptions = viewOptions) + } @Serializable data class RecentlyReleased( override val id: UUID, val parentId: UUID, override val viewOptions: HomeRowViewOptions, - ) : HomeRowConfig() + ) : HomeRowConfig() { + override fun updateViewOptions(viewOptions: HomeRowViewOptions): HomeRowConfig = this.copy(viewOptions = viewOptions) + } @Serializable data class Genres( override val id: UUID, val parentId: UUID, override val viewOptions: HomeRowViewOptions, - ) : HomeRowConfig() + ) : HomeRowConfig() { + override fun updateViewOptions(viewOptions: HomeRowViewOptions): HomeRowConfig = this.copy(viewOptions = viewOptions) + } } data class HomeRowConfigDisplay( @@ -59,4 +75,101 @@ data class HomeRowViewOptions( val imageType: ViewOptionImageType = ViewOptionImageType.PRIMARY, val showTitles: Boolean = true, val useSeries: Boolean = true, -) +) { + companion object { + val ViewOptionsColumns = + AppSliderPreference( + title = R.string.height, + defaultValue = Cards.HEIGHT_2X3_DP.toLong(), + min = Cards.HEIGHT_2X3_DP / 2L, + max = Cards.HEIGHT_2X3_DP + 64L, + interval = 4, + getter = { it.heightDp.toLong() }, + setter = { prefs, value -> prefs.copy(heightDp = value.toInt()) }, + ) + val ViewOptionsSpacing = + AppSliderPreference( + title = R.string.spacing, + defaultValue = 16, + min = 0, + max = 32, + interval = 2, + getter = { it.spacing.toLong() }, + setter = { prefs, value -> prefs.copy(spacing = value.toInt()) }, + ) + + val ViewOptionsContentScale = + AppChoicePreference( + title = R.string.global_content_scale, + defaultValue = PrefContentScale.FIT, + displayValues = R.array.content_scale, + getter = { it.contentScale }, + setter = { viewOptions, value -> viewOptions.copy(contentScale = value) }, + indexToValue = { PrefContentScale.forNumber(it) }, + valueToIndex = { it.number }, + ) + + val ViewOptionsAspectRatio = + AppChoicePreference( + title = R.string.aspect_ratio, + defaultValue = AspectRatio.TALL, + displayValues = R.array.aspect_ratios, + getter = { it.aspectRatio }, + setter = { viewOptions, value -> viewOptions.copy(aspectRatio = value) }, + indexToValue = { AspectRatio.entries[it] }, + valueToIndex = { it.ordinal }, + ) + + val ViewOptionsShowTitles = + AppSwitchPreference( + title = R.string.show_titles, + defaultValue = true, + getter = { it.showTitles }, + setter = { vo, value -> vo.copy(showTitles = value) }, + ) + + val ViewOptionsUseSeries = + AppSwitchPreference( + title = R.string.go_to_series, // TODO + defaultValue = true, + getter = { it.useSeries }, + setter = { vo, value -> vo.copy(useSeries = value) }, + ) + + val ViewOptionsImageType = + AppChoicePreference( + title = R.string.image_type, + defaultValue = ViewOptionImageType.PRIMARY, + displayValues = R.array.image_types, + getter = { it.imageType }, + setter = { viewOptions, value -> + val aspectRatio = + when (value) { + ViewOptionImageType.PRIMARY -> AspectRatio.TALL + ViewOptionImageType.THUMB -> AspectRatio.WIDE + } + viewOptions.copy(imageType = value, aspectRatio = aspectRatio) + }, + indexToValue = { ViewOptionImageType.entries[it] }, + valueToIndex = { it.ordinal }, + ) + + val ViewOptionsReset = + AppClickablePreference( + title = R.string.reset, + ) + + val OPTIONS = + listOf( + ViewOptionsImageType, + ViewOptionsAspectRatio, + // TODO +// ViewOptionsShowTitles, +// ViewOptionsUseSeries, + ViewOptionsColumns, + ViewOptionsSpacing, + ViewOptionsContentScale, + ViewOptionsReset, + ) + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt index 70de419b..1c75cc6f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt @@ -60,6 +60,8 @@ fun BannerCard( cardHeight: Dp = 120.dp, aspectRatio: Float = AspectRatios.WIDE, interactionSource: MutableInteractionSource? = null, + imageType: ImageType = ImageType.PRIMARY, + imageContentScale: ContentScale = ContentScale.FillBounds, ) { val imageUrlService = LocalImageUrlService.current val density = LocalDensity.current @@ -74,11 +76,11 @@ fun BannerCard( } } val imageUrl = - remember(item, fillHeight) { + remember(item, fillHeight, imageType) { if (item != null) { imageUrlService.getItemImageUrl( item, - ImageType.PRIMARY, + imageType, fillWidth = null, fillHeight = fillHeight, ) @@ -107,7 +109,7 @@ fun BannerCard( AsyncImage( model = imageUrl, contentDescription = null, - contentScale = ContentScale.FillBounds, + contentScale = imageContentScale, onError = { imageError = true }, modifier = Modifier.fillMaxSize(), ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index 084c00d7..88b2fd27 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -46,7 +46,6 @@ import androidx.tv.material3.Text import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences -import com.github.damontecres.wholphin.ui.AspectRatios import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.cards.BannerCard import com.github.damontecres.wholphin.ui.cards.ItemRow @@ -67,6 +66,7 @@ import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.playback.isPlayKeyUp import com.github.damontecres.wholphin.ui.playback.playable +import com.github.damontecres.wholphin.ui.playback.scale import com.github.damontecres.wholphin.ui.rememberPosition import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.HomeRowLoadingState @@ -308,6 +308,7 @@ fun HomePageContent( is HomeRowLoadingState.Success -> { if (row.items.isNotEmpty()) { + val viewOptions = row.viewOptions ItemRow( title = row.title, items = row.items, @@ -323,11 +324,14 @@ fun HomePageContent( .focusGroup() .focusRequester(rowFocusRequesters[rowIndex]) .animateItem(), + horizontalPadding = viewOptions.spacing.dp, cardContent = { index, item, cardModifier, onClick, onLongClick -> BannerCard( name = item?.data?.seriesName ?: item?.name, item = item, - aspectRatio = AspectRatios.TALL, + aspectRatio = viewOptions.aspectRatio.ratio, + imageType = viewOptions.imageType.imageType, + imageContentScale = viewOptions.contentScale.scale, cornerText = item?.ui?.episdodeUnplayedCornerText, played = item?.data?.userData?.played ?: false, favorite = item?.favorite ?: false, @@ -366,7 +370,7 @@ fun HomePageContent( return@onKeyEvent false }, interactionSource = null, - cardHeight = Cards.height2x3, + cardHeight = viewOptions.heightDp.dp, ) }, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageRowList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageRowList.kt index 81342db9..c5c10d3e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageRowList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageRowList.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed @@ -26,6 +25,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.tv.material3.Icon +import androidx.tv.material3.ListItem import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import androidx.tv.material3.surfaceColorAtElevation @@ -44,6 +44,7 @@ enum class MoveDirection { @Composable fun HomePageRowList( state: HomePageSettingsState, + onClick: (Int, HomeRowConfigDisplay) -> Unit, onClickAdd: () -> Unit, onClickMove: (MoveDirection, Int) -> Unit, onClickDelete: (Int) -> Unit, @@ -66,6 +67,7 @@ fun HomePageRowList( deleteAllowed = row.config !is HomeRowConfig.ContinueWatching, onClickMove = { onClickMove.invoke(it, index) }, onClickDelete = { onClickDelete.invoke(index) }, + onClick = { onClick.invoke(index, row) }, modifier = Modifier .fillMaxWidth() @@ -92,6 +94,7 @@ fun HomeRowConfigContent( moveUpAllowed: Boolean, moveDownAllowed: Boolean, deleteAllowed: Boolean, + onClick: () -> Unit, onClickMove: (MoveDirection) -> Unit, onClickDelete: () -> Unit, modifier: Modifier, @@ -111,15 +114,18 @@ fun HomeRowConfigContent( shape = RoundedCornerShape(8.dp), ), ) { - Text( - text = config.title, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.titleMedium, - overflow = TextOverflow.Ellipsis, - modifier = - Modifier - .weight(1f) - .padding(start = 4.dp), + ListItem( + selected = false, + headlineContent = { + Text( + text = config.title, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier, + ) + }, + onClick = onClick, + modifier = Modifier.weight(1f), ) Row( horizontalArrangement = Arrangement.spacedBy(4.dp), diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageRowSettings.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageRowSettings.kt new file mode 100644 index 00000000..7b605ebd --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageRowSettings.kt @@ -0,0 +1,56 @@ +package com.github.damontecres.wholphin.ui.main.settings + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text +import androidx.tv.material3.surfaceColorAtElevation +import com.github.damontecres.wholphin.data.model.HomeRowViewOptions +import com.github.damontecres.wholphin.preferences.AppPreference +import com.github.damontecres.wholphin.ui.preferences.ComposablePreference + +@Composable +fun HomePageRowSettings( + title: String, + viewOptions: HomeRowViewOptions, + onViewOptionsChange: (HomeRowViewOptions) -> Unit, + modifier: Modifier = Modifier, + defaultViewOptions: HomeRowViewOptions = HomeRowViewOptions(), +) { + Column(modifier = modifier) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + LazyColumn { + items(HomeRowViewOptions.OPTIONS) { pref -> + pref as AppPreference + val interactionSource = remember { MutableInteractionSource() } + val value = pref.getter.invoke(viewOptions) + ComposablePreference( + preference = pref, + value = value, + onNavigate = {}, + onValueChange = { newValue -> + onViewOptionsChange.invoke(pref.setter(viewOptions, newValue)) + }, + interactionSource = interactionSource, + onClickPreference = { pref -> + if (pref == HomeRowViewOptions.ViewOptionsReset) { + onViewOptionsChange.invoke(defaultViewOptions) + } + }, + modifier = Modifier.background(MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp)), + ) + } + } + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageSettings.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageSettings.kt index 0070bd2d..fba9c5db 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageSettings.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageSettings.kt @@ -159,6 +159,7 @@ class HomePageSettingsViewModel HomeRowLoadingState.Success( title = context.getString(R.string.continue_watching), items = items, + viewOptions = row.viewOptions, ), ) } else { @@ -167,6 +168,7 @@ class HomePageSettingsViewModel HomeRowLoadingState.Success( title = context.getString(R.string.continue_watching), items = resume, + viewOptions = row.viewOptions, ), ) } @@ -175,6 +177,7 @@ class HomePageSettingsViewModel HomeRowLoadingState.Success( title = context.getString(R.string.next_up), items = nextUp, + viewOptions = row.viewOptions, ), ) } @@ -202,7 +205,13 @@ class HomePageSettingsViewModel val title = name?.let { context.getString(R.string.genres_in, it) } ?: context.getString(R.string.genres) - listOf(HomeRowLoadingState.Success(title, items)) + listOf( + HomeRowLoadingState.Success( + title, + items, + viewOptions = row.viewOptions, + ), + ) } is HomeRowConfig.RecentlyAdded -> { @@ -222,7 +231,17 @@ class HomePageSettingsViewModel limit = limit, isPlayed = null, // Server will handle user's preference ) - latestNextUpService.loadLatest(listOf(LatestData(title, request))) + latestNextUpService + .loadLatest(listOf(LatestData(title, request))) + .let { + it.map { + if (it is HomeRowLoadingState.Success) { + it.copy(viewOptions = row.viewOptions) + } else { + it + } + } + } } is HomeRowConfig.RecentlyReleased -> { @@ -342,6 +361,39 @@ class HomePageSettingsViewModel fetchRowData() } } + + fun updateViewOptions( + rowId: UUID, + viewOptions: HomeRowViewOptions, + ) { + viewModelScope.launchIO { + _state.update { + val index = state.value.rows.indexOfFirst { it.config.id == rowId } + val newRowConfig = + state.value.rows[index] + .config + .updateViewOptions(viewOptions) + val newRow = state.value.rows[index].copy(config = newRowConfig) + it.copy( + rows = + it.rows.toMutableList().apply { + set(index, newRow) + }, + rowData = + it.rowData.toMutableList().apply { + val row = it.rowData[index] + val newRow = + if (row is HomeRowLoadingState.Success) { + row.copy(viewOptions = viewOptions) + } else { + row + } + set(index, newRow) + }, + ) + } + } + } } data class HomePageSettingsState( @@ -410,6 +462,9 @@ fun HomePageSettings( onClickAdd = { destination = HomePageSettingsDestination.ChooseLibrary }, onClickMove = viewModel::moveRow, onClickDelete = viewModel::deleteRow, + onClick = { index, row -> + destination = HomePageSettingsDestination.RowSettings(row.config.id) + }, modifier = destModifier, ) } @@ -434,7 +489,17 @@ fun HomePageSettings( } is HomePageSettingsDestination.RowSettings -> { - TODO() + val row = + state.rows + .first { it.config.id == dest.rowId } + HomePageRowSettings( + title = row.title, + viewOptions = row.config.viewOptions, + onViewOptionsChange = { + viewModel.updateViewOptions(dest.rowId, it) + }, + modifier = destModifier, + ) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageSettingsDestination.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageSettingsDestination.kt index 51d7a8e8..f6c21219 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageSettingsDestination.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageSettingsDestination.kt @@ -1,6 +1,6 @@ package com.github.damontecres.wholphin.ui.main.settings -import com.github.damontecres.wholphin.data.model.HomeRowConfigDisplay +import java.util.UUID sealed interface HomePageSettingsDestination { data object RowList : HomePageSettingsDestination @@ -12,6 +12,6 @@ sealed interface HomePageSettingsDestination { ) : HomePageSettingsDestination data class RowSettings( - val row: HomeRowConfigDisplay, + val rowId: UUID, ) : HomePageSettingsDestination } diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt b/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt index c35e9512..50e42889 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/LoadingState.kt @@ -1,6 +1,7 @@ package com.github.damontecres.wholphin.util import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.HomeRowViewOptions /** * Generic state for loading something from the API @@ -59,6 +60,7 @@ sealed interface HomeRowLoadingState { data class Success( override val title: String, val items: List, + val viewOptions: HomeRowViewOptions = HomeRowViewOptions(), ) : HomeRowLoadingState data class Error( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e9629e19..e3663513 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -465,6 +465,7 @@ Add row Genres in %1$s Recently released in %1$s + Height Disabled