Apply view options to preview

This commit is contained in:
Damontecres 2026-01-23 12:44:20 -05:00
parent 5dcdde36c3
commit 1ce380f3b5
No known key found for this signature in database
9 changed files with 275 additions and 26 deletions

View file

@ -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<HomeRowViewOptions>(
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<HomeRowViewOptions>(
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<HomeRowViewOptions, PrefContentScale>(
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<HomeRowViewOptions, AspectRatio>(
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<HomeRowViewOptions>(
title = R.string.show_titles,
defaultValue = true,
getter = { it.showTitles },
setter = { vo, value -> vo.copy(showTitles = value) },
)
val ViewOptionsUseSeries =
AppSwitchPreference<HomeRowViewOptions>(
title = R.string.go_to_series, // TODO
defaultValue = true,
getter = { it.useSeries },
setter = { vo, value -> vo.copy(useSeries = value) },
)
val ViewOptionsImageType =
AppChoicePreference<HomeRowViewOptions, ViewOptionImageType>(
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<ViewOptions>(
title = R.string.reset,
)
val OPTIONS =
listOf(
ViewOptionsImageType,
ViewOptionsAspectRatio,
// TODO
// ViewOptionsShowTitles,
// ViewOptionsUseSeries,
ViewOptionsColumns,
ViewOptionsSpacing,
ViewOptionsContentScale,
ViewOptionsReset,
)
}
}

View file

@ -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(),
)

View file

@ -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,
)
},
)

View file

@ -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),
),
) {
ListItem(
selected = false,
headlineContent = {
Text(
text = config.title,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleMedium,
overflow = TextOverflow.Ellipsis,
modifier =
Modifier
.weight(1f)
.padding(start = 4.dp),
Modifier,
)
},
onClick = onClick,
modifier = Modifier.weight(1f),
)
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),

View file

@ -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<HomeRowViewOptions, Any>
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)),
)
}
}
}
}

View file

@ -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,
)
}
}
}

View file

@ -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
}

View file

@ -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<BaseItem?>,
val viewOptions: HomeRowViewOptions = HomeRowViewOptions(),
) : HomeRowLoadingState
data class Error(

View file

@ -465,6 +465,7 @@
<string name="add_row">Add row</string>
<string name="genres_in">Genres in %1$s</string>
<string name="recently_released_in">Recently released in %1$s</string>
<string name="height">Height</string>
<string-array name="theme_song_volume">
<item>Disabled</item>