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 8149f8b6..12b78bfb 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 @@ -504,6 +504,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, @@ -1074,6 +1123,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 ddac936a..83829a62 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 @@ -100,6 +100,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 bf98744a..fa1d7c2b 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 dd511f29..be1d70c1 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