mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 15:32:01 +00:00
feat(ios): ios native navbar
This commit is contained in:
parent
b3ed47732a
commit
8b2a635174
16 changed files with 506 additions and 60 deletions
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.nuvio.app.core.ui
|
||||||
|
|
||||||
|
internal actual fun isLiquidGlassNativeTabBarSupported(): Boolean = false
|
||||||
|
|
||||||
|
internal actual fun publishLiquidGlassNativeTabBarEnabled(enabled: Boolean) = Unit
|
||||||
|
|
||||||
|
internal actual fun publishNativeTabBarVisible(visible: Boolean) = Unit
|
||||||
|
|
||||||
|
internal actual fun publishNativeSelectedTab(tabName: String) = Unit
|
||||||
|
|
@ -17,8 +17,13 @@ actual object ThemeSettingsStorage {
|
||||||
private const val preferencesName = "nuvio_theme_settings"
|
private const val preferencesName = "nuvio_theme_settings"
|
||||||
private const val selectedThemeKey = "selected_theme"
|
private const val selectedThemeKey = "selected_theme"
|
||||||
private const val amoledEnabledKey = "amoled_enabled"
|
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 selectedAppLanguageKey = "selected_app_language"
|
||||||
private val profileScopedSyncKeys = listOf(selectedThemeKey, amoledEnabledKey)
|
private val profileScopedSyncKeys = listOf(
|
||||||
|
selectedThemeKey,
|
||||||
|
amoledEnabledKey,
|
||||||
|
liquidGlassNativeTabBarEnabledKey,
|
||||||
|
)
|
||||||
private val globalSyncKeys = listOf(selectedAppLanguageKey)
|
private val globalSyncKeys = listOf(selectedAppLanguageKey)
|
||||||
|
|
||||||
private var preferences: SharedPreferences? = null
|
private var preferences: SharedPreferences? = null
|
||||||
|
|
@ -51,6 +56,19 @@ actual object ThemeSettingsStorage {
|
||||||
?.apply()
|
?.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun loadLiquidGlassNativeTabBarEnabled(): Boolean? =
|
||||||
|
preferences?.let { prefs ->
|
||||||
|
val key = ProfileScopedKey.of(liquidGlassNativeTabBarEnabledKey)
|
||||||
|
if (prefs.contains(key)) prefs.getBoolean(key, false) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun saveLiquidGlassNativeTabBarEnabled(enabled: Boolean) {
|
||||||
|
preferences
|
||||||
|
?.edit()
|
||||||
|
?.putBoolean(ProfileScopedKey.of(liquidGlassNativeTabBarEnabledKey), enabled)
|
||||||
|
?.apply()
|
||||||
|
}
|
||||||
|
|
||||||
actual fun loadSelectedAppLanguage(): String? {
|
actual fun loadSelectedAppLanguage(): String? {
|
||||||
val value = preferences?.getString(selectedAppLanguageKey, null)
|
val value = preferences?.getString(selectedAppLanguageKey, null)
|
||||||
if (value != null) return value
|
if (value != null) return value
|
||||||
|
|
@ -75,6 +93,7 @@ actual object ThemeSettingsStorage {
|
||||||
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||||
loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) }
|
loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) }
|
||||||
loadAmoledEnabled()?.let { put(amoledEnabledKey, encodeSyncBoolean(it)) }
|
loadAmoledEnabled()?.let { put(amoledEnabledKey, encodeSyncBoolean(it)) }
|
||||||
|
loadLiquidGlassNativeTabBarEnabled()?.let { put(liquidGlassNativeTabBarEnabledKey, encodeSyncBoolean(it)) }
|
||||||
loadSelectedAppLanguage()?.let { put(selectedAppLanguageKey, encodeSyncString(it)) }
|
loadSelectedAppLanguage()?.let { put(selectedAppLanguageKey, encodeSyncString(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,6 +105,7 @@ actual object ThemeSettingsStorage {
|
||||||
|
|
||||||
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
|
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
|
||||||
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)
|
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)
|
||||||
|
payload.decodeSyncBoolean(liquidGlassNativeTabBarEnabledKey)?.let(::saveLiquidGlassNativeTabBarEnabled)
|
||||||
payload.decodeSyncString(selectedAppLanguageKey)?.let(::saveSelectedAppLanguage)
|
payload.decodeSyncString(selectedAppLanguageKey)?.let(::saveSelectedAppLanguage)
|
||||||
applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.ENGLISH.code)
|
applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.ENGLISH.code)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -449,6 +449,8 @@
|
||||||
<string name="settings_appearance_app_language">App Language</string>
|
<string name="settings_appearance_app_language">App Language</string>
|
||||||
<string name="settings_appearance_app_language_sheet_title">Choose 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_continue_watching_description">Settings for the Continue Watching section.</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.</string>
|
||||||
<string name="settings_appearance_poster_customization_description">Tune card width and corner radius.</string>
|
<string name="settings_appearance_poster_customization_description">Tune card width and corner radius.</string>
|
||||||
<string name="settings_appearance_section_display">DISPLAY</string>
|
<string name="settings_appearance_section_display">DISPLAY</string>
|
||||||
<string name="settings_appearance_section_home">HOME</string>
|
<string name="settings_appearance_section_home">HOME</string>
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -92,6 +93,10 @@ import com.nuvio.app.core.ui.NuvioToastController
|
||||||
import com.nuvio.app.core.ui.NuvioFloatingPrompt
|
import com.nuvio.app.core.ui.NuvioFloatingPrompt
|
||||||
import com.nuvio.app.core.ui.TraktListPickerDialog
|
import com.nuvio.app.core.ui.TraktListPickerDialog
|
||||||
import com.nuvio.app.core.ui.NuvioTheme
|
import com.nuvio.app.core.ui.NuvioTheme
|
||||||
|
import com.nuvio.app.core.ui.LocalNuvioBottomNavigationOverlayPadding
|
||||||
|
import com.nuvio.app.core.ui.NativeNavigationTab
|
||||||
|
import com.nuvio.app.core.ui.NativeTabBridge
|
||||||
|
import com.nuvio.app.core.ui.isLiquidGlassNativeTabBarSupported
|
||||||
import com.nuvio.app.core.ui.localizedContinueWatchingSubtitle
|
import com.nuvio.app.core.ui.localizedContinueWatchingSubtitle
|
||||||
import com.nuvio.app.features.auth.AuthScreen
|
import com.nuvio.app.features.auth.AuthScreen
|
||||||
import com.nuvio.app.features.addons.AddonRepository
|
import com.nuvio.app.features.addons.AddonRepository
|
||||||
|
|
@ -260,6 +265,20 @@ enum class AppScreenTab {
|
||||||
Settings,
|
Settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun AppScreenTab.toNativeNavigationTab(): NativeNavigationTab = when (this) {
|
||||||
|
AppScreenTab.Home -> NativeNavigationTab.Home
|
||||||
|
AppScreenTab.Search -> NativeNavigationTab.Search
|
||||||
|
AppScreenTab.Library -> NativeNavigationTab.Library
|
||||||
|
AppScreenTab.Settings -> NativeNavigationTab.Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NativeNavigationTab.toAppScreenTab(): AppScreenTab = when (this) {
|
||||||
|
NativeNavigationTab.Home -> AppScreenTab.Home
|
||||||
|
NativeNavigationTab.Search -> AppScreenTab.Search
|
||||||
|
NativeNavigationTab.Library -> AppScreenTab.Library
|
||||||
|
NativeNavigationTab.Settings -> AppScreenTab.Settings
|
||||||
|
}
|
||||||
|
|
||||||
private enum class AppGateScreen {
|
private enum class AppGateScreen {
|
||||||
Loading,
|
Loading,
|
||||||
Auth,
|
Auth,
|
||||||
|
|
@ -466,6 +485,11 @@ private fun MainAppContent(
|
||||||
val hapticFeedback = LocalHapticFeedback.current
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
var selectedTab by rememberSaveable { mutableStateOf(AppScreenTab.Home) }
|
var selectedTab by rememberSaveable { mutableStateOf(AppScreenTab.Home) }
|
||||||
|
val nativeRequestedTab by remember { NativeTabBridge.requestedTab }.collectAsStateWithLifecycle()
|
||||||
|
val liquidGlassNativeTabBarEnabled by remember {
|
||||||
|
ThemeSettingsRepository.liquidGlassNativeTabBarEnabled
|
||||||
|
}.collectAsStateWithLifecycle()
|
||||||
|
val liquidGlassNativeTabBarSupported = remember { isLiquidGlassNativeTabBarSupported() }
|
||||||
var showExitConfirmation by rememberSaveable { mutableStateOf(false) }
|
var showExitConfirmation by rememberSaveable { mutableStateOf(false) }
|
||||||
var selectedPosterForActions by remember { mutableStateOf<MetaPreview?>(null) }
|
var selectedPosterForActions by remember { mutableStateOf<MetaPreview?>(null) }
|
||||||
var selectedContinueWatchingForActions by remember { mutableStateOf<ContinueWatchingItem?>(null) }
|
var selectedContinueWatchingForActions by remember { mutableStateOf<ContinueWatchingItem?>(null) }
|
||||||
|
|
@ -515,6 +539,16 @@ private fun MainAppContent(
|
||||||
.sorted()
|
.sorted()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(nativeRequestedTab) {
|
||||||
|
if (liquidGlassNativeTabBarSupported && liquidGlassNativeTabBarEnabled) {
|
||||||
|
selectedTab = nativeRequestedTab.toAppScreenTab()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(selectedTab) {
|
||||||
|
NativeTabBridge.publishSelectedTab(selectedTab.toNativeNavigationTab())
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
NetworkStatusRepository.ensureStarted()
|
NetworkStatusRepository.ensureStarted()
|
||||||
EpisodeReleaseNotificationsRepository.refreshAsync()
|
EpisodeReleaseNotificationsRepository.refreshAsync()
|
||||||
|
|
@ -886,6 +920,8 @@ private fun MainAppContent(
|
||||||
|
|
||||||
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
||||||
val isTabletLayout = maxWidth >= 768.dp
|
val isTabletLayout = maxWidth >= 768.dp
|
||||||
|
val useNativeBottomTabs =
|
||||||
|
liquidGlassNativeTabBarSupported && liquidGlassNativeTabBarEnabled && initialHomeReady
|
||||||
val onProfileSelected: (NuvioProfile) -> Unit = { profile ->
|
val onProfileSelected: (NuvioProfile) -> Unit = { profile ->
|
||||||
profileSwitchLoading = true
|
profileSwitchLoading = true
|
||||||
selectedTab = AppScreenTab.Home
|
selectedTab = AppScreenTab.Home
|
||||||
|
|
@ -893,6 +929,13 @@ private fun MainAppContent(
|
||||||
com.nuvio.app.core.sync.SyncManager.pullAllForProfile(profile.profileIndex)
|
com.nuvio.app.core.sync.SyncManager.pullAllForProfile(profile.profileIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisposableEffect(useNativeBottomTabs) {
|
||||||
|
NativeTabBridge.publishTabBarVisible(useNativeBottomTabs)
|
||||||
|
onDispose {
|
||||||
|
NativeTabBridge.publishTabBarVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -900,7 +943,7 @@ private fun MainAppContent(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
contentWindowInsets = WindowInsets(0),
|
contentWindowInsets = WindowInsets(0),
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
if (!isTabletLayout) {
|
if (!isTabletLayout && !useNativeBottomTabs) {
|
||||||
NuvioNavigationBar {
|
NuvioNavigationBar {
|
||||||
NavItem(
|
NavItem(
|
||||||
selected = selectedTab == AppScreenTab.Home,
|
selected = selectedTab == AppScreenTab.Home,
|
||||||
|
|
@ -936,58 +979,62 @@ private fun MainAppContent(
|
||||||
},
|
},
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
AppTabHost(
|
CompositionLocalProvider(
|
||||||
modifier = Modifier
|
LocalNuvioBottomNavigationOverlayPadding provides if (useNativeBottomTabs) 49.dp else 0.dp,
|
||||||
.fillMaxSize()
|
) {
|
||||||
.padding(innerPadding),
|
AppTabHost(
|
||||||
selectedTab = selectedTab,
|
modifier = Modifier
|
||||||
onCatalogClick = onCatalogClick,
|
.fillMaxSize()
|
||||||
onPosterClick = { meta ->
|
.padding(innerPadding),
|
||||||
navController.navigate(DetailRoute(type = meta.type, id = meta.id))
|
selectedTab = selectedTab,
|
||||||
},
|
onCatalogClick = onCatalogClick,
|
||||||
onPosterLongClick = { meta ->
|
onPosterClick = { meta ->
|
||||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
navController.navigate(DetailRoute(type = meta.type, id = meta.id))
|
||||||
selectedPosterForActions = meta
|
},
|
||||||
},
|
onPosterLongClick = { meta ->
|
||||||
onLibraryPosterClick = { item ->
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
navController.navigate(DetailRoute(type = item.type, id = item.id))
|
selectedPosterForActions = meta
|
||||||
},
|
},
|
||||||
onLibrarySectionViewAllClick = onLibrarySectionViewAllClick,
|
onLibraryPosterClick = { item ->
|
||||||
onContinueWatchingClick = onContinueWatchingClick,
|
navController.navigate(DetailRoute(type = item.type, id = item.id))
|
||||||
onContinueWatchingLongPress = onContinueWatchingLongPress,
|
},
|
||||||
onSwitchProfile = onSwitchProfile,
|
onLibrarySectionViewAllClick = onLibrarySectionViewAllClick,
|
||||||
onHomescreenSettingsClick = { navController.navigate(HomescreenSettingsRoute) },
|
onContinueWatchingClick = onContinueWatchingClick,
|
||||||
onMetaScreenSettingsClick = { navController.navigate(MetaScreenSettingsRoute) },
|
onContinueWatchingLongPress = onContinueWatchingLongPress,
|
||||||
onContinueWatchingSettingsClick = { navController.navigate(ContinueWatchingSettingsRoute) },
|
onSwitchProfile = onSwitchProfile,
|
||||||
onDownloadsSettingsClick = { navController.navigate(DownloadsSettingsRoute) },
|
onHomescreenSettingsClick = { navController.navigate(HomescreenSettingsRoute) },
|
||||||
onAddonsSettingsClick = { navController.navigate(AddonsSettingsRoute) },
|
onMetaScreenSettingsClick = { navController.navigate(MetaScreenSettingsRoute) },
|
||||||
onPluginsSettingsClick = {
|
onContinueWatchingSettingsClick = { navController.navigate(ContinueWatchingSettingsRoute) },
|
||||||
if (AppFeaturePolicy.pluginsEnabled) {
|
onDownloadsSettingsClick = { navController.navigate(DownloadsSettingsRoute) },
|
||||||
navController.navigate(PluginsSettingsRoute)
|
onAddonsSettingsClick = { navController.navigate(AddonsSettingsRoute) },
|
||||||
}
|
onPluginsSettingsClick = {
|
||||||
},
|
if (AppFeaturePolicy.pluginsEnabled) {
|
||||||
onAccountSettingsClick = { navController.navigate(AccountSettingsRoute) },
|
navController.navigate(PluginsSettingsRoute)
|
||||||
onSupportersContributorsSettingsClick = {
|
}
|
||||||
navController.navigate(SupportersContributorsSettingsRoute)
|
},
|
||||||
},
|
onAccountSettingsClick = { navController.navigate(AccountSettingsRoute) },
|
||||||
onCheckForUpdatesClick = if (AppFeaturePolicy.inAppUpdaterEnabled) {
|
onSupportersContributorsSettingsClick = {
|
||||||
{
|
navController.navigate(SupportersContributorsSettingsRoute)
|
||||||
appUpdaterController.checkForUpdates(
|
},
|
||||||
force = true,
|
onCheckForUpdatesClick = if (AppFeaturePolicy.inAppUpdaterEnabled) {
|
||||||
showNoUpdateFeedback = true,
|
{
|
||||||
)
|
appUpdaterController.checkForUpdates(
|
||||||
}
|
force = true,
|
||||||
} else {
|
showNoUpdateFeedback = true,
|
||||||
null
|
)
|
||||||
},
|
}
|
||||||
onCollectionsSettingsClick = { navController.navigate(CollectionsRoute) },
|
} else {
|
||||||
onFolderClick = { collectionId, folderId ->
|
null
|
||||||
navController.navigate(FolderDetailRoute(collectionId = collectionId, folderId = folderId))
|
},
|
||||||
},
|
onCollectionsSettingsClick = { navController.navigate(CollectionsRoute) },
|
||||||
onInitialHomeContentRendered = { initialHomeReady = true },
|
onFolderClick = { collectionId, folderId ->
|
||||||
)
|
navController.navigate(FolderDetailRoute(collectionId = collectionId, folderId = folderId))
|
||||||
|
},
|
||||||
|
onInitialHomeContentRendered = { initialHomeReady = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (isTabletLayout) {
|
if (isTabletLayout && !useNativeBottomTabs) {
|
||||||
TabletFloatingTopBar(
|
TabletFloatingTopBar(
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
onTabSelected = { selectedTab = it },
|
onTabSelected = { selectedTab = it },
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,7 @@ object ProfileSettingsSync {
|
||||||
val signatureFlows = listOf(
|
val signatureFlows = listOf(
|
||||||
ThemeSettingsRepository.selectedTheme.map { "theme" },
|
ThemeSettingsRepository.selectedTheme.map { "theme" },
|
||||||
ThemeSettingsRepository.amoledEnabled.map { "amoled" },
|
ThemeSettingsRepository.amoledEnabled.map { "amoled" },
|
||||||
|
ThemeSettingsRepository.liquidGlassNativeTabBarEnabled.map { "liquid_glass_tab_bar" },
|
||||||
PosterCardStyleRepository.uiState.map { "poster_card_style" },
|
PosterCardStyleRepository.uiState.map { "poster_card_style" },
|
||||||
PlayerSettingsRepository.uiState.map { "player" },
|
PlayerSettingsRepository.uiState.map { "player" },
|
||||||
TmdbSettingsRepository.uiState.map { "tmdb" },
|
TmdbSettingsRepository.uiState.map { "tmdb" },
|
||||||
|
|
@ -265,6 +266,7 @@ object ProfileSettingsSync {
|
||||||
private fun currentObservedStateSignature(): String = listOf(
|
private fun currentObservedStateSignature(): String = listOf(
|
||||||
"theme=${ThemeSettingsRepository.selectedTheme.value.name}",
|
"theme=${ThemeSettingsRepository.selectedTheme.value.name}",
|
||||||
"amoled=${ThemeSettingsRepository.amoledEnabled.value}",
|
"amoled=${ThemeSettingsRepository.amoledEnabled.value}",
|
||||||
|
"liquid_glass_tab_bar=${ThemeSettingsRepository.liquidGlassNativeTabBarEnabled.value}",
|
||||||
"poster_card_style=${PosterCardStyleRepository.uiState.value}",
|
"poster_card_style=${PosterCardStyleRepository.uiState.value}",
|
||||||
"player=${PlayerSettingsRepository.uiState.value}",
|
"player=${PlayerSettingsRepository.uiState.value}",
|
||||||
"tmdb=${TmdbSettingsRepository.uiState.value}",
|
"tmdb=${TmdbSettingsRepository.uiState.value}",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package com.nuvio.app.core.ui
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
internal enum class NativeNavigationTab {
|
||||||
|
Home,
|
||||||
|
Search,
|
||||||
|
Library,
|
||||||
|
Settings,
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromName(name: String): NativeNavigationTab =
|
||||||
|
entries.firstOrNull { it.name.equals(name, ignoreCase = true) } ?: Home
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal object NativeTabBridge {
|
||||||
|
private val _requestedTab = MutableStateFlow(NativeNavigationTab.Home)
|
||||||
|
val requestedTab: StateFlow<NativeNavigationTab> = _requestedTab.asStateFlow()
|
||||||
|
|
||||||
|
fun requestTab(tabName: String) {
|
||||||
|
_requestedTab.value = NativeNavigationTab.fromName(tabName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun publishSelectedTab(tab: NativeNavigationTab) {
|
||||||
|
publishNativeSelectedTab(tab.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun publishTabBarVisible(visible: Boolean) {
|
||||||
|
publishNativeTabBarVisible(visible && isLiquidGlassNativeTabBarSupported())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun publishLiquidGlassEnabled(enabled: Boolean) {
|
||||||
|
publishLiquidGlassNativeTabBarEnabled(enabled && isLiquidGlassNativeTabBarSupported())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nativeTabSelect(tabName: String) {
|
||||||
|
NativeTabBridge.requestTab(tabName)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal expect fun isLiquidGlassNativeTabBarSupported(): Boolean
|
||||||
|
|
||||||
|
internal expect fun publishLiquidGlassNativeTabBarEnabled(enabled: Boolean)
|
||||||
|
|
||||||
|
internal expect fun publishNativeTabBarVisible(visible: Boolean)
|
||||||
|
|
||||||
|
internal expect fun publishNativeSelectedTab(tabName: String)
|
||||||
|
|
@ -3,6 +3,7 @@ package com.nuvio.app.core.ui
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
|
@ -12,10 +13,14 @@ internal expect val nuvioBottomNavigationExtraVerticalPadding: Dp
|
||||||
@Composable
|
@Composable
|
||||||
internal expect fun nuvioBottomNavigationBarInsets(): WindowInsets
|
internal expect fun nuvioBottomNavigationBarInsets(): WindowInsets
|
||||||
|
|
||||||
|
internal val LocalNuvioBottomNavigationOverlayPadding = staticCompositionLocalOf { 0.dp }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun nuvioSafeBottomPadding(extra: Dp = 0.dp): Dp {
|
internal fun nuvioSafeBottomPadding(extra: Dp = 0.dp): Dp {
|
||||||
val navigationBarBottom = nuvioBottomNavigationBarInsets()
|
val navigationBarBottom = nuvioBottomNavigationBarInsets()
|
||||||
.asPaddingValues()
|
.asPaddingValues()
|
||||||
.calculateBottomPadding()
|
.calculateBottomPadding()
|
||||||
return navigationBarBottom.coerceAtLeast(nuvioPlatformExtraBottomPadding) + extra
|
return navigationBarBottom.coerceAtLeast(nuvioPlatformExtraBottomPadding) +
|
||||||
|
LocalNuvioBottomNavigationOverlayPadding.current +
|
||||||
|
extra
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.nuvio.app.core.network.NetworkCondition
|
import com.nuvio.app.core.network.NetworkCondition
|
||||||
import com.nuvio.app.core.network.NetworkStatusRepository
|
import com.nuvio.app.core.network.NetworkStatusRepository
|
||||||
|
import com.nuvio.app.core.ui.LocalNuvioBottomNavigationOverlayPadding
|
||||||
import com.nuvio.app.core.ui.NuvioScreen
|
import com.nuvio.app.core.ui.NuvioScreen
|
||||||
import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
|
import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
|
||||||
|
import com.nuvio.app.core.ui.nuvioSafeBottomPadding
|
||||||
import com.nuvio.app.features.addons.AddonRepository
|
import com.nuvio.app.features.addons.AddonRepository
|
||||||
import com.nuvio.app.features.details.MetaDetailsRepository
|
import com.nuvio.app.features.details.MetaDetailsRepository
|
||||||
import com.nuvio.app.features.details.nextReleasedEpisodeAfter
|
import com.nuvio.app.features.details.nextReleasedEpisodeAfter
|
||||||
|
|
@ -405,12 +407,19 @@ fun HomeScreen(
|
||||||
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
|
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
|
||||||
val homeSectionPadding = homeSectionHorizontalPaddingForWidth(maxWidth.value)
|
val homeSectionPadding = homeSectionHorizontalPaddingForWidth(maxWidth.value)
|
||||||
val continueWatchingLayout = rememberContinueWatchingLayout(maxWidth.value)
|
val continueWatchingLayout = rememberContinueWatchingLayout(maxWidth.value)
|
||||||
|
val nativeBottomNavigationOverlayHeight =
|
||||||
|
if (LocalNuvioBottomNavigationOverlayPadding.current > 0.dp) {
|
||||||
|
nuvioSafeBottomPadding()
|
||||||
|
} else {
|
||||||
|
0.dp
|
||||||
|
}
|
||||||
val mobileHeroBelowSectionHeightHint = remember(
|
val mobileHeroBelowSectionHeightHint = remember(
|
||||||
maxWidth.value,
|
maxWidth.value,
|
||||||
continueWatchingPreferences.isVisible,
|
continueWatchingPreferences.isVisible,
|
||||||
continueWatchingPreferences.style,
|
continueWatchingPreferences.style,
|
||||||
continueWatchingItems.isNotEmpty(),
|
continueWatchingItems.isNotEmpty(),
|
||||||
continueWatchingLayout,
|
continueWatchingLayout,
|
||||||
|
nativeBottomNavigationOverlayHeight,
|
||||||
) {
|
) {
|
||||||
heroMobileBelowSectionHeightHint(
|
heroMobileBelowSectionHeightHint(
|
||||||
maxWidthDp = maxWidth.value,
|
maxWidthDp = maxWidth.value,
|
||||||
|
|
@ -418,6 +427,7 @@ fun HomeScreen(
|
||||||
hasContinueWatchingItems = continueWatchingItems.isNotEmpty(),
|
hasContinueWatchingItems = continueWatchingItems.isNotEmpty(),
|
||||||
continueWatchingStyle = continueWatchingPreferences.style,
|
continueWatchingStyle = continueWatchingPreferences.style,
|
||||||
continueWatchingLayout = continueWatchingLayout,
|
continueWatchingLayout = continueWatchingLayout,
|
||||||
|
bottomNavigationOverlayHeight = nativeBottomNavigationOverlayHeight,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -605,14 +615,16 @@ private fun heroMobileBelowSectionHeightHint(
|
||||||
hasContinueWatchingItems: Boolean,
|
hasContinueWatchingItems: Boolean,
|
||||||
continueWatchingStyle: ContinueWatchingSectionStyle,
|
continueWatchingStyle: ContinueWatchingSectionStyle,
|
||||||
continueWatchingLayout: ContinueWatchingLayout,
|
continueWatchingLayout: ContinueWatchingLayout,
|
||||||
|
bottomNavigationOverlayHeight: Dp,
|
||||||
): Dp? {
|
): Dp? {
|
||||||
if (maxWidthDp >= 600f || !continueWatchingVisible || !hasContinueWatchingItems) return null
|
if (maxWidthDp >= 600f || !continueWatchingVisible || !hasContinueWatchingItems) return null
|
||||||
|
|
||||||
return when (continueWatchingStyle) {
|
val sectionHeight = when (continueWatchingStyle) {
|
||||||
ContinueWatchingSectionStyle.Wide -> continueWatchingLayout.wideCardHeight + 56.dp
|
ContinueWatchingSectionStyle.Wide -> continueWatchingLayout.wideCardHeight + 56.dp
|
||||||
ContinueWatchingSectionStyle.Poster ->
|
ContinueWatchingSectionStyle.Poster ->
|
||||||
continueWatchingLayout.posterCardHeight + continueWatchingLayout.posterTitleBlockHeight + 70.dp
|
continueWatchingLayout.posterCardHeight + continueWatchingLayout.posterTitleBlockHeight + 70.dp
|
||||||
}
|
}
|
||||||
|
return sectionHeight + bottomNavigationOverlayHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun buildHomeContinueWatchingItems(
|
internal fun buildHomeContinueWatchingItems(
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,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_black
|
||||||
import nuvio.composeapp.generated.resources.settings_appearance_amoled_description
|
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_continue_watching_description
|
||||||
|
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
|
import nuvio.composeapp.generated.resources.settings_appearance_poster_customization_description
|
||||||
import nuvio.composeapp.generated.resources.settings_appearance_section_display
|
import nuvio.composeapp.generated.resources.settings_appearance_section_display
|
||||||
import nuvio.composeapp.generated.resources.settings_appearance_section_home
|
import nuvio.composeapp.generated.resources.settings_appearance_section_home
|
||||||
|
|
@ -70,6 +72,9 @@ internal fun LazyListScope.appearanceSettingsContent(
|
||||||
onThemeSelected: (AppTheme) -> Unit,
|
onThemeSelected: (AppTheme) -> Unit,
|
||||||
amoledEnabled: Boolean,
|
amoledEnabled: Boolean,
|
||||||
onAmoledToggle: (Boolean) -> Unit,
|
onAmoledToggle: (Boolean) -> Unit,
|
||||||
|
liquidGlassNativeTabBarSupported: Boolean,
|
||||||
|
liquidGlassNativeTabBarEnabled: Boolean,
|
||||||
|
onLiquidGlassNativeTabBarToggle: (Boolean) -> Unit,
|
||||||
selectedAppLanguage: AppLanguage,
|
selectedAppLanguage: AppLanguage,
|
||||||
onAppLanguageSelected: (AppLanguage) -> Unit,
|
onAppLanguageSelected: (AppLanguage) -> Unit,
|
||||||
onContinueWatchingClick: () -> Unit,
|
onContinueWatchingClick: () -> Unit,
|
||||||
|
|
@ -118,6 +123,16 @@ internal fun LazyListScope.appearanceSettingsContent(
|
||||||
isTablet = isTablet,
|
isTablet = isTablet,
|
||||||
onCheckedChange = onAmoledToggle,
|
onCheckedChange = onAmoledToggle,
|
||||||
)
|
)
|
||||||
|
if (liquidGlassNativeTabBarSupported) {
|
||||||
|
SettingsGroupDivider(isTablet = isTablet)
|
||||||
|
SettingsSwitchRow(
|
||||||
|
title = stringResource(Res.string.settings_appearance_liquid_glass),
|
||||||
|
description = stringResource(Res.string.settings_appearance_liquid_glass_description),
|
||||||
|
checked = liquidGlassNativeTabBarEnabled,
|
||||||
|
isTablet = isTablet,
|
||||||
|
onCheckedChange = onLiquidGlassNativeTabBarToggle,
|
||||||
|
)
|
||||||
|
}
|
||||||
SettingsGroupDivider(isTablet = isTablet)
|
SettingsGroupDivider(isTablet = isTablet)
|
||||||
SettingsNavigationRow(
|
SettingsNavigationRow(
|
||||||
title = stringResource(Res.string.settings_appearance_app_language),
|
title = stringResource(Res.string.settings_appearance_app_language),
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,11 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.max
|
import androidx.compose.ui.unit.max
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.nuvio.app.core.ui.AppTheme
|
import com.nuvio.app.core.ui.AppTheme
|
||||||
|
import com.nuvio.app.core.ui.LocalNuvioBottomNavigationOverlayPadding
|
||||||
import com.nuvio.app.core.ui.NuvioScreen
|
import com.nuvio.app.core.ui.NuvioScreen
|
||||||
import com.nuvio.app.core.ui.NuvioScreenHeader
|
import com.nuvio.app.core.ui.NuvioScreenHeader
|
||||||
import com.nuvio.app.core.ui.PlatformBackHandler
|
import com.nuvio.app.core.ui.PlatformBackHandler
|
||||||
|
import com.nuvio.app.core.ui.isLiquidGlassNativeTabBarSupported
|
||||||
import com.nuvio.app.features.addons.AddonRepository
|
import com.nuvio.app.features.addons.AddonRepository
|
||||||
import com.nuvio.app.features.details.MetaScreenSettingsRepository
|
import com.nuvio.app.features.details.MetaScreenSettingsRepository
|
||||||
import com.nuvio.app.features.details.MetaScreenSettingsUiState
|
import com.nuvio.app.features.details.MetaScreenSettingsUiState
|
||||||
|
|
@ -94,6 +96,10 @@ fun SettingsScreen(
|
||||||
ThemeSettingsRepository.selectedTheme
|
ThemeSettingsRepository.selectedTheme
|
||||||
}.collectAsStateWithLifecycle()
|
}.collectAsStateWithLifecycle()
|
||||||
val amoledEnabled by remember { ThemeSettingsRepository.amoledEnabled }.collectAsStateWithLifecycle()
|
val amoledEnabled by remember { ThemeSettingsRepository.amoledEnabled }.collectAsStateWithLifecycle()
|
||||||
|
val liquidGlassNativeTabBarEnabled by remember {
|
||||||
|
ThemeSettingsRepository.liquidGlassNativeTabBarEnabled
|
||||||
|
}.collectAsStateWithLifecycle()
|
||||||
|
val liquidGlassNativeTabBarSupported = remember { isLiquidGlassNativeTabBarSupported() }
|
||||||
val selectedAppLanguage by remember { ThemeSettingsRepository.selectedAppLanguage }.collectAsStateWithLifecycle()
|
val selectedAppLanguage by remember { ThemeSettingsRepository.selectedAppLanguage }.collectAsStateWithLifecycle()
|
||||||
val tmdbSettings by remember {
|
val tmdbSettings by remember {
|
||||||
TmdbSettingsRepository.ensureLoaded()
|
TmdbSettingsRepository.ensureLoaded()
|
||||||
|
|
@ -191,6 +197,9 @@ fun SettingsScreen(
|
||||||
onThemeSelected = ThemeSettingsRepository::setTheme,
|
onThemeSelected = ThemeSettingsRepository::setTheme,
|
||||||
amoledEnabled = amoledEnabled,
|
amoledEnabled = amoledEnabled,
|
||||||
onAmoledToggle = ThemeSettingsRepository::setAmoled,
|
onAmoledToggle = ThemeSettingsRepository::setAmoled,
|
||||||
|
liquidGlassNativeTabBarSupported = liquidGlassNativeTabBarSupported,
|
||||||
|
liquidGlassNativeTabBarEnabled = liquidGlassNativeTabBarEnabled,
|
||||||
|
onLiquidGlassNativeTabBarToggle = ThemeSettingsRepository::setLiquidGlassNativeTabBar,
|
||||||
selectedAppLanguage = selectedAppLanguage,
|
selectedAppLanguage = selectedAppLanguage,
|
||||||
onAppLanguageSelected = ThemeSettingsRepository::setAppLanguage,
|
onAppLanguageSelected = ThemeSettingsRepository::setAppLanguage,
|
||||||
episodeReleaseNotificationsUiState = episodeReleaseNotificationsUiState,
|
episodeReleaseNotificationsUiState = episodeReleaseNotificationsUiState,
|
||||||
|
|
@ -233,6 +242,9 @@ fun SettingsScreen(
|
||||||
onThemeSelected = ThemeSettingsRepository::setTheme,
|
onThemeSelected = ThemeSettingsRepository::setTheme,
|
||||||
amoledEnabled = amoledEnabled,
|
amoledEnabled = amoledEnabled,
|
||||||
onAmoledToggle = ThemeSettingsRepository::setAmoled,
|
onAmoledToggle = ThemeSettingsRepository::setAmoled,
|
||||||
|
liquidGlassNativeTabBarSupported = liquidGlassNativeTabBarSupported,
|
||||||
|
liquidGlassNativeTabBarEnabled = liquidGlassNativeTabBarEnabled,
|
||||||
|
onLiquidGlassNativeTabBarToggle = ThemeSettingsRepository::setLiquidGlassNativeTabBar,
|
||||||
selectedAppLanguage = selectedAppLanguage,
|
selectedAppLanguage = selectedAppLanguage,
|
||||||
onAppLanguageSelected = ThemeSettingsRepository::setAppLanguage,
|
onAppLanguageSelected = ThemeSettingsRepository::setAppLanguage,
|
||||||
episodeReleaseNotificationsUiState = episodeReleaseNotificationsUiState,
|
episodeReleaseNotificationsUiState = episodeReleaseNotificationsUiState,
|
||||||
|
|
@ -285,6 +297,9 @@ private fun MobileSettingsScreen(
|
||||||
onThemeSelected: (AppTheme) -> Unit,
|
onThemeSelected: (AppTheme) -> Unit,
|
||||||
amoledEnabled: Boolean,
|
amoledEnabled: Boolean,
|
||||||
onAmoledToggle: (Boolean) -> Unit,
|
onAmoledToggle: (Boolean) -> Unit,
|
||||||
|
liquidGlassNativeTabBarSupported: Boolean,
|
||||||
|
liquidGlassNativeTabBarEnabled: Boolean,
|
||||||
|
onLiquidGlassNativeTabBarToggle: (Boolean) -> Unit,
|
||||||
selectedAppLanguage: AppLanguage,
|
selectedAppLanguage: AppLanguage,
|
||||||
onAppLanguageSelected: (AppLanguage) -> Unit,
|
onAppLanguageSelected: (AppLanguage) -> Unit,
|
||||||
episodeReleaseNotificationsUiState: EpisodeReleaseNotificationsUiState,
|
episodeReleaseNotificationsUiState: EpisodeReleaseNotificationsUiState,
|
||||||
|
|
@ -366,6 +381,9 @@ private fun MobileSettingsScreen(
|
||||||
onThemeSelected = onThemeSelected,
|
onThemeSelected = onThemeSelected,
|
||||||
amoledEnabled = amoledEnabled,
|
amoledEnabled = amoledEnabled,
|
||||||
onAmoledToggle = onAmoledToggle,
|
onAmoledToggle = onAmoledToggle,
|
||||||
|
liquidGlassNativeTabBarSupported = liquidGlassNativeTabBarSupported,
|
||||||
|
liquidGlassNativeTabBarEnabled = liquidGlassNativeTabBarEnabled,
|
||||||
|
onLiquidGlassNativeTabBarToggle = onLiquidGlassNativeTabBarToggle,
|
||||||
selectedAppLanguage = selectedAppLanguage,
|
selectedAppLanguage = selectedAppLanguage,
|
||||||
onAppLanguageSelected = onAppLanguageSelected,
|
onAppLanguageSelected = onAppLanguageSelected,
|
||||||
onContinueWatchingClick = onContinueWatchingClick,
|
onContinueWatchingClick = onContinueWatchingClick,
|
||||||
|
|
@ -457,6 +475,9 @@ private fun TabletSettingsScreen(
|
||||||
onThemeSelected: (AppTheme) -> Unit,
|
onThemeSelected: (AppTheme) -> Unit,
|
||||||
amoledEnabled: Boolean,
|
amoledEnabled: Boolean,
|
||||||
onAmoledToggle: (Boolean) -> Unit,
|
onAmoledToggle: (Boolean) -> Unit,
|
||||||
|
liquidGlassNativeTabBarSupported: Boolean,
|
||||||
|
liquidGlassNativeTabBarEnabled: Boolean,
|
||||||
|
onLiquidGlassNativeTabBarToggle: (Boolean) -> Unit,
|
||||||
selectedAppLanguage: AppLanguage,
|
selectedAppLanguage: AppLanguage,
|
||||||
onAppLanguageSelected: (AppLanguage) -> Unit,
|
onAppLanguageSelected: (AppLanguage) -> Unit,
|
||||||
episodeReleaseNotificationsUiState: EpisodeReleaseNotificationsUiState,
|
episodeReleaseNotificationsUiState: EpisodeReleaseNotificationsUiState,
|
||||||
|
|
@ -539,6 +560,7 @@ private fun TabletSettingsScreen(
|
||||||
|
|
||||||
saveableStateHolder.SaveableStateProvider(page.name) {
|
saveableStateHolder.SaveableStateProvider(page.name) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
val bottomOverlayPadding = LocalNuvioBottomNavigationOverlayPadding.current
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|
@ -546,7 +568,7 @@ private fun TabletSettingsScreen(
|
||||||
start = 40.dp,
|
start = 40.dp,
|
||||||
top = topOffset,
|
top = topOffset,
|
||||||
end = 40.dp,
|
end = 40.dp,
|
||||||
bottom = 40.dp,
|
bottom = 40.dp + bottomOverlayPadding,
|
||||||
),
|
),
|
||||||
verticalArrangement = Arrangement.spacedBy(18.dp),
|
verticalArrangement = Arrangement.spacedBy(18.dp),
|
||||||
) {
|
) {
|
||||||
|
|
@ -609,6 +631,9 @@ private fun TabletSettingsScreen(
|
||||||
onThemeSelected = onThemeSelected,
|
onThemeSelected = onThemeSelected,
|
||||||
amoledEnabled = amoledEnabled,
|
amoledEnabled = amoledEnabled,
|
||||||
onAmoledToggle = onAmoledToggle,
|
onAmoledToggle = onAmoledToggle,
|
||||||
|
liquidGlassNativeTabBarSupported = liquidGlassNativeTabBarSupported,
|
||||||
|
liquidGlassNativeTabBarEnabled = liquidGlassNativeTabBarEnabled,
|
||||||
|
onLiquidGlassNativeTabBarToggle = onLiquidGlassNativeTabBarToggle,
|
||||||
selectedAppLanguage = selectedAppLanguage,
|
selectedAppLanguage = selectedAppLanguage,
|
||||||
onAppLanguageSelected = onAppLanguageSelected,
|
onAppLanguageSelected = onAppLanguageSelected,
|
||||||
onContinueWatchingClick = { openInlinePage(SettingsPage.ContinueWatching) },
|
onContinueWatchingClick = { openInlinePage(SettingsPage.ContinueWatching) },
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.nuvio.app.features.settings
|
package com.nuvio.app.features.settings
|
||||||
|
|
||||||
import com.nuvio.app.core.ui.AppTheme
|
import com.nuvio.app.core.ui.AppTheme
|
||||||
|
import com.nuvio.app.core.ui.NativeTabBridge
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
@ -12,6 +13,9 @@ object ThemeSettingsRepository {
|
||||||
private val _amoledEnabled = MutableStateFlow(false)
|
private val _amoledEnabled = MutableStateFlow(false)
|
||||||
val amoledEnabled: StateFlow<Boolean> = _amoledEnabled.asStateFlow()
|
val amoledEnabled: StateFlow<Boolean> = _amoledEnabled.asStateFlow()
|
||||||
|
|
||||||
|
private val _liquidGlassNativeTabBarEnabled = MutableStateFlow(false)
|
||||||
|
val liquidGlassNativeTabBarEnabled: StateFlow<Boolean> = _liquidGlassNativeTabBarEnabled.asStateFlow()
|
||||||
|
|
||||||
private val _selectedAppLanguage = MutableStateFlow(AppLanguage.ENGLISH)
|
private val _selectedAppLanguage = MutableStateFlow(AppLanguage.ENGLISH)
|
||||||
val selectedAppLanguage: StateFlow<AppLanguage> = _selectedAppLanguage.asStateFlow()
|
val selectedAppLanguage: StateFlow<AppLanguage> = _selectedAppLanguage.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -30,6 +34,8 @@ object ThemeSettingsRepository {
|
||||||
hasLoaded = false
|
hasLoaded = false
|
||||||
_selectedTheme.value = AppTheme.WHITE
|
_selectedTheme.value = AppTheme.WHITE
|
||||||
_amoledEnabled.value = false
|
_amoledEnabled.value = false
|
||||||
|
_liquidGlassNativeTabBarEnabled.value = false
|
||||||
|
NativeTabBridge.publishLiquidGlassEnabled(false)
|
||||||
_selectedAppLanguage.value = AppLanguage.ENGLISH
|
_selectedAppLanguage.value = AppLanguage.ENGLISH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,6 +53,9 @@ object ThemeSettingsRepository {
|
||||||
}
|
}
|
||||||
_selectedTheme.value = theme
|
_selectedTheme.value = theme
|
||||||
_amoledEnabled.value = ThemeSettingsStorage.loadAmoledEnabled() ?: false
|
_amoledEnabled.value = ThemeSettingsStorage.loadAmoledEnabled() ?: false
|
||||||
|
val liquidGlassEnabled = ThemeSettingsStorage.loadLiquidGlassNativeTabBarEnabled() ?: false
|
||||||
|
_liquidGlassNativeTabBarEnabled.value = liquidGlassEnabled
|
||||||
|
NativeTabBridge.publishLiquidGlassEnabled(liquidGlassEnabled)
|
||||||
val appLanguage = AppLanguage.fromCode(ThemeSettingsStorage.loadSelectedAppLanguage())
|
val appLanguage = AppLanguage.fromCode(ThemeSettingsStorage.loadSelectedAppLanguage())
|
||||||
ThemeSettingsStorage.applySelectedAppLanguage(appLanguage.code)
|
ThemeSettingsStorage.applySelectedAppLanguage(appLanguage.code)
|
||||||
_selectedAppLanguage.value = appLanguage
|
_selectedAppLanguage.value = appLanguage
|
||||||
|
|
@ -66,6 +75,14 @@ object ThemeSettingsRepository {
|
||||||
ThemeSettingsStorage.saveAmoledEnabled(enabled)
|
ThemeSettingsStorage.saveAmoledEnabled(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setLiquidGlassNativeTabBar(enabled: Boolean) {
|
||||||
|
ensureLoaded()
|
||||||
|
if (_liquidGlassNativeTabBarEnabled.value == enabled) return
|
||||||
|
_liquidGlassNativeTabBarEnabled.value = enabled
|
||||||
|
ThemeSettingsStorage.saveLiquidGlassNativeTabBarEnabled(enabled)
|
||||||
|
NativeTabBridge.publishLiquidGlassEnabled(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
fun setAppLanguage(language: AppLanguage) {
|
fun setAppLanguage(language: AppLanguage) {
|
||||||
ensureLoaded()
|
ensureLoaded()
|
||||||
if (_selectedAppLanguage.value == language) return
|
if (_selectedAppLanguage.value == language) return
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ internal expect object ThemeSettingsStorage {
|
||||||
fun saveSelectedTheme(themeName: String)
|
fun saveSelectedTheme(themeName: String)
|
||||||
fun loadAmoledEnabled(): Boolean?
|
fun loadAmoledEnabled(): Boolean?
|
||||||
fun saveAmoledEnabled(enabled: Boolean)
|
fun saveAmoledEnabled(enabled: Boolean)
|
||||||
|
fun loadLiquidGlassNativeTabBarEnabled(): Boolean?
|
||||||
|
fun saveLiquidGlassNativeTabBarEnabled(enabled: Boolean)
|
||||||
fun loadSelectedAppLanguage(): String?
|
fun loadSelectedAppLanguage(): String?
|
||||||
fun saveSelectedAppLanguage(languageCode: String)
|
fun saveSelectedAppLanguage(languageCode: String)
|
||||||
fun applySelectedAppLanguage(languageCode: String)
|
fun applySelectedAppLanguage(languageCode: String)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.nuvio.app.core.ui
|
||||||
|
|
||||||
|
import platform.Foundation.NSNotificationCenter
|
||||||
|
import platform.Foundation.NSUserDefaults
|
||||||
|
import platform.UIKit.UIDevice
|
||||||
|
import platform.UIKit.UIUserInterfaceIdiomPhone
|
||||||
|
|
||||||
|
private const val liquidGlassNativeTabBarEnabledKey = "NuvioLiquidGlassNativeTabBarEnabled"
|
||||||
|
private const val nativeTabBarVisibleKey = "NuvioNativeTabBarVisible"
|
||||||
|
private const val nativeSelectedTabKey = "NuvioNativeSelectedTab"
|
||||||
|
private const val nativeTabChromeDidChangeNotification = "NuvioNativeTabChromeDidChange"
|
||||||
|
|
||||||
|
internal actual fun isLiquidGlassNativeTabBarSupported(): Boolean {
|
||||||
|
return UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone &&
|
||||||
|
(UIDevice.currentDevice.systemVersion.substringBefore(".").toIntOrNull() ?: 0) >= 26
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual fun publishLiquidGlassNativeTabBarEnabled(enabled: Boolean) {
|
||||||
|
publishBool(liquidGlassNativeTabBarEnabledKey, enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual fun publishNativeTabBarVisible(visible: Boolean) {
|
||||||
|
publishBool(nativeTabBarVisibleKey, visible)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual fun publishNativeSelectedTab(tabName: String) {
|
||||||
|
NSUserDefaults.standardUserDefaults.setObject(tabName, forKey = nativeSelectedTabKey)
|
||||||
|
notifyNativeTabChromeChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun publishBool(key: String, value: Boolean) {
|
||||||
|
NSUserDefaults.standardUserDefaults.setBool(value, forKey = key)
|
||||||
|
notifyNativeTabChromeChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyNativeTabChromeChanged() {
|
||||||
|
NSNotificationCenter.defaultCenter.postNotificationName(nativeTabChromeDidChangeNotification, null)
|
||||||
|
}
|
||||||
|
|
@ -13,8 +13,13 @@ import platform.Foundation.NSUserDefaults
|
||||||
actual object ThemeSettingsStorage {
|
actual object ThemeSettingsStorage {
|
||||||
private const val selectedThemeKey = "selected_theme"
|
private const val selectedThemeKey = "selected_theme"
|
||||||
private const val amoledEnabledKey = "amoled_enabled"
|
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 selectedAppLanguageKey = "selected_app_language"
|
||||||
private val profileScopedSyncKeys = listOf(selectedThemeKey, amoledEnabledKey)
|
private val profileScopedSyncKeys = listOf(
|
||||||
|
selectedThemeKey,
|
||||||
|
amoledEnabledKey,
|
||||||
|
liquidGlassNativeTabBarEnabledKey,
|
||||||
|
)
|
||||||
private val globalSyncKeys = listOf(selectedAppLanguageKey)
|
private val globalSyncKeys = listOf(selectedAppLanguageKey)
|
||||||
|
|
||||||
actual fun loadSelectedTheme(): String? =
|
actual fun loadSelectedTheme(): String? =
|
||||||
|
|
@ -38,6 +43,23 @@ actual object ThemeSettingsStorage {
|
||||||
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(amoledEnabledKey))
|
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(amoledEnabledKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun loadLiquidGlassNativeTabBarEnabled(): Boolean? {
|
||||||
|
val defaults = NSUserDefaults.standardUserDefaults
|
||||||
|
val key = ProfileScopedKey.of(liquidGlassNativeTabBarEnabledKey)
|
||||||
|
return if (defaults.objectForKey(key) != null) {
|
||||||
|
defaults.boolForKey(key)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun saveLiquidGlassNativeTabBarEnabled(enabled: Boolean) {
|
||||||
|
NSUserDefaults.standardUserDefaults.setBool(
|
||||||
|
enabled,
|
||||||
|
forKey = ProfileScopedKey.of(liquidGlassNativeTabBarEnabledKey),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
actual fun loadSelectedAppLanguage(): String? {
|
actual fun loadSelectedAppLanguage(): String? {
|
||||||
val value = NSUserDefaults.standardUserDefaults.stringForKey(selectedAppLanguageKey)
|
val value = NSUserDefaults.standardUserDefaults.stringForKey(selectedAppLanguageKey)
|
||||||
if (value != null) return value
|
if (value != null) return value
|
||||||
|
|
@ -65,6 +87,7 @@ actual object ThemeSettingsStorage {
|
||||||
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
|
||||||
loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) }
|
loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) }
|
||||||
loadAmoledEnabled()?.let { put(amoledEnabledKey, encodeSyncBoolean(it)) }
|
loadAmoledEnabled()?.let { put(amoledEnabledKey, encodeSyncBoolean(it)) }
|
||||||
|
loadLiquidGlassNativeTabBarEnabled()?.let { put(liquidGlassNativeTabBarEnabledKey, encodeSyncBoolean(it)) }
|
||||||
loadSelectedAppLanguage()?.let { put(selectedAppLanguageKey, encodeSyncString(it)) }
|
loadSelectedAppLanguage()?.let { put(selectedAppLanguageKey, encodeSyncString(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,6 +101,7 @@ actual object ThemeSettingsStorage {
|
||||||
|
|
||||||
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
|
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
|
||||||
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)
|
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)
|
||||||
|
payload.decodeSyncBoolean(liquidGlassNativeTabBarEnabledKey)?.let(::saveLiquidGlassNativeTabBarEnabled)
|
||||||
payload.decodeSyncString(selectedAppLanguageKey)?.let(::saveSelectedAppLanguage)
|
payload.decodeSyncString(selectedAppLanguageKey)?.let(::saveSelectedAppLanguage)
|
||||||
applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.ENGLISH.code)
|
applySelectedAppLanguage(loadSelectedAppLanguage() ?: AppLanguage.ENGLISH.code)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
CURRENT_PROJECT_VERSION=54
|
CURRENT_PROJECT_VERSION=54
|
||||||
MARKETING_VERSION=0.1.15
|
MARKETING_VERSION=0.1.0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,57 @@ import UIKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ComposeApp
|
import ComposeApp
|
||||||
|
|
||||||
final class RootComposeViewController: UIViewController {
|
final class RootComposeViewController: UIViewController, UITabBarDelegate {
|
||||||
|
private enum NativeTab: String, CaseIterable {
|
||||||
|
case home = "Home"
|
||||||
|
case search = "Search"
|
||||||
|
case library = "Library"
|
||||||
|
case settings = "Settings"
|
||||||
|
|
||||||
|
var tag: Int {
|
||||||
|
switch self {
|
||||||
|
case .home: return 0
|
||||||
|
case .search: return 1
|
||||||
|
case .library: return 2
|
||||||
|
case .settings: return 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .home: return "Home"
|
||||||
|
case .search: return "Search"
|
||||||
|
case .library: return "Library"
|
||||||
|
case .settings: return "Profile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var systemImageName: String {
|
||||||
|
switch self {
|
||||||
|
case .home: return "house"
|
||||||
|
case .search: return "magnifyingglass"
|
||||||
|
case .library: return "books.vertical"
|
||||||
|
case .settings: return "person.crop.circle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(tag: Int) {
|
||||||
|
guard let tab = Self.allCases.first(where: { $0.tag == tag }) else { return nil }
|
||||||
|
self = tab
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let liquidGlassEnabledKey = "NuvioLiquidGlassNativeTabBarEnabled"
|
||||||
|
private static let nativeTabBarVisibleKey = "NuvioNativeTabBarVisible"
|
||||||
|
private static let nativeSelectedTabKey = "NuvioNativeSelectedTab"
|
||||||
|
private static let nativeTabChromeDidChangeNotification = Notification.Name("NuvioNativeTabChromeDidChange")
|
||||||
|
|
||||||
private let contentController: UIViewController
|
private let contentController: UIViewController
|
||||||
|
private let tabBar = UITabBar()
|
||||||
|
private var contentBottomToViewBottom: NSLayoutConstraint?
|
||||||
|
private var tabBarHeightConstraint: NSLayoutConstraint?
|
||||||
|
private var userDefaultsObserver: NSObjectProtocol?
|
||||||
|
private var tabChromeObserver: NSObjectProtocol?
|
||||||
|
|
||||||
init(contentController: UIViewController) {
|
init(contentController: UIViewController) {
|
||||||
self.contentController = contentController
|
self.contentController = contentController
|
||||||
|
|
@ -20,17 +69,44 @@ final class RootComposeViewController: UIViewController {
|
||||||
|
|
||||||
view.backgroundColor = .black
|
view.backgroundColor = .black
|
||||||
contentController.view.backgroundColor = .black
|
contentController.view.backgroundColor = .black
|
||||||
|
UserDefaults.standard.set(false, forKey: Self.nativeTabBarVisibleKey)
|
||||||
|
|
||||||
addChild(contentController)
|
addChild(contentController)
|
||||||
view.addSubview(contentController.view)
|
view.addSubview(contentController.view)
|
||||||
contentController.view.translatesAutoresizingMaskIntoConstraints = false
|
contentController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
let bottomToViewBottom = contentController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||||
|
self.contentBottomToViewBottom = bottomToViewBottom
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
contentController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
contentController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
contentController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
contentController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
contentController.view.topAnchor.constraint(equalTo: view.topAnchor),
|
contentController.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
contentController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
bottomToViewBottom,
|
||||||
])
|
])
|
||||||
contentController.didMove(toParent: self)
|
contentController.didMove(toParent: self)
|
||||||
|
|
||||||
|
configureNativeTabBar()
|
||||||
|
installNativeTabObservers()
|
||||||
|
syncNativeTabChrome(animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if let userDefaultsObserver {
|
||||||
|
NotificationCenter.default.removeObserver(userDefaultsObserver)
|
||||||
|
}
|
||||||
|
if let tabChromeObserver {
|
||||||
|
NotificationCenter.default.removeObserver(tabChromeObserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewSafeAreaInsetsDidChange() {
|
||||||
|
super.viewSafeAreaInsetsDidChange()
|
||||||
|
updateTabBarHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
|
||||||
|
guard let tab = NativeTab(tag: item.tag) else { return }
|
||||||
|
UserDefaults.standard.set(tab.rawValue, forKey: Self.nativeSelectedTabKey)
|
||||||
|
NativeTabBridgeKt.nativeTabSelect(tabName: tab.rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
override var childForHomeIndicatorAutoHidden: UIViewController? {
|
override var childForHomeIndicatorAutoHidden: UIViewController? {
|
||||||
|
|
@ -88,6 +164,107 @@ final class RootComposeViewController: UIViewController {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var nativeTabsSupported: Bool {
|
||||||
|
UIDevice.current.userInterfaceIdiom == .phone &&
|
||||||
|
ProcessInfo.processInfo.operatingSystemVersion.majorVersion >= 26
|
||||||
|
}
|
||||||
|
|
||||||
|
private var shouldShowNativeTabBar: Bool {
|
||||||
|
nativeTabsSupported &&
|
||||||
|
UserDefaults.standard.bool(forKey: Self.liquidGlassEnabledKey) &&
|
||||||
|
UserDefaults.standard.bool(forKey: Self.nativeTabBarVisibleKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureNativeTabBar() {
|
||||||
|
tabBar.delegate = self
|
||||||
|
tabBar.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
tabBar.items = NativeTab.allCases.map { tab in
|
||||||
|
UITabBarItem(
|
||||||
|
title: tab.title,
|
||||||
|
image: UIImage(systemName: tab.systemImageName),
|
||||||
|
tag: tab.tag
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tabBar.selectedItem = tabBar.items?.first
|
||||||
|
tabBar.alpha = 0
|
||||||
|
tabBar.isHidden = true
|
||||||
|
|
||||||
|
view.addSubview(tabBar)
|
||||||
|
let heightConstraint = tabBar.heightAnchor.constraint(equalToConstant: tabBarHeight)
|
||||||
|
tabBarHeightConstraint = heightConstraint
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
tabBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
tabBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
tabBar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
heightConstraint,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func installNativeTabObservers() {
|
||||||
|
userDefaultsObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: UserDefaults.didChangeNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.syncNativeTabChrome(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
tabChromeObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: Self.nativeTabChromeDidChangeNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.syncNativeTabChrome(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var tabBarHeight: CGFloat {
|
||||||
|
49 + view.safeAreaInsets.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTabBarHeight() {
|
||||||
|
tabBarHeightConstraint?.constant = tabBarHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
private func syncNativeTabChrome(animated: Bool) {
|
||||||
|
updateTabBarHeight()
|
||||||
|
syncSelectedNativeTab()
|
||||||
|
|
||||||
|
let visible = shouldShowNativeTabBar
|
||||||
|
contentBottomToViewBottom?.isActive = true
|
||||||
|
if visible {
|
||||||
|
tabBar.isHidden = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let changes = {
|
||||||
|
self.tabBar.alpha = visible ? 1 : 0
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
let completion: (Bool) -> Void = { _ in
|
||||||
|
self.tabBar.isHidden = !visible
|
||||||
|
}
|
||||||
|
|
||||||
|
if animated && view.window != nil {
|
||||||
|
UIView.animate(
|
||||||
|
withDuration: 0.22,
|
||||||
|
delay: 0,
|
||||||
|
options: [.beginFromCurrentState, .curveEaseInOut],
|
||||||
|
animations: changes,
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
changes()
|
||||||
|
completion(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func syncSelectedNativeTab() {
|
||||||
|
let rawValue = UserDefaults.standard.string(forKey: Self.nativeSelectedTabKey) ?? NativeTab.home.rawValue
|
||||||
|
let selectedTab = NativeTab(rawValue: rawValue) ?? .home
|
||||||
|
tabBar.selectedItem = tabBar.items?.first(where: { $0.tag == selectedTab.tag })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ComposeView: UIViewControllerRepresentable {
|
struct ComposeView: UIViewControllerRepresentable {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue