From 4d575b1e1186c7263a4faab300d7a433356e5bab Mon Sep 17 00:00:00 2001 From: Justin Visser Date: Tue, 26 May 2026 10:48:16 +0200 Subject: [PATCH] Add Display size customization page Consolidates UI sizing into three percentage sliders surfaced through a new Settings -> Interface -> Display size page: UI scale (50-150%), Card size (50-150%), Spacing (25-175%). Each persists immediately via DataStore; the rest of the app reflects them through LocalInterfaceCustomization and a root-level LocalDensity override in MainContent. The page mirrors HomeSettingsPage's two-column shell: a fixed-width settings panel on the left, HomePageContent rendering live preview rows on the right. The page snapshots its own density on entry so the slider rail stays fixed while UI scale changes the rest of the app. Includes a one-shot AppUpgradeHandler migration block for installs at or before 0.6.4-31-g0 that resets the three new percent fields to their defaults. --- .../damontecres/wholphin/MainContent.kt | 16 +- .../wholphin/preferences/AppPreference.kt | 50 +++ .../preferences/AppPreferencesSerializer.kt | 3 + .../wholphin/services/AppUpgradeHandler.kt | 10 + .../damontecres/wholphin/ui/UiConstants.kt | 2 + .../wholphin/ui/nav/Destination.kt | 3 + .../wholphin/ui/nav/DestinationContent.kt | 8 + .../displaysize/DisplaySizePage.kt | 417 ++++++++++++++++++ .../ui/util/InterfaceCustomization.kt | 33 +- .../wholphin/ui/util/InterfaceHelpers.kt | 9 + app/src/main/proto/WholphinDataStore.proto | 3 + app/src/main/res/values/strings.xml | 8 +- 12 files changed, 551 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/preferences/displaysize/DisplaySizePage.kt create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/util/InterfaceHelpers.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt b/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt index 714d80dc..212f4bba 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt @@ -18,6 +18,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleEventEffect @@ -60,7 +62,19 @@ fun MainContent( // setupNavigationManager.backStack = backStack val interfaceCustomization = remember(appPreferences) { InterfaceCustomization(appPreferences) } - CompositionLocalProvider(LocalInterfaceCustomization provides interfaceCustomization) { + val baseDensity = LocalDensity.current + val scaledDensity = + remember(baseDensity, interfaceCustomization.uiScalePercent) { + val factor = interfaceCustomization.uiScalePercent / 100f + Density( + density = baseDensity.density * factor, + fontScale = baseDensity.fontScale * factor, + ) + } + CompositionLocalProvider( + LocalInterfaceCustomization provides interfaceCustomization, + LocalDensity provides scaledDensity, + ) { NavDisplay( backStack = backStack, onBack = { backStack.removeLastOrNull() }, diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt index 6c92358c..62a3bd92 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt @@ -516,6 +516,55 @@ sealed interface AppPreference { summaryOff = R.string.disabled, ) + val UiScale = + AppSliderPreference( + title = R.string.ui_scale, + defaultValue = 100, + min = 50, + max = 150, + interval = 5, + getter = { it.interfacePreferences.uiScalePercent.toLong() }, + setter = { prefs, value -> + prefs.updateInterfacePreferences { uiScalePercent = value.toInt() } + }, + summarizer = { it?.let { "$it%" } }, + ) + + val CardSize = + AppSliderPreference( + title = R.string.card_size, + defaultValue = 100, + min = 50, + max = 150, + interval = 5, + getter = { it.interfacePreferences.cardSizePercent.toLong() }, + setter = { prefs, value -> + prefs.updateInterfacePreferences { cardSizePercent = value.toInt() } + }, + summarizer = { it?.let { "$it%" } }, + ) + + val Spacing = + AppSliderPreference( + title = R.string.spacing, + defaultValue = 100, + min = 25, + max = 175, + interval = 10, + getter = { it.interfacePreferences.spacingPercent.toLong() }, + setter = { prefs, value -> + prefs.updateInterfacePreferences { spacingPercent = value.toInt() } + }, + summarizer = { it?.let { "$it%" } }, + ) + + val DisplaySizePref = + AppDestinationPreference( + title = R.string.display_size, + summary = R.string.display_size_summary, + destination = Destination.DisplaySize, + ) + val DisplayTogglesPref = AppMultiChoicePreference( title = R.string.display_toggles_title, @@ -1086,6 +1135,7 @@ val basicPreferences = AppPreference.SignInAuto, AppPreference.RememberSelectedTab, AppPreference.PlayThemeMusic, + AppPreference.DisplaySizePref, AppPreference.SubtitleStyle, AppPreference.ThemeColors, AppPreference.ScreensaverSettings, diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt index 59b9a6ea..677bcf51 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt @@ -101,6 +101,9 @@ class AppPreferencesSerializer showLogos = AppPreference.ShowLogos.defaultValue clearDisplayToggles() addAllDisplayToggles(AppPreference.DisplayTogglesPref.defaultValue) + uiScalePercent = AppPreference.UiScale.defaultValue.toInt() + cardSizePercent = AppPreference.CardSize.defaultValue.toInt() + spacingPercent = AppPreference.Spacing.defaultValue.toInt() searchPreferences = SearchPreferences diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index 281eddc2..909c7a52 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -350,5 +350,15 @@ class AppUpgradeHandler } } } + + if (previous.isEqualOrBefore(Version.fromString("0.6.4-31-g0"))) { + appPreferences.updateData { + it.updateInterfacePreferences { + uiScalePercent = AppPreference.UiScale.defaultValue.toInt() + cardSizePercent = AppPreference.CardSize.defaultValue.toInt() + spacingPercent = AppPreference.Spacing.defaultValue.toInt() + } + } + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt index ab0f69f8..b9c76bc6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/UiConstants.kt @@ -43,6 +43,8 @@ object AppColors { const val DEFAULT_PAGE_SIZE = 100 +const val BASE_SPACING_DP = 16 + /** * The default [ItemFields] to fetch for most queries */ diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt index 761978ef..eeee201c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt @@ -52,6 +52,9 @@ sealed class Destination( val hdr: Boolean, ) : Destination(true) + @Serializable + data object DisplaySize : Destination(true) + @Serializable data object Search : Destination() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt index 15c95717..3b2c80e8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt @@ -45,6 +45,7 @@ import com.github.damontecres.wholphin.ui.main.settings.HomeSettingsPage import com.github.damontecres.wholphin.ui.playback.PlayExternalPage import com.github.damontecres.wholphin.ui.playback.PlaybackPage import com.github.damontecres.wholphin.ui.preferences.PreferencesPage +import com.github.damontecres.wholphin.ui.preferences.displaysize.DisplaySizePage import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleStylePage import com.github.damontecres.wholphin.ui.setup.InstallUpdatePage import com.github.damontecres.wholphin.ui.slideshow.SlideshowPage @@ -127,6 +128,13 @@ fun DestinationContent( ) } + is Destination.DisplaySize -> { + DisplaySizePage( + preferences.appPreferences, + modifier, + ) + } + is Destination.SeriesOverview -> { SeriesOverview( preferences = preferences, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/displaysize/DisplaySizePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/displaysize/DisplaySizePage.kt new file mode 100644 index 00000000..a484df02 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/displaysize/DisplaySizePage.kt @@ -0,0 +1,417 @@ +package com.github.damontecres.wholphin.ui.preferences.displaysize + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.focusRestorer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.tv.material3.Icon +import androidx.tv.material3.MaterialTheme +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.AppPreference +import com.github.damontecres.wholphin.preferences.AppPreferences +import com.github.damontecres.wholphin.preferences.AppSliderPreference +import com.github.damontecres.wholphin.preferences.updateInterfacePreferences +import com.github.damontecres.wholphin.services.BackdropService +import com.github.damontecres.wholphin.ui.FontAwesome +import com.github.damontecres.wholphin.ui.data.RowColumn +import com.github.damontecres.wholphin.ui.launchIO +import com.github.damontecres.wholphin.ui.main.HomePageContent +import com.github.damontecres.wholphin.ui.main.settings.HomeSettingsListItem +import com.github.damontecres.wholphin.ui.main.settings.TitleText +import com.github.damontecres.wholphin.ui.main.settings.settingsWidth +import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel +import com.github.damontecres.wholphin.ui.preferences.SliderPreference +import com.github.damontecres.wholphin.ui.tryRequestFocus +import com.github.damontecres.wholphin.ui.util.ResStringProvider +import com.github.damontecres.wholphin.ui.util.withinBoundsOrDefault +import com.github.damontecres.wholphin.util.HomeRowLoadingState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.itemsApi +import org.jellyfin.sdk.model.api.BaseItemKind +import org.jellyfin.sdk.model.api.ItemSortBy +import org.jellyfin.sdk.model.api.SortOrder +import org.jellyfin.sdk.model.api.request.GetItemsRequest +import timber.log.Timber +import javax.inject.Inject + +private const val PREVIEW_POSTER_COUNT = 5 +private const val PREVIEW_EPISODE_COUNT = 4 +private val PREVIEW_NAV_RAIL_WIDTH_DP = 64.dp +private val PREVIEW_NAV_ITEM_HEIGHT_DP = 56.dp +private val PREVIEW_NAV_ICON_SIZE_DP = 24.dp + +private val PreviewNavRailIcons: List = + listOf( + Icons.Default.Person, + Icons.Default.Home, + Icons.Default.Favorite, + Icons.Default.Search, + Icons.Default.Add, + Icons.Default.Star, + Icons.Default.PlayArrow, + Icons.Default.ArrowDropDown, + Icons.Default.Settings, + ) + +@HiltViewModel +class DisplaySizeViewModel + @Inject + constructor( + private val api: ApiClient, + private val backdropService: BackdropService, + ) : ViewModel() { + val posters = MutableStateFlow>(emptyList()) + val episodes = MutableStateFlow>(emptyList()) + + fun updateBackdrop(item: BaseItem) { + viewModelScope.launchIO { backdropService.submit(item) } + } + + init { + viewModelScope.launchIO { + fetch(BaseItemKind.MOVIE, PREVIEW_POSTER_COUNT, posters) + } + viewModelScope.launchIO { + fetch(BaseItemKind.EPISODE, PREVIEW_EPISODE_COUNT, episodes) + } + } + + private suspend fun fetch( + kind: BaseItemKind, + limit: Int, + target: MutableStateFlow>, + ) { + try { + val request = + GetItemsRequest( + recursive = true, + includeItemTypes = listOf(kind), + sortBy = listOf(ItemSortBy.DATE_CREATED), + sortOrder = listOf(SortOrder.DESCENDING), + limit = limit, + ) + val result = api.itemsApi.getItems(request).content + val items = (result.items ?: emptyList()).map { BaseItem.from(it, api, false) } + target.value = items + } catch (ex: Exception) { + Timber.w(ex, "DisplaySize preview: could not fetch %s sample", kind) + } + } + } + +@Composable +fun DisplaySizePage( + initialPreferences: AppPreferences, + modifier: Modifier = Modifier, + viewModel: PreferencesViewModel = hiltViewModel(), + previewViewModel: DisplaySizeViewModel = hiltViewModel(), +) { + var preferences by remember { mutableStateOf(initialPreferences) } + LaunchedEffect(Unit) { + viewModel.preferenceDataStore.data.collect { preferences = it } + } + val scope = rememberCoroutineScope() + val iface = preferences.interfacePreferences + + val uiScale = iface.uiScalePercent.withinBoundsOrDefault(AppPreference.UiScale) + val cardSize = iface.cardSizePercent.withinBoundsOrDefault(AppPreference.CardSize) + val spacing = iface.spacingPercent.withinBoundsOrDefault(AppPreference.Spacing) + + val posters by previewViewModel.posters.collectAsState() + val episodes by previewViewModel.episodes.collectAsState() + + // Snapshot the page's own density on first composition so the slider rail + // and labels stay fixed while the user drags UI scale. + val ambientDensity = LocalDensity.current + val pageDensity = remember { ambientDensity } + + // Resolve back to system base so the preview density is independent of the + // snapshotted page density and reflects the draft uiScale faithfully. + val context = LocalContext.current + val systemDensity = + remember { + val dm = context.resources.displayMetrics + Density( + density = dm.density, + fontScale = context.resources.configuration.fontScale, + ) + } + val previewDensity = + remember(systemDensity, uiScale) { + val factor = uiScale / 100f + Density( + density = systemDensity.density * factor, + fontScale = systemDensity.fontScale * factor, + ) + } + + CompositionLocalProvider(LocalDensity provides pageDensity) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier.fillMaxSize(), + ) { + Box( + modifier = + Modifier + .width(settingsWidth) + .fillMaxHeight() + .background(color = MaterialTheme.colorScheme.surface) + .padding(8.dp), + ) { + DisplaySizeSettingsColumn( + uiScale = uiScale, + cardSize = cardSize, + spacing = spacing, + onUiScaleChange = { value -> + scope.launch { + viewModel.preferenceDataStore.updateData { + it.updateInterfacePreferences { uiScalePercent = value } + } + } + }, + onCardSizeChange = { value -> + scope.launch { + viewModel.preferenceDataStore.updateData { + it.updateInterfacePreferences { cardSizePercent = value } + } + } + }, + onSpacingChange = { value -> + scope.launch { + viewModel.preferenceDataStore.updateData { + it.updateInterfacePreferences { spacingPercent = value } + } + } + }, + onClickReset = { + scope.launch { + viewModel.preferenceDataStore.updateData { + it.updateInterfacePreferences { + uiScalePercent = AppPreference.UiScale.defaultValue.toInt() + cardSizePercent = AppPreference.CardSize.defaultValue.toInt() + spacingPercent = AppPreference.Spacing.defaultValue.toInt() + } + } + } + }, + ) + } + + Box( + modifier = + Modifier + .fillMaxHeight() + .weight(1f), + ) { + CompositionLocalProvider(LocalDensity provides previewDensity) { + DisplaySizePreview( + posters = posters, + episodes = episodes, + showLogos = iface.showLogos, + onUpdateBackdrop = previewViewModel::updateBackdrop, + ) + } + } + } + } +} + +@Composable +private fun DisplaySizeSettingsColumn( + uiScale: Int, + cardSize: Int, + spacing: Int, + onUiScaleChange: (Int) -> Unit, + onCardSizeChange: (Int) -> Unit, + onSpacingChange: (Int) -> Unit, + onClickReset: () -> Unit, +) { + val firstFocus = remember { FocusRequester() } + LaunchedEffect(Unit) { firstFocus.tryRequestFocus() } + + Column(modifier = Modifier.fillMaxSize()) { + TitleText(stringResource(R.string.display_size)) + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(horizontal = 8.dp), + modifier = + Modifier + .fillMaxHeight() + .focusRestorer(firstFocus), + ) { + item { + DisplaySizeSlider( + preference = AppPreference.UiScale, + value = uiScale, + onChange = onUiScaleChange, + modifier = Modifier.focusRequester(firstFocus), + ) + } + item { + DisplaySizeSlider( + preference = AppPreference.CardSize, + value = cardSize, + onChange = onCardSizeChange, + ) + } + item { + DisplaySizeSlider( + preference = AppPreference.Spacing, + value = spacing, + onChange = onSpacingChange, + ) + } + item { HorizontalDivider() } + item { + HomeSettingsListItem( + selected = false, + headlineText = stringResource(R.string.reset_to_defaults), + leadingContent = { + Text( + text = stringResource(R.string.fa_arrows_rotate), + fontFamily = FontAwesome, + ) + }, + onClick = onClickReset, + ) + } + } + } +} + +@Composable +private fun DisplaySizeSlider( + preference: AppSliderPreference, + value: Int, + onChange: (Int) -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + SliderPreference( + preference = preference, + title = stringResource(preference.title), + summary = preference.summary(context, value.toLong()), + value = value.toLong(), + onChange = { onChange(it.toInt()) }, + modifier = modifier, + ) +} + +@Composable +private fun DisplaySizePreview( + posters: List, + episodes: List, + showLogos: Boolean, + onUpdateBackdrop: (BaseItem) -> Unit, +) { + val recentlyAddedTitle = ResStringProvider(R.string.recently_added) + val nextUpTitle = ResStringProvider(R.string.next_up) + val previewRows = + remember(posters, episodes) { + listOf( + HomeRowLoadingState.Success(title = recentlyAddedTitle, items = posters), + HomeRowLoadingState.Success(title = nextUpTitle, items = episodes), + ) + } + var position by remember { mutableStateOf(RowColumn(0, 0)) } + + Row(modifier = Modifier.fillMaxSize()) { + PreviewNavRail() + HomePageContent( + homeRows = previewRows, + position = position, + onFocusPosition = { position = it }, + onClickItem = { _, _ -> }, + onLongClickItem = { _, _ -> }, + onClickPlay = { _, _ -> }, + showClock = false, + onUpdateBackdrop = onUpdateBackdrop, + showLogo = showLogos, + showViewMore = false, + takeFocus = false, + showEmptyRows = true, + modifier = + Modifier + .fillMaxHeight() + .weight(1f), + ) + } +} + +@Composable +private fun PreviewNavRail() { + Column( + modifier = + Modifier + .width(PREVIEW_NAV_RAIL_WIDTH_DP) + .fillMaxHeight(), + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + PreviewNavRailIcons.forEach { icon -> + Box( + modifier = + Modifier + .fillMaxWidth() + .height(PREVIEW_NAV_ITEM_HEIGHT_DP), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(PREVIEW_NAV_ICON_SIZE_DP), + ) + } + } + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/util/InterfaceCustomization.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/util/InterfaceCustomization.kt index 30bcac68..b59e7162 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/util/InterfaceCustomization.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/util/InterfaceCustomization.kt @@ -1,8 +1,11 @@ package com.github.damontecres.wholphin.ui.util import androidx.compose.runtime.staticCompositionLocalOf +import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.DisplayToggle +import com.github.damontecres.wholphin.ui.BASE_SPACING_DP +import com.github.damontecres.wholphin.ui.Cards import java.util.EnumSet /** @@ -10,16 +13,32 @@ import java.util.EnumSet */ data class InterfaceCustomization( val enabledDisplayToggles: EnumSet, + val uiScalePercent: Int = AppPreference.UiScale.defaultValue.toInt(), + val cardSizePercent: Int = AppPreference.CardSize.defaultValue.toInt(), + val spacingPercent: Int = AppPreference.Spacing.defaultValue.toInt(), ) { constructor(prefs: AppPreferences) : this( - prefs.interfacePreferences.displayTogglesList.let { - if (it.isEmpty()) { - EnumSet.noneOf(DisplayToggle::class.java) - } else { - EnumSet.copyOf(it) - } - }, + enabledDisplayToggles = + prefs.interfacePreferences.displayTogglesList.let { + if (it.isEmpty()) { + EnumSet.noneOf(DisplayToggle::class.java) + } else { + EnumSet.copyOf(it) + } + }, + uiScalePercent = + prefs.interfacePreferences.uiScalePercent.withinBoundsOrDefault(AppPreference.UiScale), + cardSizePercent = + prefs.interfacePreferences.cardSizePercent.withinBoundsOrDefault(AppPreference.CardSize), + spacingPercent = + prefs.interfacePreferences.spacingPercent.withinBoundsOrDefault(AppPreference.Spacing), ) + + val cardHeightDp: Int + get() = Cards.HEIGHT_2X3_DP * cardSizePercent / 100 + + val spacingDp: Int + get() = BASE_SPACING_DP * spacingPercent / 100 } val LocalInterfaceCustomization = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/util/InterfaceHelpers.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/util/InterfaceHelpers.kt new file mode 100644 index 00000000..449619c1 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/util/InterfaceHelpers.kt @@ -0,0 +1,9 @@ +package com.github.damontecres.wholphin.ui.util + +import com.github.damontecres.wholphin.preferences.AppSliderPreference + +internal fun Int.withinBoundsOrDefault(preference: AppSliderPreference<*>): Int { + val min = preference.min.toInt() + val max = preference.max.toInt() + return if (this in min..max) this else preference.defaultValue.toInt() +} diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index f4ca3f98..77378d93 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -193,6 +193,9 @@ message InterfacePreferences { bool show_logos = 13; SearchPreferences search_preferences = 14; repeated DisplayToggle display_toggles = 15; + int32 ui_scale_percent = 16; + int32 card_size_percent = 17; + int32 spacing_percent = 18; } message AdvancedPreferences { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 119a4a84..1d86a65a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -356,6 +356,7 @@ Subtitle style Background color Reset + Reset to defaults Bold font Playback Backend @@ -396,6 +397,10 @@ View options Columns Spacing + Card size + Display size + Scale the app UI, card size, and spacing + UI scale Aspect Ratio Show details Image type @@ -525,7 +530,6 @@ Add row Genres in %1$s Recently released in %1$s - Height Apply to all rows Customize home page Home rows @@ -538,8 +542,6 @@ Overwrite local settings? For episodes Suggestions for %1$s - Increase size for all cards - Decrease size for all cards Use thumb images Settings saved Display presets