mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
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.
This commit is contained in:
parent
81e5f31401
commit
8fb301c2bb
12 changed files with 551 additions and 11 deletions
|
|
@ -18,6 +18,8 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
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.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.compose.LifecycleEventEffect
|
import androidx.lifecycle.compose.LifecycleEventEffect
|
||||||
|
|
@ -60,7 +62,19 @@ fun MainContent(
|
||||||
// setupNavigationManager.backStack = backStack
|
// setupNavigationManager.backStack = backStack
|
||||||
val interfaceCustomization =
|
val interfaceCustomization =
|
||||||
remember(appPreferences) { InterfaceCustomization(appPreferences) }
|
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(
|
NavDisplay(
|
||||||
backStack = backStack,
|
backStack = backStack,
|
||||||
onBack = { backStack.removeLastOrNull() },
|
onBack = { backStack.removeLastOrNull() },
|
||||||
|
|
|
||||||
|
|
@ -504,6 +504,55 @@ sealed interface AppPreference<Pref, T> {
|
||||||
summaryOff = R.string.disabled,
|
summaryOff = R.string.disabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val UiScale =
|
||||||
|
AppSliderPreference<AppPreferences>(
|
||||||
|
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<AppPreferences>(
|
||||||
|
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<AppPreferences>(
|
||||||
|
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<AppPreferences>(
|
||||||
|
title = R.string.display_size,
|
||||||
|
summary = R.string.display_size_summary,
|
||||||
|
destination = Destination.DisplaySize,
|
||||||
|
)
|
||||||
|
|
||||||
val DisplayTogglesPref =
|
val DisplayTogglesPref =
|
||||||
AppMultiChoicePreference<AppPreferences, DisplayToggle>(
|
AppMultiChoicePreference<AppPreferences, DisplayToggle>(
|
||||||
title = R.string.display_toggles_title,
|
title = R.string.display_toggles_title,
|
||||||
|
|
@ -1074,6 +1123,7 @@ val basicPreferences =
|
||||||
AppPreference.SignInAuto,
|
AppPreference.SignInAuto,
|
||||||
AppPreference.RememberSelectedTab,
|
AppPreference.RememberSelectedTab,
|
||||||
AppPreference.PlayThemeMusic,
|
AppPreference.PlayThemeMusic,
|
||||||
|
AppPreference.DisplaySizePref,
|
||||||
AppPreference.SubtitleStyle,
|
AppPreference.SubtitleStyle,
|
||||||
AppPreference.ThemeColors,
|
AppPreference.ThemeColors,
|
||||||
AppPreference.ScreensaverSettings,
|
AppPreference.ScreensaverSettings,
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,9 @@ class AppPreferencesSerializer
|
||||||
showLogos = AppPreference.ShowLogos.defaultValue
|
showLogos = AppPreference.ShowLogos.defaultValue
|
||||||
clearDisplayToggles()
|
clearDisplayToggles()
|
||||||
addAllDisplayToggles(AppPreference.DisplayTogglesPref.defaultValue)
|
addAllDisplayToggles(AppPreference.DisplayTogglesPref.defaultValue)
|
||||||
|
uiScalePercent = AppPreference.UiScale.defaultValue.toInt()
|
||||||
|
cardSizePercent = AppPreference.CardSize.defaultValue.toInt()
|
||||||
|
spacingPercent = AppPreference.Spacing.defaultValue.toInt()
|
||||||
|
|
||||||
searchPreferences =
|
searchPreferences =
|
||||||
SearchPreferences
|
SearchPreferences
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ object AppColors {
|
||||||
|
|
||||||
const val DEFAULT_PAGE_SIZE = 100
|
const val DEFAULT_PAGE_SIZE = 100
|
||||||
|
|
||||||
|
const val BASE_SPACING_DP = 16
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default [ItemFields] to fetch for most queries
|
* The default [ItemFields] to fetch for most queries
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ sealed class Destination(
|
||||||
val hdr: Boolean,
|
val hdr: Boolean,
|
||||||
) : Destination(true)
|
) : Destination(true)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object DisplaySize : Destination(true)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Search : Destination()
|
data object Search : Destination()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.PlayExternalPage
|
||||||
import com.github.damontecres.wholphin.ui.playback.PlaybackPage
|
import com.github.damontecres.wholphin.ui.playback.PlaybackPage
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesPage
|
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.preferences.subtitle.SubtitleStylePage
|
||||||
import com.github.damontecres.wholphin.ui.setup.InstallUpdatePage
|
import com.github.damontecres.wholphin.ui.setup.InstallUpdatePage
|
||||||
import com.github.damontecres.wholphin.ui.slideshow.SlideshowPage
|
import com.github.damontecres.wholphin.ui.slideshow.SlideshowPage
|
||||||
|
|
@ -127,6 +128,13 @@ fun DestinationContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is Destination.DisplaySize -> {
|
||||||
|
DisplaySizePage(
|
||||||
|
preferences.appPreferences,
|
||||||
|
modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
is Destination.SeriesOverview -> {
|
is Destination.SeriesOverview -> {
|
||||||
SeriesOverview(
|
SeriesOverview(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
|
|
|
||||||
|
|
@ -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<ImageVector> =
|
||||||
|
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<List<BaseItem>>(emptyList())
|
||||||
|
val episodes = MutableStateFlow<List<BaseItem>>(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<List<BaseItem>>,
|
||||||
|
) {
|
||||||
|
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<AppPreferences>,
|
||||||
|
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<BaseItem>,
|
||||||
|
episodes: List<BaseItem>,
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package com.github.damontecres.wholphin.ui.util
|
package com.github.damontecres.wholphin.ui.util
|
||||||
|
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
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.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.DisplayToggle
|
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
|
import java.util.EnumSet
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -10,16 +13,32 @@ import java.util.EnumSet
|
||||||
*/
|
*/
|
||||||
data class InterfaceCustomization(
|
data class InterfaceCustomization(
|
||||||
val enabledDisplayToggles: EnumSet<DisplayToggle>,
|
val enabledDisplayToggles: EnumSet<DisplayToggle>,
|
||||||
|
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(
|
constructor(prefs: AppPreferences) : this(
|
||||||
prefs.interfacePreferences.displayTogglesList.let {
|
enabledDisplayToggles =
|
||||||
if (it.isEmpty()) {
|
prefs.interfacePreferences.displayTogglesList.let {
|
||||||
EnumSet.noneOf(DisplayToggle::class.java)
|
if (it.isEmpty()) {
|
||||||
} else {
|
EnumSet.noneOf(DisplayToggle::class.java)
|
||||||
EnumSet.copyOf(it)
|
} 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 =
|
val LocalInterfaceCustomization =
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -193,6 +193,9 @@ message InterfacePreferences {
|
||||||
bool show_logos = 13;
|
bool show_logos = 13;
|
||||||
SearchPreferences search_preferences = 14;
|
SearchPreferences search_preferences = 14;
|
||||||
repeated DisplayToggle display_toggles = 15;
|
repeated DisplayToggle display_toggles = 15;
|
||||||
|
int32 ui_scale_percent = 16;
|
||||||
|
int32 card_size_percent = 17;
|
||||||
|
int32 spacing_percent = 18;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AdvancedPreferences {
|
message AdvancedPreferences {
|
||||||
|
|
|
||||||
|
|
@ -356,6 +356,7 @@
|
||||||
<string name="subtitle_style">Subtitle style</string>
|
<string name="subtitle_style">Subtitle style</string>
|
||||||
<string name="background_color">Background color</string>
|
<string name="background_color">Background color</string>
|
||||||
<string name="reset">Reset</string>
|
<string name="reset">Reset</string>
|
||||||
|
<string name="reset_to_defaults">Reset to defaults</string>
|
||||||
<string name="bold_font">Bold font</string>
|
<string name="bold_font">Bold font</string>
|
||||||
|
|
||||||
<string name="player_backend">Playback Backend</string>
|
<string name="player_backend">Playback Backend</string>
|
||||||
|
|
@ -396,6 +397,10 @@
|
||||||
<string name="view_options">View options</string>
|
<string name="view_options">View options</string>
|
||||||
<string name="columns">Columns</string>
|
<string name="columns">Columns</string>
|
||||||
<string name="spacing">Spacing</string>
|
<string name="spacing">Spacing</string>
|
||||||
|
<string name="card_size">Card size</string>
|
||||||
|
<string name="display_size">Display size</string>
|
||||||
|
<string name="display_size_summary">Scale the app UI, card size, and spacing</string>
|
||||||
|
<string name="ui_scale">UI scale</string>
|
||||||
<string name="aspect_ratio">Aspect Ratio</string>
|
<string name="aspect_ratio">Aspect Ratio</string>
|
||||||
<string name="show_details">Show details</string>
|
<string name="show_details">Show details</string>
|
||||||
<string name="image_type">Image type</string>
|
<string name="image_type">Image type</string>
|
||||||
|
|
@ -525,7 +530,6 @@
|
||||||
<string name="add_row">Add row</string>
|
<string name="add_row">Add row</string>
|
||||||
<string name="genres_in">Genres in %1$s</string>
|
<string name="genres_in">Genres in %1$s</string>
|
||||||
<string name="recently_released_in">Recently released in %1$s</string>
|
<string name="recently_released_in">Recently released in %1$s</string>
|
||||||
<string name="height">Height</string>
|
|
||||||
<string name="apply_all_rows">Apply to all rows</string>
|
<string name="apply_all_rows">Apply to all rows</string>
|
||||||
<string name="customize_home">Customize home page</string>
|
<string name="customize_home">Customize home page</string>
|
||||||
<string name="home_rows">Home rows</string>
|
<string name="home_rows">Home rows</string>
|
||||||
|
|
@ -538,8 +542,6 @@
|
||||||
<string name="overwrite_local_settings">Overwrite local settings?</string>
|
<string name="overwrite_local_settings">Overwrite local settings?</string>
|
||||||
<string name="for_episodes">For episodes</string>
|
<string name="for_episodes">For episodes</string>
|
||||||
<string name="suggestions_for">Suggestions for %1$s</string>
|
<string name="suggestions_for">Suggestions for %1$s</string>
|
||||||
<string name="increase_all_cards_size">Increase size for all cards</string>
|
|
||||||
<string name="decrease_all_cards_size">Decrease size for all cards</string>
|
|
||||||
<string name="use_thumb_images">Use thumb images</string>
|
<string name="use_thumb_images">Use thumb images</string>
|
||||||
<string name="settings_saved">Settings saved</string>
|
<string name="settings_saved">Settings saved</string>
|
||||||
<string name="display_presets">Display presets</string>
|
<string name="display_presets">Display presets</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue