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 07954478b..bb27c4ec2 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
@@ -19,10 +19,12 @@ actual object ThemeSettingsStorage {
private const val amoledEnabledKey = "amoled_enabled"
private const val liquidGlassNativeTabBarEnabledKey = "liquid_glass_native_tab_bar_enabled"
private const val selectedAppLanguageKey = "selected_app_language"
+ private const val NAV_BAR_STYLE_KEY = "nav_bar_style"
private val profileScopedSyncKeys = listOf(
selectedThemeKey,
amoledEnabledKey,
liquidGlassNativeTabBarEnabledKey,
+ NAV_BAR_STYLE_KEY,
)
private var preferences: SharedPreferences? = null
@@ -93,10 +95,21 @@ actual object ThemeSettingsStorage {
}
}
+ actual fun loadNavBarStyle(): String? =
+ preferences?.getString(ProfileScopedKey.of(NAV_BAR_STYLE_KEY), null)
+
+ actual fun saveNavBarStyle(styleKey: String) {
+ preferences
+ ?.edit()
+ ?.putString(ProfileScopedKey.of(NAV_BAR_STYLE_KEY), styleKey)
+ ?.apply()
+ }
+
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) }
loadAmoledEnabled()?.let { put(amoledEnabledKey, encodeSyncBoolean(it)) }
loadLiquidGlassNativeTabBarEnabled()?.let { put(liquidGlassNativeTabBarEnabledKey, encodeSyncBoolean(it)) }
+ loadNavBarStyle()?.let { put(NAV_BAR_STYLE_KEY, encodeSyncString(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
@@ -107,6 +120,7 @@ actual object ThemeSettingsStorage {
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)
payload.decodeSyncBoolean(liquidGlassNativeTabBarEnabledKey)?.let(::saveLiquidGlassNativeTabBarEnabled)
+ payload.decodeSyncString(NAV_BAR_STYLE_KEY)?.let(::saveNavBarStyle)
applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.DEVICE.code)
}
}
diff --git a/composeApp/src/commonMain/composeResources/values-pl/strings.xml b/composeApp/src/commonMain/composeResources/values-pl/strings.xml
index 473e6a21a..012ba9ae2 100644
--- a/composeApp/src/commonMain/composeResources/values-pl/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-pl/strings.xml
@@ -1879,4 +1879,111 @@
Błąd API GitHub releases: %1$d
Nie opublikowano jeszcze żadnej aktualizacji.
Wydanie nie ma tagu ani nazwy
+ Przełącz
+ Rejestrując się, akceptuję
+ Regulamin
+ DIAGNOSTYKA
+ Raporty awarii Sentry
+ Wysyłaj raporty awarii i ANR z bezpiecznym kontekstem. Domyślnie włączone.
+ Włączyć raporty awarii?
+ Raporty awarii pomagają identyfikować problemy specyficzne dla urządzenia bez konieczności reprodukcji błędu z logami ADB.
+ Wyłączyć raporty awarii?
+ Przyszłe awarie i bieżące ANR z tego urządzenia nie będą zgłaszane. Może to znacznie utrudnić naprawę błędów specyficznych dla urządzenia.
+ Jak to pomaga
+ Raporty są grupowane według wersji aplikacji, modelu urządzenia, wersji Androida i śladu stosu, dzięki czemu najczęstsze awarie mogą być naprawiane w pierwszej kolejności.
+ Wysyłane
+ Awarie, bieżące ANR, ślady stosu, wersja aplikacji, typ kompilacji, wariant, metadane urządzenia i systemu operacyjnego oraz bezpieczne breadcrumby sieciowe z metodą, hostem, ścieżką, statusem, czasem trwania i typem wyjątku.
+ Nie wysyłane
+ Hasła, tokeny dostępu, tokeny odświeżania, treści żądań, treści odpowiedzi, nagłówki, ciasteczka, zrzuty ekranu, hierarchia widoków, odtwarzanie sesji, surowa diagnostyka oraz wartości zapytania lub fragmentu URL strumienia.
+ Pozostaw włączone
+ Wyłącz
+ Włącz
+ Otwórz folder pobranych
+ Nie udało się otworzyć folderu pobranych
+ Pasek nawigacji
+ Styl paska nawigacji
+ Adaptacyjny
+ Zawsze rozwinięty
+ Zawsze kompaktowy
+ Klasyczny
+ Efekt głębi karty
+ Dodaje podświetloną górną krawędź i delikatny połysk do kart graficznych, tworząc subtelne wrażenie głębi.
+ Włącz efekt głębi
+ Blask krawędzi
+ Subtelny
+ Zrównoważony
+ Wyrazisty
+ Górny połysk
+ Wył.
+ Miękki
+ Jasny
+ Zastosuj do
+ Dostrojenie
+ Dostrojenie głębi
+ Przeciągnij punkt w prawo dla jaśniejszej krawędzi, w górę dla większego połysku. Użyj suwaka, aby rozszerzyć blask na cały kontur.
+ Blask krawędzi →
+ ↑ Połysk
+ Blask krawędzi
+ Połysk
+ Pokrycie krawędzi
+ Tylko góra
+ Połowa
+ Pełny kontur
+ Pokrycie krawędzi
+ Tytuł odcinka
+ S1 · E1 · 45 min
+ Plakaty
+ Kontynuuj oglądanie
+ Karty odcinków
+ Obsada
+ Zwiastuny
+ Tryb tła
+ Wybierz sposób wyświetlania grafiki za stronami metadanych.
+ Normalny
+ Użyj standardowego tła aplikacji.
+ Kinowe
+ Pokaż miękkie rozmyte tło za stroną.
+ Kolor dominujący
+ Dopasuj tło strony do głównego koloru z grafiki tła.
+ Serwer i utrzymanie w tym miesiącu
+ Pokryte. Dodatkowe wsparcie trafia teraz na rozwój.
+ Po 100% dodatkowe wsparcie trafia na rozwój.
+ Ładowanie postępu finansowania...
+ Wysyłaj znaczniki intro i outro
+ Przekaż rozwiązane znaczniki pomijania intro i outro do zewnętrznego odtwarzacza w celu automatycznego pomijania. Działa tylko w odtwarzaczach, które to obsługują; inne odtwarzacze to ignorują.
+ Silnik odtwarzania
+ Użyj najpierw ExoPlayera i przejdź na libmpv, jeśli odtwarzanie się nie powiedzie.
+ Użyj ExoPlayera i dekoderów Android Media3.
+ Użyj libmpv do odtwarzania na Androidzie.
+ Renderer libmpv
+ Renderer libmpv
+ Dekodowanie sprzętowe libmpv
+ Używaj dekodowania sprzętowego mpv, gdy jest dostępne.
+ Kompatybilność YUV420P libmpv
+ Wymuś wyjście YUV420P dla urządzeń z problemami renderera lub kolorów.
+ Ostrzeżenia dotyczące treści
+ Pokaż nakładkę z informacjami o treści przy rozpoczęciu odtwarzania.
+ Ładowanie segmentów do pominięcia…
+ Test banera zakończony
+ Podgląd banera i symulowanego postępu pobierania.
+ Testuj baner aktualizacji
+ Nie podano informacji o wydaniu.
+ ID kolekcji „%1$s" jest użyte więcej niż raz.
+ ID folderu „%1$s" jest użyte więcej niż raz w „%2$s".
+ Urodzony
+ Miejsce urodzenia
+ Filmografia
+ Biografia
+ Kraj
+ Typ
+ Katalog
+ O
+ Ładowanie napisów z dodatków...
+ Pobieranie napisów...
+
+ - %d tytuł
+ - %d tytuły
+ - %d tytułów
+ - %d tytułów
+
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index e02d10ec7..e120b6c0c 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -567,6 +567,12 @@
App Language
Device language
Choose Language
+ Navigation Bar
+ Navigation Bar Style
+ Adaptive
+ Always Expanded
+ Always Compact
+ Classic
Settings for the Continue Watching section.
Liquid Glass
Use the native iPhone tab bar on iOS 26 and later.
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt
index 94d278da5..ebdba5e9d 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt
@@ -59,6 +59,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
@@ -93,7 +94,11 @@ import com.nuvio.app.core.sync.ProfileSettingsSync
import com.nuvio.app.core.sync.RealtimeSyncConfig
import com.nuvio.app.core.sync.RealtimeSyncInvalidationService
import com.nuvio.app.core.sync.SyncManager
+import com.nuvio.app.core.ui.LocalNuvioNavBarScrollState
import com.nuvio.app.core.ui.NuvioNavigationBar
+import com.nuvio.app.core.ui.NuvioClassicNavigationBar
+import com.nuvio.app.core.ui.NuvioNavBarScrollState
+import com.nuvio.app.core.ui.rememberNuvioNavBarScrollState
import com.nuvio.app.core.format.formatReleaseDateForDisplay
import com.nuvio.app.core.ui.NuvioContinueWatchingActionSheet
import com.nuvio.app.core.ui.NuvioPosterZoomActionOverlay
@@ -190,6 +195,7 @@ import com.nuvio.app.features.settings.PluginsSettingsScreen
import com.nuvio.app.features.settings.AccountSettingsScreen
import com.nuvio.app.features.settings.SupportersContributorsSettingsScreen
import com.nuvio.app.features.settings.LicensesAttributionsSettingsScreen
+import com.nuvio.app.features.settings.NavBarStyle
import com.nuvio.app.features.settings.ThemeSettingsRepository
import com.nuvio.app.features.collection.CollectionManagementScreen
import com.nuvio.app.features.collection.CollectionEditorScreen
@@ -1834,6 +1840,9 @@ private fun MainAppContent(
liquidGlassNativeTabBarSupported && liquidGlassNativeTabBarEnabled && initialHomeReady
}
val tabsRouteActive = currentRoute is TabsRoute
+ val navBarScrollState = rememberNuvioNavBarScrollState()
+ val navBarHazeState = rememberHazeState()
+ val navBarStyleSetting by remember { ThemeSettingsRepository.navBarStyle }.collectAsStateWithLifecycle()
val onProfileSelected: (NuvioProfile) -> Unit = { profile ->
profileSwitchLoading = true
NativeTabBridge.publishTabBarVisible(false)
@@ -1849,8 +1858,8 @@ private fun MainAppContent(
containerColor = Color.Transparent,
contentWindowInsets = WindowInsets(0),
bottomBar = {
- if (!isTabletLayout && !useNativeBottomTabs) {
- NuvioNavigationBar {
+ if (!isTabletLayout && !useNativeBottomTabs && navBarStyleSetting == NavBarStyle.CLASSIC) {
+ NuvioClassicNavigationBar {
NavItem(
selected = selectedTab == AppScreenTab.Home,
onClick = { handleRootTabClick(AppScreenTab.Home) },
@@ -1886,11 +1895,14 @@ private fun MainAppContent(
) { innerPadding ->
Box(modifier = Modifier.fillMaxSize()) {
CompositionLocalProvider(
- LocalNuvioBottomNavigationOverlayPadding provides if (useNativeBottomTabs) 49.dp else 0.dp,
+ LocalNuvioBottomNavigationOverlayPadding provides if (useNativeBottomTabs) 49.dp else if (!isTabletLayout && navBarStyleSetting != NavBarStyle.CLASSIC) 72.dp else 0.dp,
+ LocalNuvioNavBarScrollState provides navBarScrollState,
) {
AppTabHost(
modifier = Modifier
.fillMaxSize()
+ .then(if (navBarStyleSetting != NavBarStyle.CLASSIC) Modifier.hazeSource(state = navBarHazeState) else Modifier)
+ .then(if (navBarStyleSetting == NavBarStyle.ADAPTIVE) Modifier.nestedScroll(navBarScrollState.nestedScrollConnection) else Modifier)
.padding(innerPadding),
selectedTab = selectedTab,
searchFocusRequestCount = searchFocusRequestCount,
@@ -2033,6 +2045,55 @@ private fun MainAppContent(
onAddProfileRequested = onSwitchProfile,
)
}
+
+ // Floating pill navigation bar overlay
+ if (!isTabletLayout && !useNativeBottomTabs && navBarStyleSetting != NavBarStyle.CLASSIC) {
+ // Force expand/collapse for non-adaptive modes
+ when (navBarStyleSetting) {
+ NavBarStyle.EXPANDED -> navBarScrollState.expand()
+ NavBarStyle.COMPACT -> navBarScrollState.collapse()
+ else -> {} // ADAPTIVE — scroll controls it
+ }
+ NuvioNavigationBar(
+ modifier = Modifier.align(Alignment.BottomCenter),
+ scrollState = navBarScrollState,
+ hazeState = navBarHazeState,
+ ) {
+ NavItem(
+ selected = selectedTab == AppScreenTab.Home,
+ onClick = { handleRootTabClick(AppScreenTab.Home) },
+ icon = Icons.Filled.Home,
+ contentDescription = stringResource(Res.string.compose_nav_home),
+ label = stringResource(Res.string.compose_nav_home),
+ )
+ NavItem(
+ selected = selectedTab == AppScreenTab.Search,
+ onClick = { handleRootTabClick(AppScreenTab.Search) },
+ icon = Res.drawable.sidebar_search,
+ contentDescription = stringResource(Res.string.compose_nav_search),
+ label = stringResource(Res.string.compose_nav_search),
+ )
+ NavItem(
+ selected = selectedTab == AppScreenTab.Library,
+ onClick = { handleRootTabClick(AppScreenTab.Library) },
+ icon = Res.drawable.sidebar_library,
+ contentDescription = stringResource(Res.string.compose_nav_library),
+ label = stringResource(Res.string.compose_nav_library),
+ )
+ NavItem(
+ selected = selectedTab == AppScreenTab.Settings,
+ onClick = { handleRootTabClick(AppScreenTab.Settings) },
+ label = stringResource(Res.string.compose_nav_profile),
+ ) {
+ ProfileSwitcherTab(
+ selected = selectedTab == AppScreenTab.Settings,
+ onClick = { handleRootTabClick(AppScreenTab.Settings) },
+ onProfileSelected = onProfileSelected,
+ onAddProfileRequested = onSwitchProfile,
+ )
+ }
+ }
+ }
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NavigationBar.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NavigationBar.kt
index 2d3eb4b4a..f5a452e0e 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NavigationBar.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NavigationBar.kt
@@ -1,48 +1,176 @@
package com.nuvio.app.core.ui
import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+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.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.asPaddingValues
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.widthIn
import androidx.compose.foundation.selection.selectable
-import androidx.compose.material3.HorizontalDivider
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import dev.chrisbanes.haze.HazeState
+import dev.chrisbanes.haze.hazeEffect
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.painterResource
+/**
+ * Scroll-aware state for the floating navigation bar.
+ * Tracks scroll direction and exposes a label visibility fraction (1 = fully visible, 0 = hidden).
+ */
+@Stable
+class NuvioNavBarScrollState {
+ /** 1f = labels fully visible (expanded), 0f = labels hidden (collapsed, icons only) */
+ var labelVisibility by mutableFloatStateOf(1f)
+ private set
+
+ private var accumulatedDelta = 0f
+
+ /** Call to expand (show labels) – e.g. when user scrolls back to top */
+ fun expand() {
+ labelVisibility = 1f
+ accumulatedDelta = 0f
+ }
+
+ /** Call to collapse (hide labels) */
+ fun collapse() {
+ labelVisibility = 0f
+ accumulatedDelta = 0f
+ }
+
+ val nestedScrollConnection: NestedScrollConnection = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ val deltaY = available.y
+ if (deltaY == 0f) return Offset.Zero
+
+ accumulatedDelta += deltaY
+
+ if (accumulatedDelta < -SCROLL_THRESHOLD && labelVisibility != 0f) {
+ // Scrolling down past threshold → snap collapse
+ labelVisibility = 0f
+ accumulatedDelta = 0f
+ } else if (accumulatedDelta > SCROLL_THRESHOLD && labelVisibility != 1f) {
+ // Scrolling up past threshold → snap expand
+ labelVisibility = 1f
+ accumulatedDelta = 0f
+ }
+
+ // Reset accumulator if direction changed
+ if (deltaY < 0f && accumulatedDelta > 0f) accumulatedDelta = deltaY
+ if (deltaY > 0f && accumulatedDelta < 0f) accumulatedDelta = deltaY
+
+ return Offset.Zero // Don't consume any scroll
+ }
+ }
+
+ companion object {
+ private const val SCROLL_THRESHOLD = 60f
+ }
+}
+
+@Composable
+fun rememberNuvioNavBarScrollState(): NuvioNavBarScrollState {
+ return androidx.compose.runtime.remember { NuvioNavBarScrollState() }
+}
+
+/**
+ * Floating pill-shaped navigation bar with scroll-responsive labels.
+ *
+ * @param hazeState Optional [HazeState] whose source is placed on the content behind this bar.
+ * When provided, the pill gets a blur-through effect.
+ */
@Composable
fun NuvioNavigationBar(
modifier: Modifier = Modifier,
+ scrollState: NuvioNavBarScrollState? = null,
+ hazeState: HazeState? = null,
content: @Composable NuvioNavigationBarScope.() -> Unit,
) {
- val tokens = MaterialTheme.nuvio
- Column(modifier.fillMaxWidth()) {
- HorizontalDivider(
- thickness = tokens.borders.hairline,
- color = tokens.colors.borderDefault,
- )
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(nuvioBottomNavigationBarInsets().asPaddingValues())
- .padding(horizontal = NuvioTokens.Space.s4, vertical = nuvioBottomNavigationExtraVerticalPadding),
- horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap, Alignment.CenterHorizontally),
- ) {
- NuvioNavigationBarScopeImpl(this).content()
+ val labelFraction by animateFloatAsState(
+ targetValue = scrollState?.labelVisibility ?: 1f,
+ animationSpec = tween(
+ durationMillis = NuvioTokens.Motion.sheetEnterMillis,
+ easing = NuvioTokens.Motion.standard,
+ ),
+ label = "nav_label_alpha",
+ )
+
+ val navigationBarInsets = nuvioBottomNavigationBarInsets()
+ val bottomSafePadding = navigationBarInsets.asPaddingValues().calculateBottomPadding()
+
+ // Dynamic horizontal padding: pill shrinks when labels are hidden — driven by same labelFraction
+ val expandedHorizontalPadding = 28.dp
+ val collapsedHorizontalPadding = 58.dp
+ val horizontalPadding = expandedHorizontalPadding + (collapsedHorizontalPadding - expandedHorizontalPadding) * (1f - labelFraction)
+
+ // Outer container — no background, just safe padding
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(bottom = bottomSafePadding + nuvioBottomNavigationExtraVerticalPadding + NuvioTokens.Space.s8),
+ contentAlignment = Alignment.BottomCenter,
+ ) {
+ // The floating pill
+ val pillModifier = Modifier
+ .padding(horizontal = horizontalPadding)
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(NuvioTokens.Radius.full))
+ .then(
+ if (hazeState != null) {
+ Modifier.hazeEffect(state = hazeState) {
+ blurRadius = 24.dp
+ }
+ } else {
+ Modifier
+ },
+ )
+ .background(Color(0xFF1C1C1E).copy(alpha = if (hazeState != null) 0.55f else 0.82f))
+
+ Box(modifier = pillModifier) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ horizontal = NuvioTokens.Space.s6,
+ vertical = NuvioTokens.Space.s4,
+ ),
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ NuvioNavigationBarScopeImpl(
+ rowScope = this,
+ labelFraction = labelFraction,
+ ).content()
+ }
}
}
}
@@ -55,6 +183,7 @@ interface NuvioNavigationBarScope {
icon: ImageVector,
contentDescription: String?,
modifier: Modifier = Modifier,
+ label: String? = null,
)
@Composable
@@ -64,6 +193,7 @@ interface NuvioNavigationBarScope {
icon: DrawableResource,
contentDescription: String?,
modifier: Modifier = Modifier,
+ label: String? = null,
)
@Composable
@@ -71,12 +201,208 @@ interface NuvioNavigationBarScope {
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
+ label: String? = null,
content: @Composable () -> Unit,
)
}
private class NuvioNavigationBarScopeImpl(
private val rowScope: androidx.compose.foundation.layout.RowScope,
+ private val labelFraction: Float,
+) : NuvioNavigationBarScope {
+
+ @Composable
+ override fun NavItem(
+ selected: Boolean,
+ onClick: () -> Unit,
+ icon: ImageVector,
+ contentDescription: String?,
+ modifier: Modifier,
+ label: String?,
+ ) {
+ val tokens = MaterialTheme.nuvio
+ val iconColor by animateColorAsState(
+ targetValue = if (selected) tokens.colors.accent else tokens.colors.textMuted,
+ label = "nav_icon_color",
+ )
+ // Selected item gets a pill-shaped highlight using accent at low opacity
+ val selectedBgColor by animateColorAsState(
+ targetValue = if (selected) tokens.colors.accent.copy(alpha = NuvioTokens.Opacity.selected)
+ else Color.Transparent,
+ label = "nav_bg_color",
+ )
+
+ with(rowScope) {
+ Column(
+ modifier = modifier
+ .weight(1f)
+ .clip(RoundedCornerShape(NuvioTokens.Radius.full))
+ .background(selectedBgColor)
+ .selectable(
+ selected = selected,
+ enabled = true,
+ role = Role.Tab,
+ onClick = onClick,
+ )
+ .padding(vertical = NuvioTokens.Space.s6),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Icon(
+ modifier = Modifier.size(28.dp),
+ imageVector = icon,
+ contentDescription = contentDescription,
+ tint = iconColor,
+ )
+ NavItemLabel(label = label, labelFraction = labelFraction, iconColor = iconColor, selected = selected)
+ }
+ }
+ }
+
+ @Composable
+ override fun NavItem(
+ selected: Boolean,
+ onClick: () -> Unit,
+ icon: DrawableResource,
+ contentDescription: String?,
+ modifier: Modifier,
+ label: String?,
+ ) {
+ val tokens = MaterialTheme.nuvio
+ val iconColor by animateColorAsState(
+ targetValue = if (selected) tokens.colors.accent else tokens.colors.textMuted,
+ label = "nav_icon_color",
+ )
+ val selectedBgColor by animateColorAsState(
+ targetValue = if (selected) tokens.colors.accent.copy(alpha = NuvioTokens.Opacity.selected)
+ else Color.Transparent,
+ label = "nav_bg_color",
+ )
+
+ with(rowScope) {
+ Column(
+ modifier = modifier
+ .weight(1f)
+ .clip(RoundedCornerShape(NuvioTokens.Radius.full))
+ .background(selectedBgColor)
+ .selectable(
+ selected = selected,
+ enabled = true,
+ role = Role.Tab,
+ onClick = onClick,
+ )
+ .padding(vertical = NuvioTokens.Space.s6),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Icon(
+ modifier = Modifier.size(28.dp),
+ painter = painterResource(icon),
+ contentDescription = contentDescription,
+ tint = iconColor,
+ )
+ NavItemLabel(label = label, labelFraction = labelFraction, iconColor = iconColor, selected = selected)
+ }
+ }
+ }
+
+ @Composable
+ override fun NavItem(
+ selected: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier,
+ label: String?,
+ content: @Composable () -> Unit,
+ ) {
+ val tokens = MaterialTheme.nuvio
+ val selectedBgColor by animateColorAsState(
+ targetValue = if (selected) tokens.colors.accent.copy(alpha = NuvioTokens.Opacity.selected)
+ else Color.Transparent,
+ label = "nav_bg_color",
+ )
+ val iconColor by animateColorAsState(
+ targetValue = if (selected) tokens.colors.accent else tokens.colors.textMuted,
+ label = "nav_icon_color",
+ )
+
+ with(rowScope) {
+ Column(
+ modifier = modifier
+ .weight(1f)
+ .clip(RoundedCornerShape(NuvioTokens.Radius.full))
+ .background(selectedBgColor)
+ .selectable(
+ selected = selected,
+ enabled = true,
+ role = Role.Tab,
+ onClick = onClick,
+ )
+ .padding(vertical = NuvioTokens.Space.s6),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ content()
+ NavItemLabel(label = label, labelFraction = labelFraction, iconColor = iconColor, selected = selected)
+ }
+ }
+ }
+}
+
+@Composable
+private fun NavItemLabel(
+ label: String?,
+ labelFraction: Float,
+ iconColor: Color,
+ selected: Boolean,
+) {
+ if (label == null || labelFraction <= 0f) return
+ Spacer(modifier = Modifier.height(NuvioTokens.Space.s3 * labelFraction))
+ Box(
+ modifier = Modifier
+ .height(NuvioTokens.Space.s14 * labelFraction)
+ .alpha(labelFraction),
+ ) {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.labelSmall.copy(
+ fontSize = NuvioTokens.Type.labelXs,
+ lineHeight = NuvioTokens.LineHeight.labelXs,
+ fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal,
+ ),
+ color = iconColor,
+ maxLines = 1,
+ overflow = TextOverflow.Clip,
+ )
+ }
+}
+
+
+/**
+ * Classic flat navigation bar — the original pre-pill implementation.
+ * No floating pill, no labels, no scroll behavior. Simple icon row with a top divider.
+ */
+@Composable
+fun NuvioClassicNavigationBar(
+ modifier: Modifier = Modifier,
+ content: @Composable NuvioNavigationBarScope.() -> Unit,
+) {
+ val tokens = MaterialTheme.nuvio
+ Column(modifier.fillMaxWidth()) {
+ androidx.compose.material3.HorizontalDivider(
+ thickness = NuvioTokens.Space.hairline,
+ color = tokens.colors.borderDefault,
+ )
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(nuvioBottomNavigationBarInsets().asPaddingValues())
+ .padding(horizontal = NuvioTokens.Space.s4, vertical = nuvioBottomNavigationExtraVerticalPadding),
+ horizontalArrangement = Arrangement.spacedBy(tokens.spacing.controlGap, Alignment.CenterHorizontally),
+ ) {
+ NuvioClassicNavigationBarScopeImpl(this).content()
+ }
+ }
+}
+
+private class NuvioClassicNavigationBarScopeImpl(
+ private val rowScope: androidx.compose.foundation.layout.RowScope,
) : NuvioNavigationBarScope {
@Composable
@@ -86,10 +412,12 @@ private class NuvioNavigationBarScopeImpl(
icon: ImageVector,
contentDescription: String?,
modifier: Modifier,
+ label: String?,
) {
val tokens = MaterialTheme.nuvio
val iconColor by animateColorAsState(
targetValue = if (selected) tokens.colors.accent else tokens.colors.textMuted,
+ label = "classic_nav_icon_color",
)
with(rowScope) {
Icon(
@@ -120,10 +448,12 @@ private class NuvioNavigationBarScopeImpl(
icon: DrawableResource,
contentDescription: String?,
modifier: Modifier,
+ label: String?,
) {
val tokens = MaterialTheme.nuvio
val iconColor by animateColorAsState(
targetValue = if (selected) tokens.colors.accent else tokens.colors.textMuted,
+ label = "classic_nav_icon_color",
)
with(rowScope) {
Icon(
@@ -152,6 +482,7 @@ private class NuvioNavigationBarScopeImpl(
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier,
+ label: String?,
content: @Composable () -> Unit,
) {
val tokens = MaterialTheme.nuvio
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.kt
index bd4c58f07..ab29bb96f 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/PlatformInsets.kt
@@ -19,6 +19,9 @@ internal expect fun platformPhysicalTopInset(): Dp
internal val LocalNuvioBottomNavigationOverlayPadding = staticCompositionLocalOf { 0.dp }
+/** CompositionLocal providing the shared [NuvioNavBarScrollState] so child screens can attach the nestedScrollConnection. */
+val LocalNuvioNavBarScrollState = staticCompositionLocalOf { null }
+
@Composable
internal fun nuvioSafeBottomPadding(extra: Dp = 0.dp): Dp {
val navigationBarBottom = nuvioBottomNavigationBarInsets()
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 1ac3eb1b6..e12425041 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
@@ -38,6 +38,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.nuvio.app.core.ui.AppTheme
+import com.nuvio.app.isIos
import com.nuvio.app.core.ui.NuvioBottomSheetActionRow
import com.nuvio.app.core.ui.NuvioBottomSheetDivider
import com.nuvio.app.core.ui.NuvioModalBottomSheet
@@ -55,6 +56,8 @@ import nuvio.composeapp.generated.resources.compose_settings_page_poster_customi
import nuvio.composeapp.generated.resources.compose_settings_page_streams
import nuvio.composeapp.generated.resources.settings_appearance_app_language
import nuvio.composeapp.generated.resources.settings_appearance_app_language_sheet_title
+import nuvio.composeapp.generated.resources.settings_appearance_nav_bar_style
+import nuvio.composeapp.generated.resources.settings_appearance_nav_bar_style_sheet_title
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
@@ -84,6 +87,8 @@ internal fun LazyListScope.appearanceSettingsContent(
onLiquidGlassNativeTabBarToggle: (Boolean) -> Unit,
selectedAppLanguage: AppLanguage,
onAppLanguageSelected: (AppLanguage) -> Unit,
+ selectedNavBarStyle: NavBarStyle,
+ onNavBarStyleSelected: (NavBarStyle) -> Unit,
onHomescreenClick: () -> Unit,
onMetaScreenClick: () -> Unit,
onStreamsClick: () -> Unit,
@@ -144,6 +149,7 @@ internal fun LazyListScope.appearanceSettingsContent(
}
item {
var showLanguageSheet by remember { mutableStateOf(false) }
+ var showNavBarStyleSheet by remember { mutableStateOf(false) }
SettingsSection(
title = stringResource(Res.string.settings_appearance_section_display),
isTablet = isTablet,
@@ -173,6 +179,15 @@ internal fun LazyListScope.appearanceSettingsContent(
isTablet = isTablet,
onClick = { showLanguageSheet = true },
)
+ if (!isIos) {
+ SettingsGroupDivider(isTablet = isTablet)
+ SettingsNavigationRow(
+ title = stringResource(Res.string.settings_appearance_nav_bar_style),
+ description = stringResource(selectedNavBarStyle.labelRes),
+ isTablet = isTablet,
+ onClick = { showNavBarStyleSheet = true },
+ )
+ }
}
}
@@ -186,6 +201,17 @@ internal fun LazyListScope.appearanceSettingsContent(
onDismiss = { showLanguageSheet = false },
)
}
+
+ if (showNavBarStyleSheet) {
+ NavBarStyleBottomSheet(
+ selectedStyle = selectedNavBarStyle,
+ onStyleSelected = {
+ onNavBarStyleSelected(it)
+ showNavBarStyleSheet = false
+ },
+ onDismiss = { showNavBarStyleSheet = false },
+ )
+ }
}
item {
@@ -406,3 +432,64 @@ private fun ThemeChip(
)
}
}
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun NavBarStyleBottomSheet(
+ selectedStyle: NavBarStyle,
+ onStyleSelected: (NavBarStyle) -> 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_nav_bar_style_sheet_title),
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
+ )
+ }
+
+ itemsIndexed(NavBarStyle.entries.toList()) { index, style ->
+ if (index > 0) {
+ NuvioBottomSheetDivider()
+ }
+ NuvioBottomSheetActionRow(
+ title = stringResource(style.labelRes),
+ onClick = {
+ onStyleSelected(style)
+ coroutineScope.launch {
+ dismissNuvioBottomSheet(sheetState = sheetState, onDismiss = onDismiss)
+ }
+ },
+ trailingContent = {
+ if (style == selectedStyle) {
+ Icon(
+ imageVector = Icons.Default.Check,
+ contentDescription = stringResource(Res.string.cd_selected),
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ }
+ },
+ )
+ }
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/NavBarStyle.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/NavBarStyle.kt
new file mode 100644
index 000000000..fd4842b4a
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/NavBarStyle.kt
@@ -0,0 +1,24 @@
+package com.nuvio.app.features.settings
+
+import nuvio.composeapp.generated.resources.Res
+import nuvio.composeapp.generated.resources.settings_nav_bar_style_adaptive
+import nuvio.composeapp.generated.resources.settings_nav_bar_style_expanded
+import nuvio.composeapp.generated.resources.settings_nav_bar_style_compact
+import nuvio.composeapp.generated.resources.settings_nav_bar_style_classic
+import org.jetbrains.compose.resources.StringResource
+
+enum class NavBarStyle(
+ val key: String,
+ val labelRes: StringResource,
+) {
+ ADAPTIVE("adaptive", Res.string.settings_nav_bar_style_adaptive),
+ EXPANDED("expanded", Res.string.settings_nav_bar_style_expanded),
+ COMPACT("compact", Res.string.settings_nav_bar_style_compact),
+ CLASSIC("classic", Res.string.settings_nav_bar_style_classic),
+ ;
+
+ companion object {
+ fun fromKey(key: String?): NavBarStyle =
+ entries.firstOrNull { it.key.equals(key, ignoreCase = true) } ?: ADAPTIVE
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt
index 9d58c39b4..9ac66fccf 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsScreen.kt
@@ -155,6 +155,7 @@ fun SettingsScreen(
!useNativeNavigation && isLiquidGlassNativeTabBarSupported()
}
val selectedAppLanguage by remember { ThemeSettingsRepository.selectedAppLanguage }.collectAsStateWithLifecycle()
+ val navBarStyle by remember { ThemeSettingsRepository.navBarStyle }.collectAsStateWithLifecycle()
val tmdbSettings by remember {
TmdbSettingsRepository.ensureLoaded()
TmdbSettingsRepository.uiState
@@ -392,6 +393,8 @@ fun SettingsScreen(
onLiquidGlassNativeTabBarToggle = ThemeSettingsRepository::setLiquidGlassNativeTabBar,
selectedAppLanguage = selectedAppLanguage,
onAppLanguageSelected = ThemeSettingsRepository::setAppLanguage,
+ navBarStyle = navBarStyle,
+ onNavBarStyleSelected = ThemeSettingsRepository::setNavBarStyle,
episodeReleaseNotificationsUiState = episodeReleaseNotificationsUiState,
tmdbSettings = tmdbSettings,
mdbListSettings = mdbListSettings,
@@ -450,6 +453,8 @@ fun SettingsScreen(
onLiquidGlassNativeTabBarToggle = ThemeSettingsRepository::setLiquidGlassNativeTabBar,
selectedAppLanguage = selectedAppLanguage,
onAppLanguageSelected = ThemeSettingsRepository::setAppLanguage,
+ navBarStyle = navBarStyle,
+ onNavBarStyleSelected = ThemeSettingsRepository::setNavBarStyle,
episodeReleaseNotificationsUiState = episodeReleaseNotificationsUiState,
tmdbSettings = tmdbSettings,
mdbListSettings = mdbListSettings,
@@ -518,6 +523,8 @@ private fun MobileSettingsScreen(
onLiquidGlassNativeTabBarToggle: (Boolean) -> Unit,
selectedAppLanguage: AppLanguage,
onAppLanguageSelected: (AppLanguage) -> Unit,
+ navBarStyle: NavBarStyle,
+ onNavBarStyleSelected: (NavBarStyle) -> Unit,
episodeReleaseNotificationsUiState: EpisodeReleaseNotificationsUiState,
tmdbSettings: TmdbSettings,
mdbListSettings: MdbListSettings,
@@ -713,6 +720,8 @@ private fun MobileSettingsScreen(
onLiquidGlassNativeTabBarToggle = onLiquidGlassNativeTabBarToggle,
selectedAppLanguage = selectedAppLanguage,
onAppLanguageSelected = onAppLanguageSelected,
+ selectedNavBarStyle = navBarStyle,
+ onNavBarStyleSelected = onNavBarStyleSelected,
onHomescreenClick = onHomescreenClick,
onMetaScreenClick = onMetaScreenClick,
onStreamsClick = { onPageChange(SettingsPage.Streams) },
@@ -870,6 +879,8 @@ private fun TabletSettingsScreen(
onLiquidGlassNativeTabBarToggle: (Boolean) -> Unit,
selectedAppLanguage: AppLanguage,
onAppLanguageSelected: (AppLanguage) -> Unit,
+ navBarStyle: NavBarStyle,
+ onNavBarStyleSelected: (NavBarStyle) -> Unit,
episodeReleaseNotificationsUiState: EpisodeReleaseNotificationsUiState,
tmdbSettings: TmdbSettings,
mdbListSettings: MdbListSettings,
@@ -1121,6 +1132,8 @@ private fun TabletSettingsScreen(
onLiquidGlassNativeTabBarToggle = onLiquidGlassNativeTabBarToggle,
selectedAppLanguage = selectedAppLanguage,
onAppLanguageSelected = onAppLanguageSelected,
+ selectedNavBarStyle = navBarStyle,
+ onNavBarStyleSelected = onNavBarStyleSelected,
onHomescreenClick = { openInlinePage(SettingsPage.Homescreen) },
onMetaScreenClick = { openInlinePage(SettingsPage.MetaScreen) },
onStreamsClick = { openInlinePage(SettingsPage.Streams) },
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 217faa940..c6b5fa827 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
@@ -20,6 +20,9 @@ object ThemeSettingsRepository {
private val _selectedAppLanguage = MutableStateFlow(AppLanguage.DEVICE)
val selectedAppLanguage: StateFlow = _selectedAppLanguage.asStateFlow()
+ private val _navBarStyle = MutableStateFlow(NavBarStyle.ADAPTIVE)
+ val navBarStyle: StateFlow = _navBarStyle.asStateFlow()
+
private var hasLoaded = false
fun ensureLoaded() {
@@ -39,6 +42,7 @@ object ThemeSettingsRepository {
NativeTabBridge.publishAccentColor(AppTheme.WHITE.nativeTabAccentHex())
NativeTabBridge.publishLiquidGlassEnabled(false)
_selectedAppLanguage.value = AppLanguage.DEVICE
+ _navBarStyle.value = NavBarStyle.ADAPTIVE
}
private fun loadFromDisk() {
@@ -62,6 +66,7 @@ object ThemeSettingsRepository {
val appLanguage = AppLanguage.fromCode(ThemeSettingsStorage.loadSelectedAppLanguage())
ThemeSettingsStorage.applySelectedAppLanguage(appLanguage.code)
_selectedAppLanguage.value = appLanguage
+ _navBarStyle.value = NavBarStyle.fromKey(ThemeSettingsStorage.loadNavBarStyle())
}
fun setTheme(theme: AppTheme) {
@@ -94,6 +99,13 @@ object ThemeSettingsRepository {
ThemeSettingsStorage.applySelectedAppLanguage(language.code)
_selectedAppLanguage.value = language
}
+
+ fun setNavBarStyle(style: NavBarStyle) {
+ ensureLoaded()
+ if (_navBarStyle.value == style) return
+ _navBarStyle.value = style
+ ThemeSettingsStorage.saveNavBarStyle(style.key)
+ }
}
private fun AppTheme.nativeTabAccentHex(): String =
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 2a788baf1..00cb705b7 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
@@ -12,6 +12,8 @@ internal expect object ThemeSettingsStorage {
fun loadSelectedAppLanguage(): String?
fun saveSelectedAppLanguage(languageCode: String)
fun applySelectedAppLanguage(languageCode: String)
+ fun loadNavBarStyle(): String?
+ fun saveNavBarStyle(styleKey: String)
fun exportToSyncPayload(): JsonObject
fun replaceFromSyncPayload(payload: JsonObject)
}
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 f36f0a921..7525e63ae 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
@@ -15,10 +15,12 @@ actual object ThemeSettingsStorage {
private const val amoledEnabledKey = "amoled_enabled"
private const val liquidGlassNativeTabBarEnabledKey = "liquid_glass_native_tab_bar_enabled"
private const val selectedAppLanguageKey = "selected_app_language"
+ private const val navBarStyleKey = "nav_bar_style"
private val profileScopedSyncKeys = listOf(
selectedThemeKey,
amoledEnabledKey,
liquidGlassNativeTabBarEnabledKey,
+ navBarStyleKey,
)
actual fun loadSelectedTheme(): String? =
@@ -88,10 +90,18 @@ actual object ThemeSettingsStorage {
NSUserDefaults.standardUserDefaults.synchronize()
}
+ actual fun loadNavBarStyle(): String? =
+ NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(navBarStyleKey))
+
+ actual fun saveNavBarStyle(styleKey: String) {
+ NSUserDefaults.standardUserDefaults.setObject(styleKey, forKey = ProfileScopedKey.of(navBarStyleKey))
+ }
+
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) }
loadAmoledEnabled()?.let { put(amoledEnabledKey, encodeSyncBoolean(it)) }
loadLiquidGlassNativeTabBarEnabled()?.let { put(liquidGlassNativeTabBarEnabledKey, encodeSyncBoolean(it)) }
+ loadNavBarStyle()?.let { put(navBarStyleKey, encodeSyncString(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
@@ -102,6 +112,7 @@ actual object ThemeSettingsStorage {
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)
payload.decodeSyncBoolean(liquidGlassNativeTabBarEnabledKey)?.let(::saveLiquidGlassNativeTabBarEnabled)
+ payload.decodeSyncString(navBarStyleKey)?.let(::saveNavBarStyle)
applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.DEVICE.code)
}
}