From 84cc669bd5655c58749bce1d37b8db2e06e24fe4 Mon Sep 17 00:00:00 2001
From: tapframe <85391825+tapframe@users.noreply.github.com>
Date: Tue, 9 Jun 2026 23:10:01 +0530
Subject: [PATCH] feat: sidebar for desktop
---
.../settings/ThemeSettingsStorage.android.kt | 14 +
.../composeResources/values/strings.xml | 4 +
.../commonMain/kotlin/com/nuvio/app/App.kt | 293 +++++++++++++-
.../features/profiles/ProfileSwitcherTab.kt | 361 +++++++++++++++++-
.../settings/AppearanceSettingsPage.kt | 90 +++++
.../settings/DesktopNavigationLayout.kt | 21 +
.../settings/ThemeSettingsRepository.kt | 14 +
.../features/settings/ThemeSettingsStorage.kt | 2 +
.../settings/ThemeSettingsStorage.desktop.kt | 11 +
.../settings/ThemeSettingsStorage.ios.kt | 14 +
10 files changed, 820 insertions(+), 4 deletions(-)
create mode 100644 composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/DesktopNavigationLayout.kt
diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.android.kt
index e082a536..b4a6e5c9 100644
--- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.android.kt
+++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.android.kt
@@ -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)
}
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index af2b860e..48dfbdd0 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -536,6 +536,10 @@
App Language
Choose Language
Settings for the Continue Watching section.
+ Desktop Navigation
+ Choose Desktop Navigation
+ Sidebar
+ Top Bar
Liquid Glass
Use the native iPhone tab bar on iOS 26 and later. Instant profile switching from the tab bar is unavailable while this is on.
Tune card width and corner radius.
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt
index 6784c276..babed0ce 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt
@@ -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(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,
+ 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,
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt
index baf082f2..81545f14 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt
@@ -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(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,
+ 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(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,
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt
index abbf58e7..39130eb0 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt
@@ -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,
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/DesktopNavigationLayout.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/DesktopNavigationLayout.kt
new file mode 100644
index 00000000..507a8ea5
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/DesktopNavigationLayout.kt
@@ -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
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsRepository.kt
index 431b9d20..956dc217 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsRepository.kt
@@ -17,6 +17,9 @@ object ThemeSettingsRepository {
private val _liquidGlassNativeTabBarEnabled = MutableStateFlow(false)
val liquidGlassNativeTabBarEnabled: StateFlow = _liquidGlassNativeTabBarEnabled.asStateFlow()
+ private val _desktopNavigationLayout = MutableStateFlow(DesktopNavigationLayout.Default)
+ val desktopNavigationLayout: StateFlow = _desktopNavigationLayout.asStateFlow()
+
private val _selectedAppLanguage = MutableStateFlow(AppLanguage.ENGLISH)
val selectedAppLanguage: StateFlow = _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
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.kt
index 2a788baf..5bb6a284 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.kt
@@ -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)
diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.desktop.kt
index 77d3d4ec..68891f4a 100644
--- a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.desktop.kt
+++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.desktop.kt
@@ -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)
}
diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.ios.kt
index f66f8b8c..dcef54f8 100644
--- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.ios.kt
+++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.ios.kt
@@ -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)
}