mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-29 23:59:32 +00:00
feat: sidebar for desktop
This commit is contained in:
parent
212bab5d27
commit
84cc669bd5
10 changed files with 820 additions and 4 deletions
|
|
@ -18,11 +18,13 @@ actual object ThemeSettingsStorage {
|
|||
private const val selectedThemeKey = "selected_theme"
|
||||
private const val amoledEnabledKey = "amoled_enabled"
|
||||
private const val liquidGlassNativeTabBarEnabledKey = "liquid_glass_native_tab_bar_enabled"
|
||||
private const val desktopNavigationLayoutKey = "desktop_navigation_layout"
|
||||
private const val selectedAppLanguageKey = "selected_app_language"
|
||||
private val profileScopedSyncKeys = listOf(
|
||||
selectedThemeKey,
|
||||
amoledEnabledKey,
|
||||
liquidGlassNativeTabBarEnabledKey,
|
||||
desktopNavigationLayoutKey,
|
||||
)
|
||||
private val globalSyncKeys = listOf(selectedAppLanguageKey)
|
||||
|
||||
|
|
@ -69,6 +71,16 @@ actual object ThemeSettingsStorage {
|
|||
?.apply()
|
||||
}
|
||||
|
||||
actual fun loadDesktopNavigationLayout(): String? =
|
||||
preferences?.getString(ProfileScopedKey.of(desktopNavigationLayoutKey), null)
|
||||
|
||||
actual fun saveDesktopNavigationLayout(layoutName: String) {
|
||||
preferences
|
||||
?.edit()
|
||||
?.putString(ProfileScopedKey.of(desktopNavigationLayoutKey), layoutName)
|
||||
?.apply()
|
||||
}
|
||||
|
||||
actual fun loadSelectedAppLanguage(): String? {
|
||||
val value = preferences?.getString(selectedAppLanguageKey, null)
|
||||
if (value != null) return value
|
||||
|
|
@ -94,6 +106,7 @@ actual object ThemeSettingsStorage {
|
|||
loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) }
|
||||
loadAmoledEnabled()?.let { put(amoledEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadLiquidGlassNativeTabBarEnabled()?.let { put(liquidGlassNativeTabBarEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadDesktopNavigationLayout()?.let { put(desktopNavigationLayoutKey, encodeSyncString(it)) }
|
||||
loadSelectedAppLanguage()?.let { put(selectedAppLanguageKey, encodeSyncString(it)) }
|
||||
}
|
||||
|
||||
|
|
@ -106,6 +119,7 @@ actual object ThemeSettingsStorage {
|
|||
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
|
||||
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)
|
||||
payload.decodeSyncBoolean(liquidGlassNativeTabBarEnabledKey)?.let(::saveLiquidGlassNativeTabBarEnabled)
|
||||
payload.decodeSyncString(desktopNavigationLayoutKey)?.let(::saveDesktopNavigationLayout)
|
||||
payload.decodeSyncString(selectedAppLanguageKey)?.let(::saveSelectedAppLanguage)
|
||||
applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.ENGLISH.code)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -536,6 +536,10 @@
|
|||
<string name="settings_appearance_app_language">App Language</string>
|
||||
<string name="settings_appearance_app_language_sheet_title">Choose Language</string>
|
||||
<string name="settings_appearance_continue_watching_description">Settings for the Continue Watching section.</string>
|
||||
<string name="settings_appearance_desktop_navigation">Desktop Navigation</string>
|
||||
<string name="settings_appearance_desktop_navigation_sheet_title">Choose Desktop Navigation</string>
|
||||
<string name="settings_appearance_desktop_navigation_sidebar">Sidebar</string>
|
||||
<string name="settings_appearance_desktop_navigation_top_bar">Top Bar</string>
|
||||
<string name="settings_appearance_liquid_glass">Liquid Glass</string>
|
||||
<string name="settings_appearance_liquid_glass_description">Use the native iPhone tab bar on iOS 26 and later. Instant profile switching from the tab bar is unavailable while this is on.</string>
|
||||
<string name="settings_appearance_poster_customization_description">Tune card width and corner radius.</string>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.nuvio.app
|
|||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionLayout
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
|
|
@ -11,6 +12,9 @@ import androidx.compose.animation.togetherWith
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
|
|
@ -19,15 +23,18 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
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.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -55,6 +62,7 @@ import androidx.compose.ui.layout.ContentScale
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.max
|
||||
|
|
@ -155,12 +163,15 @@ import com.nuvio.app.features.player.prepareExternalPlayerLaunch
|
|||
import com.nuvio.app.features.player.SubtitleLanguageOption
|
||||
import com.nuvio.app.features.player.sanitizePlaybackHeaders
|
||||
import com.nuvio.app.features.player.sanitizePlaybackResponseHeaders
|
||||
import com.nuvio.app.features.profiles.ActiveProfileMiniAvatar
|
||||
import com.nuvio.app.features.profiles.AvatarCatalogItem
|
||||
import com.nuvio.app.features.profiles.AvatarRepository
|
||||
import com.nuvio.app.features.profiles.NuvioProfile
|
||||
import com.nuvio.app.features.profiles.ProfileEditScreen
|
||||
import com.nuvio.app.features.profiles.ProfileRepository
|
||||
import com.nuvio.app.features.profiles.ProfileSelectionScreen
|
||||
import com.nuvio.app.features.profiles.ProfileSwitcherTab
|
||||
import com.nuvio.app.features.profiles.SidebarProfileSwitcherStack
|
||||
import com.nuvio.app.features.profiles.profileAvatarImageUrl
|
||||
import com.nuvio.app.features.search.SearchScreen
|
||||
import com.nuvio.app.features.settings.SettingsScreen
|
||||
|
|
@ -170,6 +181,7 @@ import com.nuvio.app.features.settings.ContinueWatchingSettingsScreen
|
|||
import com.nuvio.app.features.settings.AddonsSettingsScreen
|
||||
import com.nuvio.app.features.settings.PluginsSettingsScreen
|
||||
import com.nuvio.app.features.settings.AccountSettingsScreen
|
||||
import com.nuvio.app.features.settings.DesktopNavigationLayout
|
||||
import com.nuvio.app.features.settings.SupportersContributorsSettingsScreen
|
||||
import com.nuvio.app.features.settings.LicensesAttributionsSettingsScreen
|
||||
import com.nuvio.app.features.settings.ThemeSettingsRepository
|
||||
|
|
@ -329,6 +341,11 @@ enum class AppScreenTab {
|
|||
Settings,
|
||||
}
|
||||
|
||||
private val DesktopSidebarCollapsedWidth = 76.dp
|
||||
private val DesktopSidebarExpandedWidth = 184.dp
|
||||
private val DesktopSidebarExpandedContentWidth = 144.dp
|
||||
private val DesktopSidebarIconSlotSize = 36.dp
|
||||
|
||||
private fun AppScreenTab.toNativeNavigationTab(): NativeNavigationTab = when (this) {
|
||||
AppScreenTab.Home -> NativeNavigationTab.Home
|
||||
AppScreenTab.Search -> NativeNavigationTab.Search
|
||||
|
|
@ -684,6 +701,9 @@ private fun MainAppContent(
|
|||
val liquidGlassNativeTabBarEnabled by remember {
|
||||
ThemeSettingsRepository.liquidGlassNativeTabBarEnabled
|
||||
}.collectAsStateWithLifecycle()
|
||||
val desktopNavigationLayout by remember {
|
||||
ThemeSettingsRepository.desktopNavigationLayout
|
||||
}.collectAsStateWithLifecycle()
|
||||
val liquidGlassNativeTabBarSupported = remember { isLiquidGlassNativeTabBarSupported() }
|
||||
var showExitConfirmation by rememberSaveable { mutableStateOf(false) }
|
||||
var selectedPosterActionTarget by remember { mutableStateOf<PosterActionTarget?>(null) }
|
||||
|
|
@ -1350,7 +1370,12 @@ private fun MainAppContent(
|
|||
val isTabletLayout = maxWidth >= 768.dp
|
||||
val useNativeBottomTabs =
|
||||
liquidGlassNativeTabBarSupported && liquidGlassNativeTabBarEnabled && initialHomeReady
|
||||
val topChromePadding = if (isTabletLayout && !useNativeBottomTabs) {
|
||||
val useDesktopSidebar = isDesktop &&
|
||||
isTabletLayout &&
|
||||
!useNativeBottomTabs &&
|
||||
desktopNavigationLayout == DesktopNavigationLayout.Sidebar
|
||||
val useFloatingTopBar = isTabletLayout && !useNativeBottomTabs && !useDesktopSidebar
|
||||
val topChromePadding = if (useFloatingTopBar) {
|
||||
val statusBarPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
max(statusBarPadding + 24.dp, 48.dp) + 64.dp
|
||||
} else {
|
||||
|
|
@ -1421,7 +1446,8 @@ private fun MainAppContent(
|
|||
AppTabHost(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
.padding(innerPadding)
|
||||
.padding(start = if (useDesktopSidebar) DesktopSidebarCollapsedWidth else 0.dp),
|
||||
selectedTab = selectedTab,
|
||||
topChromePadding = topChromePadding,
|
||||
searchFocusRequestCount = searchFocusRequestCount,
|
||||
|
|
@ -1519,7 +1545,14 @@ private fun MainAppContent(
|
|||
)
|
||||
}
|
||||
|
||||
if (isTabletLayout && !useNativeBottomTabs) {
|
||||
if (useDesktopSidebar) {
|
||||
DesktopHoverSidebar(
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = ::handleRootTabClick,
|
||||
onProfileSelected = onProfileSelected,
|
||||
onAddProfileRequested = onSwitchProfile,
|
||||
)
|
||||
} else if (useFloatingTopBar) {
|
||||
TabletFloatingTopBar(
|
||||
selectedTab = selectedTab,
|
||||
onTabSelected = ::handleRootTabClick,
|
||||
|
|
@ -2939,6 +2972,260 @@ private fun AppTabHost(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DesktopHoverSidebar(
|
||||
selectedTab: AppScreenTab,
|
||||
onTabSelected: (AppScreenTab) -> Unit,
|
||||
onProfileSelected: (NuvioProfile) -> Unit,
|
||||
onAddProfileRequested: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
val statusBarPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
|
||||
val profileState by ProfileRepository.state.collectAsStateWithLifecycle()
|
||||
val avatars by AvatarRepository.avatars.collectAsStateWithLifecycle()
|
||||
val activeProfile = profileState.activeProfile
|
||||
val activeProfileName = activeProfile?.name ?: stringResource(Res.string.compose_nav_profile)
|
||||
val hoverSource = remember { MutableInteractionSource() }
|
||||
val hovered by hoverSource.collectIsHoveredAsState()
|
||||
var profileStackVisible by remember { mutableStateOf(false) }
|
||||
val sidebarExpanded = hovered || profileStackVisible
|
||||
val profileTopPadding = statusBarPadding + 18.dp
|
||||
fun selectTab(tab: AppScreenTab) {
|
||||
profileStackVisible = false
|
||||
onTabSelected(tab)
|
||||
}
|
||||
val sidebarWidth by animateDpAsState(
|
||||
targetValue = if (sidebarExpanded) DesktopSidebarExpandedWidth else DesktopSidebarCollapsedWidth,
|
||||
animationSpec = tween(durationMillis = 180),
|
||||
label = "desktop_sidebar_width",
|
||||
)
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.width(sidebarWidth)
|
||||
.fillMaxHeight()
|
||||
.hoverable(hoverSource)
|
||||
.zIndex(NuvioTokens.Z.navigation),
|
||||
color = tokens.colors.background,
|
||||
contentColor = tokens.colors.textPrimary,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = profileTopPadding)
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.padding(horizontal = 10.dp, vertical = 4.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = { profileStackVisible = !profileStackVisible },
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
DesktopSidebarProfileTrigger(
|
||||
profile = activeProfile,
|
||||
avatars = avatars,
|
||||
label = activeProfileName,
|
||||
expanded = sidebarExpanded,
|
||||
)
|
||||
}
|
||||
|
||||
if (profileStackVisible) {
|
||||
SidebarProfileSwitcherStack(
|
||||
onProfileSelected = onProfileSelected,
|
||||
onAddProfileRequested = onAddProfileRequested,
|
||||
onDismissRequest = { profileStackVisible = false },
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(top = profileTopPadding + 58.dp)
|
||||
.width(DesktopSidebarExpandedContentWidth),
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
DesktopSidebarItem(
|
||||
label = stringResource(Res.string.compose_nav_home),
|
||||
selected = selectedTab == AppScreenTab.Home,
|
||||
expanded = sidebarExpanded,
|
||||
onClick = { selectTab(AppScreenTab.Home) },
|
||||
) { color ->
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Home,
|
||||
contentDescription = stringResource(Res.string.compose_nav_home),
|
||||
modifier = Modifier.size(NuvioTokens.Space.s20),
|
||||
tint = color,
|
||||
)
|
||||
}
|
||||
DesktopSidebarItem(
|
||||
label = stringResource(Res.string.compose_nav_search),
|
||||
selected = selectedTab == AppScreenTab.Search,
|
||||
expanded = sidebarExpanded,
|
||||
onClick = { selectTab(AppScreenTab.Search) },
|
||||
) { color ->
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.sidebar_search),
|
||||
contentDescription = stringResource(Res.string.compose_nav_search),
|
||||
modifier = Modifier.size(NuvioTokens.Space.s20),
|
||||
tint = color,
|
||||
)
|
||||
}
|
||||
DesktopSidebarItem(
|
||||
label = stringResource(Res.string.compose_nav_library),
|
||||
selected = selectedTab == AppScreenTab.Library,
|
||||
expanded = sidebarExpanded,
|
||||
onClick = { selectTab(AppScreenTab.Library) },
|
||||
) { color ->
|
||||
Icon(
|
||||
painter = painterResource(Res.drawable.sidebar_library),
|
||||
contentDescription = stringResource(Res.string.compose_nav_library),
|
||||
modifier = Modifier.size(NuvioTokens.Space.s20),
|
||||
tint = color,
|
||||
)
|
||||
}
|
||||
DesktopSidebarItem(
|
||||
label = stringResource(Res.string.compose_settings_page_root),
|
||||
selected = selectedTab == AppScreenTab.Settings,
|
||||
expanded = sidebarExpanded,
|
||||
onClick = { selectTab(AppScreenTab.Settings) },
|
||||
) { color ->
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Settings,
|
||||
contentDescription = stringResource(Res.string.compose_settings_page_root),
|
||||
modifier = Modifier.size(NuvioTokens.Space.s20),
|
||||
tint = color,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DesktopSidebarProfileTrigger(
|
||||
profile: NuvioProfile?,
|
||||
avatars: List<AvatarCatalogItem>,
|
||||
label: String,
|
||||
expanded: Boolean,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = Color.Transparent,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 10.dp),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.width(
|
||||
if (expanded) DesktopSidebarExpandedContentWidth else DesktopSidebarIconSlotSize,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.size(DesktopSidebarIconSlotSize),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
ActiveProfileMiniAvatar(
|
||||
profile = profile,
|
||||
avatars = avatars,
|
||||
selected = false,
|
||||
size = 28,
|
||||
)
|
||||
}
|
||||
if (expanded) {
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = label,
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = tokens.colors.textPrimary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DesktopSidebarItem(
|
||||
label: String,
|
||||
selected: Boolean,
|
||||
expanded: Boolean,
|
||||
onClick: () -> Unit,
|
||||
icon: @Composable (Color) -> Unit,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
val contentColor = if (selected) tokens.colors.textPrimary else tokens.colors.textMuted
|
||||
val iconColor = if (selected) tokens.colors.onAccent else contentColor
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.padding(horizontal = 10.dp, vertical = 4.dp)
|
||||
.clickable(onClick = onClick),
|
||||
color = Color.Transparent,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 10.dp),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.width(
|
||||
if (expanded) DesktopSidebarExpandedContentWidth else DesktopSidebarIconSlotSize,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.size(DesktopSidebarIconSlotSize),
|
||||
color = if (selected) tokens.colors.accent else Color.Transparent,
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
icon(iconColor)
|
||||
}
|
||||
}
|
||||
if (expanded) {
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = label,
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = contentColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TabletFloatingTopBar(
|
||||
selectedTab: AppScreenTab,
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ fun ProfileSwitcherTab(
|
|||
onProfileSelected: (NuvioProfile) -> Unit,
|
||||
onAddProfileRequested: () -> Unit,
|
||||
triggerContent: (@Composable (selected: Boolean) -> Unit)? = null,
|
||||
openPopupOnClick: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
|
|
@ -201,7 +202,13 @@ fun ProfileSwitcherTab(
|
|||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = onClick,
|
||||
onClick = {
|
||||
if (openPopupOnClick && profiles.isNotEmpty()) {
|
||||
showPopup = true
|
||||
} else {
|
||||
onClick()
|
||||
}
|
||||
},
|
||||
)
|
||||
.pointerInput(profiles) {
|
||||
detectDragGesturesAfterLongPress(
|
||||
|
|
@ -336,6 +343,358 @@ fun ProfileSwitcherTab(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SidebarProfileSwitcherStack(
|
||||
onProfileSelected: (NuvioProfile) -> Unit,
|
||||
onAddProfileRequested: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val profileState by ProfileRepository.state.collectAsStateWithLifecycle()
|
||||
val activeProfile = profileState.activeProfile
|
||||
val profiles = profileState.profiles
|
||||
val avatars by AvatarRepository.avatars.collectAsStateWithLifecycle()
|
||||
var pinProfile by remember { mutableStateOf<NuvioProfile?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
AvatarRepository.fetchAvatars()
|
||||
AvatarRepository.refreshAvatars()
|
||||
}
|
||||
|
||||
fun chooseProfile(profile: NuvioProfile) {
|
||||
if (profile.profileIndex == activeProfile?.profileIndex) {
|
||||
onDismissRequest()
|
||||
return
|
||||
}
|
||||
if (profile.pinEnabled) {
|
||||
pinProfile = profile
|
||||
} else {
|
||||
onProfileSelected(profile)
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
profiles.forEach { profile ->
|
||||
SidebarProfileSwitcherRow(
|
||||
profile = profile,
|
||||
avatars = avatars,
|
||||
isActive = profile.profileIndex == activeProfile?.profileIndex,
|
||||
onClick = { chooseProfile(profile) },
|
||||
)
|
||||
}
|
||||
|
||||
if (profiles.size < 4) {
|
||||
SidebarAddProfileRow(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onAddProfileRequested()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = pinProfile != null,
|
||||
enter = expandVertically() + fadeIn(tween(160)),
|
||||
exit = shrinkVertically(tween(120)) + fadeOut(tween(90)),
|
||||
) {
|
||||
pinProfile?.let { profile ->
|
||||
SidebarCompactPinEntry(
|
||||
profileName = profile.name,
|
||||
onVerified = {
|
||||
onProfileSelected(profile)
|
||||
pinProfile = null
|
||||
onDismissRequest()
|
||||
},
|
||||
onCancel = { pinProfile = null },
|
||||
verifyPin = { pin ->
|
||||
ProfileRepository.verifyPin(profile.profileIndex, pin)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SidebarProfileSwitcherRow(
|
||||
profile: NuvioProfile,
|
||||
avatars: List<AvatarCatalogItem>,
|
||||
isActive: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(40.dp)
|
||||
.clip(tokens.shapes.compactCard)
|
||||
.background(
|
||||
if (isActive) {
|
||||
tokens.colors.overlaySelected
|
||||
} else {
|
||||
tokens.colors.surface.copy(alpha = 0f)
|
||||
},
|
||||
)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.size(30.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
ActiveProfileMiniAvatar(
|
||||
profile = profile,
|
||||
avatars = avatars,
|
||||
selected = isActive,
|
||||
size = 26,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = profile.name.ifBlank {
|
||||
stringResource(Res.string.profile_label_number, profile.profileIndex)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = if (isActive) tokens.colors.textPrimary else tokens.colors.textMuted,
|
||||
fontWeight = if (isActive) FontWeight.Bold else FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (profile.pinEnabled) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Lock,
|
||||
contentDescription = null,
|
||||
tint = tokens.colors.textMuted,
|
||||
modifier = Modifier.size(12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SidebarAddProfileRow(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(40.dp)
|
||||
.clip(tokens.shapes.compactCard)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.clip(tokens.shapes.avatar)
|
||||
.background(tokens.colors.surfaceCard),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Add,
|
||||
contentDescription = stringResource(Res.string.compose_profile_add_profile),
|
||||
tint = tokens.colors.textMuted,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(Res.string.compose_profile_add_profile),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = tokens.colors.textMuted,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SidebarCompactPinEntry(
|
||||
profileName: String,
|
||||
onVerified: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
verifyPin: suspend (String) -> PinVerifyResult,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
var pin by remember(profileName) { mutableStateOf("") }
|
||||
var error by remember(profileName) { mutableStateOf<String?>(null) }
|
||||
var isVerifying by remember(profileName) { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(tokens.shapes.compactCard)
|
||||
.background(tokens.colors.surfaceCard.copy(alpha = 0.72f))
|
||||
.padding(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.pin_enter_for, profileName),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = tokens.colors.textMuted,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
repeat(4) { index ->
|
||||
val filled = index < pin.length
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.clip(tokens.shapes.avatar)
|
||||
.then(
|
||||
if (filled) {
|
||||
Modifier.background(tokens.colors.accent)
|
||||
} else {
|
||||
Modifier.border(tokens.borders.thin, tokens.colors.borderDefault, tokens.shapes.avatar)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = error != null,
|
||||
enter = expandVertically() + fadeIn(),
|
||||
exit = shrinkVertically() + fadeOut(),
|
||||
) {
|
||||
Text(
|
||||
text = error.orEmpty(),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = tokens.colors.danger,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 2,
|
||||
modifier = Modifier.padding(top = 6.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
SidebarCompactPinKeypad(
|
||||
onDigit = { digit ->
|
||||
if (pin.length < 4 && !isVerifying) {
|
||||
error = null
|
||||
pin += digit
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
if (pin.length == 4) {
|
||||
isVerifying = true
|
||||
scope.launch {
|
||||
val result = verifyPin(pin)
|
||||
if (result.unlocked) {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onVerified()
|
||||
} else {
|
||||
error = if (result.retryAfterSeconds > 0) {
|
||||
getString(Res.string.pin_locked_try_again, result.retryAfterSeconds)
|
||||
} else {
|
||||
getString(Res.string.pin_incorrect)
|
||||
}
|
||||
pin = ""
|
||||
}
|
||||
isVerifying = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onBackspace = {
|
||||
if (pin.isNotEmpty() && !isVerifying) {
|
||||
pin = pin.dropLast(1)
|
||||
error = null
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(Res.string.pin_cancel),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = tokens.colors.accent,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier
|
||||
.clip(tokens.shapes.compactCard)
|
||||
.clickable(onClick = onCancel)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SidebarCompactPinKeypad(
|
||||
onDigit: (String) -> Unit,
|
||||
onBackspace: () -> Unit,
|
||||
) {
|
||||
val tokens = MaterialTheme.nuvio
|
||||
val rows = listOf(
|
||||
listOf("1", "2", "3"),
|
||||
listOf("4", "5", "6"),
|
||||
listOf("7", "8", "9"),
|
||||
listOf("", "0", "⌫"),
|
||||
)
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
rows.forEach { row ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally),
|
||||
) {
|
||||
row.forEach { key ->
|
||||
when (key) {
|
||||
"" -> Spacer(modifier = Modifier.size(30.dp))
|
||||
"⌫" -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.clip(tokens.shapes.avatar)
|
||||
.background(tokens.colors.surface)
|
||||
.clickable(onClick = onBackspace),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.Backspace,
|
||||
contentDescription = stringResource(Res.string.pin_backspace),
|
||||
tint = tokens.colors.textPrimary,
|
||||
modifier = Modifier.size(14.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.clip(tokens.shapes.avatar)
|
||||
.background(tokens.colors.surface)
|
||||
.clickable { onDigit(key) },
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = key,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = tokens.colors.textPrimary,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PopupAddProfileBubble(
|
||||
delayMs: Int,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.nuvio.app.isDesktop
|
||||
import com.nuvio.app.core.ui.AppTheme
|
||||
import com.nuvio.app.core.ui.NuvioBottomSheetActionRow
|
||||
import com.nuvio.app.core.ui.NuvioBottomSheetDivider
|
||||
|
|
@ -56,6 +58,8 @@ import nuvio.composeapp.generated.resources.settings_appearance_app_language_she
|
|||
import nuvio.composeapp.generated.resources.settings_appearance_amoled_black
|
||||
import nuvio.composeapp.generated.resources.settings_appearance_amoled_description
|
||||
import nuvio.composeapp.generated.resources.settings_appearance_continue_watching_description
|
||||
import nuvio.composeapp.generated.resources.settings_appearance_desktop_navigation
|
||||
import nuvio.composeapp.generated.resources.settings_appearance_desktop_navigation_sheet_title
|
||||
import nuvio.composeapp.generated.resources.settings_appearance_liquid_glass
|
||||
import nuvio.composeapp.generated.resources.settings_appearance_liquid_glass_description
|
||||
import nuvio.composeapp.generated.resources.settings_appearance_poster_customization_description
|
||||
|
|
@ -113,6 +117,11 @@ internal fun LazyListScope.appearanceSettingsContent(
|
|||
|
||||
item {
|
||||
var showLanguageSheet by remember { mutableStateOf(false) }
|
||||
var showDesktopNavigationSheet by remember { mutableStateOf(false) }
|
||||
val desktopNavigationLayout by remember {
|
||||
ThemeSettingsRepository.ensureLoaded()
|
||||
ThemeSettingsRepository.desktopNavigationLayout
|
||||
}.collectAsStateWithLifecycle()
|
||||
SettingsSection(
|
||||
title = stringResource(Res.string.settings_appearance_section_display),
|
||||
isTablet = isTablet,
|
||||
|
|
@ -135,6 +144,16 @@ internal fun LazyListScope.appearanceSettingsContent(
|
|||
onCheckedChange = onLiquidGlassNativeTabBarToggle,
|
||||
)
|
||||
}
|
||||
if (isDesktop) {
|
||||
SettingsGroupDivider(isTablet = isTablet)
|
||||
SettingsNavigationRow(
|
||||
title = stringResource(Res.string.settings_appearance_desktop_navigation),
|
||||
description = stringResource(desktopNavigationLayout.labelRes),
|
||||
icon = Icons.Rounded.Style,
|
||||
isTablet = isTablet,
|
||||
onClick = { showDesktopNavigationSheet = true },
|
||||
)
|
||||
}
|
||||
SettingsGroupDivider(isTablet = isTablet)
|
||||
SettingsNavigationRow(
|
||||
title = stringResource(Res.string.settings_appearance_app_language),
|
||||
|
|
@ -146,6 +165,17 @@ internal fun LazyListScope.appearanceSettingsContent(
|
|||
}
|
||||
}
|
||||
|
||||
if (showDesktopNavigationSheet) {
|
||||
DesktopNavigationLayoutBottomSheet(
|
||||
selectedLayout = desktopNavigationLayout,
|
||||
onLayoutSelected = {
|
||||
ThemeSettingsRepository.setDesktopNavigationLayout(it)
|
||||
showDesktopNavigationSheet = false
|
||||
},
|
||||
onDismiss = { showDesktopNavigationSheet = false },
|
||||
)
|
||||
}
|
||||
|
||||
if (showLanguageSheet) {
|
||||
AppearanceLanguageBottomSheet(
|
||||
selectedLanguage = selectedAppLanguage,
|
||||
|
|
@ -184,6 +214,66 @@ internal fun LazyListScope.appearanceSettingsContent(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun DesktopNavigationLayoutBottomSheet(
|
||||
selectedLayout: DesktopNavigationLayout,
|
||||
onLayoutSelected: (DesktopNavigationLayout) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
NuvioModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
coroutineScope.launch {
|
||||
dismissNuvioBottomSheet(sheetState = sheetState, onDismiss = onDismiss)
|
||||
}
|
||||
},
|
||||
sheetState = sheetState,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(Res.string.settings_appearance_desktop_navigation_sheet_title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
)
|
||||
}
|
||||
|
||||
itemsIndexed(DesktopNavigationLayout.entries) { index, layout ->
|
||||
if (index > 0) {
|
||||
NuvioBottomSheetDivider()
|
||||
}
|
||||
NuvioBottomSheetActionRow(
|
||||
title = stringResource(layout.labelRes),
|
||||
onClick = {
|
||||
onLayoutSelected(layout)
|
||||
coroutineScope.launch {
|
||||
dismissNuvioBottomSheet(sheetState = sheetState, onDismiss = onDismiss)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
if (layout == selectedLayout) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = stringResource(Res.string.cd_selected),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class AppLanguageSheetOption(
|
||||
val language: AppLanguage,
|
||||
val labelRes: StringResource,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.nuvio.app.features.settings
|
||||
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.settings_appearance_desktop_navigation_sidebar
|
||||
import nuvio.composeapp.generated.resources.settings_appearance_desktop_navigation_top_bar
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
|
||||
enum class DesktopNavigationLayout(
|
||||
val labelRes: StringResource,
|
||||
) {
|
||||
Sidebar(Res.string.settings_appearance_desktop_navigation_sidebar),
|
||||
TopBar(Res.string.settings_appearance_desktop_navigation_top_bar),
|
||||
;
|
||||
|
||||
companion object {
|
||||
val Default = Sidebar
|
||||
|
||||
fun fromName(name: String?): DesktopNavigationLayout =
|
||||
entries.firstOrNull { it.name.equals(name, ignoreCase = true) } ?: Default
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,9 @@ object ThemeSettingsRepository {
|
|||
private val _liquidGlassNativeTabBarEnabled = MutableStateFlow(false)
|
||||
val liquidGlassNativeTabBarEnabled: StateFlow<Boolean> = _liquidGlassNativeTabBarEnabled.asStateFlow()
|
||||
|
||||
private val _desktopNavigationLayout = MutableStateFlow(DesktopNavigationLayout.Default)
|
||||
val desktopNavigationLayout: StateFlow<DesktopNavigationLayout> = _desktopNavigationLayout.asStateFlow()
|
||||
|
||||
private val _selectedAppLanguage = MutableStateFlow(AppLanguage.ENGLISH)
|
||||
val selectedAppLanguage: StateFlow<AppLanguage> = _selectedAppLanguage.asStateFlow()
|
||||
|
||||
|
|
@ -36,6 +39,7 @@ object ThemeSettingsRepository {
|
|||
_selectedTheme.value = AppTheme.WHITE
|
||||
_amoledEnabled.value = false
|
||||
_liquidGlassNativeTabBarEnabled.value = false
|
||||
_desktopNavigationLayout.value = DesktopNavigationLayout.Default
|
||||
NativeTabBridge.publishAccentColor(AppTheme.WHITE.nativeTabAccentHex())
|
||||
NativeTabBridge.publishLiquidGlassEnabled(false)
|
||||
_selectedAppLanguage.value = AppLanguage.ENGLISH
|
||||
|
|
@ -59,6 +63,9 @@ object ThemeSettingsRepository {
|
|||
val liquidGlassEnabled = ThemeSettingsStorage.loadLiquidGlassNativeTabBarEnabled() ?: false
|
||||
_liquidGlassNativeTabBarEnabled.value = liquidGlassEnabled
|
||||
NativeTabBridge.publishLiquidGlassEnabled(liquidGlassEnabled)
|
||||
_desktopNavigationLayout.value = DesktopNavigationLayout.fromName(
|
||||
ThemeSettingsStorage.loadDesktopNavigationLayout(),
|
||||
)
|
||||
val appLanguage = AppLanguage.fromCode(ThemeSettingsStorage.loadSelectedAppLanguage())
|
||||
ThemeSettingsStorage.applySelectedAppLanguage(appLanguage.code)
|
||||
_selectedAppLanguage.value = appLanguage
|
||||
|
|
@ -87,6 +94,13 @@ object ThemeSettingsRepository {
|
|||
NativeTabBridge.publishLiquidGlassEnabled(enabled)
|
||||
}
|
||||
|
||||
fun setDesktopNavigationLayout(layout: DesktopNavigationLayout) {
|
||||
ensureLoaded()
|
||||
if (_desktopNavigationLayout.value == layout) return
|
||||
_desktopNavigationLayout.value = layout
|
||||
ThemeSettingsStorage.saveDesktopNavigationLayout(layout.name)
|
||||
}
|
||||
|
||||
fun setAppLanguage(language: AppLanguage) {
|
||||
ensureLoaded()
|
||||
if (_selectedAppLanguage.value == language) return
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ internal expect object ThemeSettingsStorage {
|
|||
fun saveAmoledEnabled(enabled: Boolean)
|
||||
fun loadLiquidGlassNativeTabBarEnabled(): Boolean?
|
||||
fun saveLiquidGlassNativeTabBarEnabled(enabled: Boolean)
|
||||
fun loadDesktopNavigationLayout(): String?
|
||||
fun saveDesktopNavigationLayout(layoutName: String)
|
||||
fun loadSelectedAppLanguage(): String?
|
||||
fun saveSelectedAppLanguage(languageCode: String)
|
||||
fun applySelectedAppLanguage(languageCode: String)
|
||||
|
|
|
|||
|
|
@ -15,11 +15,13 @@ internal actual object ThemeSettingsStorage {
|
|||
private const val selectedThemeKey = "selected_theme"
|
||||
private const val amoledEnabledKey = "amoled_enabled"
|
||||
private const val liquidGlassNativeTabBarEnabledKey = "liquid_glass_native_tab_bar_enabled"
|
||||
private const val desktopNavigationLayoutKey = "desktop_navigation_layout"
|
||||
private const val selectedAppLanguageKey = "selected_app_language"
|
||||
private val profileScopedSyncKeys = listOf(
|
||||
selectedThemeKey,
|
||||
amoledEnabledKey,
|
||||
liquidGlassNativeTabBarEnabledKey,
|
||||
desktopNavigationLayoutKey,
|
||||
)
|
||||
private val store = DesktopStorage.store("nuvio_theme_settings")
|
||||
|
||||
|
|
@ -44,6 +46,13 @@ internal actual object ThemeSettingsStorage {
|
|||
store.putBoolean(ProfileScopedKey.of(liquidGlassNativeTabBarEnabledKey), enabled)
|
||||
}
|
||||
|
||||
actual fun loadDesktopNavigationLayout(): String? =
|
||||
store.getString(ProfileScopedKey.of(desktopNavigationLayoutKey))
|
||||
|
||||
actual fun saveDesktopNavigationLayout(layoutName: String) {
|
||||
store.putString(ProfileScopedKey.of(desktopNavigationLayoutKey), layoutName)
|
||||
}
|
||||
|
||||
actual fun loadSelectedAppLanguage(): String? =
|
||||
store.getString(selectedAppLanguageKey)
|
||||
?: Locale.getDefault().toLanguageTag().takeIf { it.isNotBlank() }
|
||||
|
|
@ -60,6 +69,7 @@ internal actual object ThemeSettingsStorage {
|
|||
loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) }
|
||||
loadAmoledEnabled()?.let { put(amoledEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadLiquidGlassNativeTabBarEnabled()?.let { put(liquidGlassNativeTabBarEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadDesktopNavigationLayout()?.let { put(desktopNavigationLayoutKey, encodeSyncString(it)) }
|
||||
loadSelectedAppLanguage()?.let { put(selectedAppLanguageKey, encodeSyncString(it)) }
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +78,7 @@ internal actual object ThemeSettingsStorage {
|
|||
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
|
||||
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)
|
||||
payload.decodeSyncBoolean(liquidGlassNativeTabBarEnabledKey)?.let(::saveLiquidGlassNativeTabBarEnabled)
|
||||
payload.decodeSyncString(desktopNavigationLayoutKey)?.let(::saveDesktopNavigationLayout)
|
||||
payload.decodeSyncString(selectedAppLanguageKey)?.let(::saveSelectedAppLanguage)
|
||||
applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.ENGLISH.code)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,13 @@ actual object ThemeSettingsStorage {
|
|||
private const val selectedThemeKey = "selected_theme"
|
||||
private const val amoledEnabledKey = "amoled_enabled"
|
||||
private const val liquidGlassNativeTabBarEnabledKey = "liquid_glass_native_tab_bar_enabled"
|
||||
private const val desktopNavigationLayoutKey = "desktop_navigation_layout"
|
||||
private const val selectedAppLanguageKey = "selected_app_language"
|
||||
private val profileScopedSyncKeys = listOf(
|
||||
selectedThemeKey,
|
||||
amoledEnabledKey,
|
||||
liquidGlassNativeTabBarEnabledKey,
|
||||
desktopNavigationLayoutKey,
|
||||
)
|
||||
private val globalSyncKeys = listOf(selectedAppLanguageKey)
|
||||
|
||||
|
|
@ -60,6 +62,16 @@ actual object ThemeSettingsStorage {
|
|||
)
|
||||
}
|
||||
|
||||
actual fun loadDesktopNavigationLayout(): String? =
|
||||
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(desktopNavigationLayoutKey))
|
||||
|
||||
actual fun saveDesktopNavigationLayout(layoutName: String) {
|
||||
NSUserDefaults.standardUserDefaults.setObject(
|
||||
layoutName,
|
||||
forKey = ProfileScopedKey.of(desktopNavigationLayoutKey),
|
||||
)
|
||||
}
|
||||
|
||||
actual fun loadSelectedAppLanguage(): String? {
|
||||
val value = NSUserDefaults.standardUserDefaults.stringForKey(selectedAppLanguageKey)
|
||||
if (value != null) return value
|
||||
|
|
@ -88,6 +100,7 @@ actual object ThemeSettingsStorage {
|
|||
loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) }
|
||||
loadAmoledEnabled()?.let { put(amoledEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadLiquidGlassNativeTabBarEnabled()?.let { put(liquidGlassNativeTabBarEnabledKey, encodeSyncBoolean(it)) }
|
||||
loadDesktopNavigationLayout()?.let { put(desktopNavigationLayoutKey, encodeSyncString(it)) }
|
||||
loadSelectedAppLanguage()?.let { put(selectedAppLanguageKey, encodeSyncString(it)) }
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +115,7 @@ actual object ThemeSettingsStorage {
|
|||
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
|
||||
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)
|
||||
payload.decodeSyncBoolean(liquidGlassNativeTabBarEnabledKey)?.let(::saveLiquidGlassNativeTabBarEnabled)
|
||||
payload.decodeSyncString(desktopNavigationLayoutKey)?.let(::saveDesktopNavigationLayout)
|
||||
payload.decodeSyncString(selectedAppLanguageKey)?.let(::saveSelectedAppLanguage)
|
||||
applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.ENGLISH.code)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue