From b1b5a82fce3aadd807db94aa74c3ae36582e2ce0 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Wed, 13 May 2026 00:03:01 +0530 Subject: [PATCH] fix: localising desktop --- .../settings/ThemeSettingsStorage.android.kt | 4 ++- .../app/core/sync/ProfileSettingsSync.kt | 29 ++++++++++++++++++- .../app/features/settings/SettingsScreen.kt | 15 ++++++++-- .../settings/SettingsDesktop.desktop.kt | 13 +++++++-- .../settings/ThemeSettingsStorage.ios.kt | 2 +- 5 files changed, 56 insertions(+), 7 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.android.kt index e082a536..c268f897 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 @@ -100,7 +100,9 @@ actual object ThemeSettingsStorage { actual fun replaceFromSyncPayload(payload: JsonObject) { preferences?.edit()?.apply { profileScopedSyncKeys.forEach { remove(ProfileScopedKey.of(it)) } - globalSyncKeys.forEach { remove(it) } + globalSyncKeys + .filter(payload::containsKey) + .forEach { remove(it) } }?.apply() payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt index 58df719e..030ef17c 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/sync/ProfileSettingsSync.kt @@ -55,6 +55,7 @@ import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.put private const val PUSH_DEBOUNCE_MS = 1500L +private const val APP_LANGUAGE_SYNC_KEY = "selected_app_language" object ProfileSettingsSync { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -74,6 +75,9 @@ object ProfileSettingsSync { @Volatile private var skipNextPushSignature: String? = null + @Volatile + private var pendingLocalAppLanguageCode: String? = null + private var observeJob: Job? = null fun startObserving() { @@ -121,7 +125,7 @@ object ProfileSettingsSync { return@withLock false } - applyRemoteBlob(remoteBlob) + applyRemoteBlob(remoteBlob.withPendingLocalAppLanguage()) skipNextPushSignature = currentObservedStateSignature() } finally { isApplyingRemoteBlob = false @@ -140,21 +144,31 @@ object ProfileSettingsSync { suspend fun pushCurrentProfileToRemote() { ensureRepositoriesLoaded() + val authState = AuthRepository.state.value + if (authState !is AuthState.Authenticated || authState.isAnonymous) return syncMutex.withLock { runCatching { pushToRemoteLocked(ProfileRepository.activeProfileId, exportSettingsBlob()) + if (pendingLocalAppLanguageCode == ThemeSettingsRepository.selectedAppLanguage.value.code) { + pendingLocalAppLanguageCode = null + } }.onFailure { error -> log.e(error) { "pushCurrentProfileToRemote() — FAILED" } } } } + fun markAppLanguageChanged() { + pendingLocalAppLanguageCode = ThemeSettingsRepository.selectedAppLanguage.value.code + } + @OptIn(FlowPreview::class) private fun observeLocalChangesAndPush() { val signatureFlows = listOf( ThemeSettingsRepository.selectedTheme.map { "theme" }, ThemeSettingsRepository.amoledEnabled.map { "amoled" }, ThemeSettingsRepository.liquidGlassNativeTabBarEnabled.map { "liquid_glass_tab_bar" }, + ThemeSettingsRepository.selectedAppLanguage.map { "app_language" }, PosterCardStyleRepository.uiState.map { "poster_card_style" }, PlayerSettingsRepository.uiState.map { "player" }, TmdbSettingsRepository.uiState.map { "tmdb" }, @@ -275,6 +289,7 @@ object ProfileSettingsSync { "theme=${ThemeSettingsRepository.selectedTheme.value.name}", "amoled=${ThemeSettingsRepository.amoledEnabled.value}", "liquid_glass_tab_bar=${ThemeSettingsRepository.liquidGlassNativeTabBarEnabled.value}", + "app_language=${ThemeSettingsRepository.selectedAppLanguage.value.code}", "poster_card_style=${PosterCardStyleRepository.uiState.value}", "player=${PlayerSettingsRepository.uiState.value}", "tmdb=${TmdbSettingsRepository.uiState.value}", @@ -286,6 +301,18 @@ object ProfileSettingsSync { "trakt_comments=${TraktCommentsSettings.enabled.value}", "episode_release_alerts=${EpisodeReleaseNotificationsRepository.uiState.value.isEnabled}", ).joinToString(separator = "||") + + private fun MobileProfileSettingsBlob.withPendingLocalAppLanguage(): MobileProfileSettingsBlob { + val languageCode = pendingLocalAppLanguageCode ?: return this + return copy( + features = features.copy( + themeSettings = buildJsonObject { + features.themeSettings.forEach { (key, value) -> put(key, value) } + put(APP_LANGUAGE_SYNC_KEY, encodeSyncString(languageCode)) + }, + ), + ) + } } @Serializable 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 4cd95d64..ba819fbe 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 @@ -47,6 +47,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.nuvio.app.core.sync.ProfileSettingsSync import com.nuvio.app.core.ui.AppTheme import com.nuvio.app.core.ui.LocalNuvioBottomNavigationOverlayPadding import com.nuvio.app.core.ui.NuvioScreen @@ -119,6 +120,16 @@ fun SettingsScreen( }.collectAsStateWithLifecycle() val liquidGlassNativeTabBarSupported = remember { isLiquidGlassNativeTabBarSupported() } val selectedAppLanguage by remember { ThemeSettingsRepository.selectedAppLanguage }.collectAsStateWithLifecycle() + val settingsSyncScope = rememberCoroutineScope() + val onAppLanguageSelected: (AppLanguage) -> Unit = remember(settingsSyncScope) { + { language -> + ThemeSettingsRepository.setAppLanguage(language) + ProfileSettingsSync.markAppLanguageChanged() + settingsSyncScope.launch { + ProfileSettingsSync.pushCurrentProfileToRemote() + } + } + } val tmdbSettings by remember { TmdbSettingsRepository.ensureLoaded() TmdbSettingsRepository.uiState @@ -228,7 +239,7 @@ fun SettingsScreen( liquidGlassNativeTabBarEnabled = liquidGlassNativeTabBarEnabled, onLiquidGlassNativeTabBarToggle = ThemeSettingsRepository::setLiquidGlassNativeTabBar, selectedAppLanguage = selectedAppLanguage, - onAppLanguageSelected = ThemeSettingsRepository::setAppLanguage, + onAppLanguageSelected = onAppLanguageSelected, episodeReleaseNotificationsUiState = episodeReleaseNotificationsUiState, tmdbSettings = tmdbSettings, mdbListSettings = mdbListSettings, @@ -275,7 +286,7 @@ fun SettingsScreen( liquidGlassNativeTabBarEnabled = liquidGlassNativeTabBarEnabled, onLiquidGlassNativeTabBarToggle = ThemeSettingsRepository::setLiquidGlassNativeTabBar, selectedAppLanguage = selectedAppLanguage, - onAppLanguageSelected = ThemeSettingsRepository::setAppLanguage, + onAppLanguageSelected = onAppLanguageSelected, episodeReleaseNotificationsUiState = episodeReleaseNotificationsUiState, tmdbSettings = tmdbSettings, mdbListSettings = mdbListSettings, diff --git a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/settings/SettingsDesktop.desktop.kt b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/settings/SettingsDesktop.desktop.kt index 9dc180c8..e2ede04c 100644 --- a/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/settings/SettingsDesktop.desktop.kt +++ b/composeApp/src/desktopMain/kotlin/com/nuvio/app/features/settings/SettingsDesktop.desktop.kt @@ -18,6 +18,7 @@ import nuvio.composeapp.generated.resources.mdblist_logo import nuvio.composeapp.generated.resources.rating_tmdb import nuvio.composeapp.generated.resources.trakt_tv_favicon import org.jetbrains.compose.resources.painterResource +import java.util.Locale internal actual object ThemeSettingsStorage { private const val preferencesName = "nuvio_theme_settings" @@ -65,7 +66,13 @@ internal actual object ThemeSettingsStorage { DesktopPreferences.putString(preferencesName, selectedAppLanguageKey, languageCode) } - actual fun applySelectedAppLanguage(languageCode: String) = Unit + actual fun applySelectedAppLanguage(languageCode: String) { + val normalizedCode = languageCode + .trim() + .takeIf { it.isNotBlank() } + ?: AppLanguage.ENGLISH.code + Locale.setDefault(Locale.forLanguageTag(normalizedCode)) + } actual fun exportToSyncPayload(): JsonObject = buildJsonObject { loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) } @@ -76,7 +83,9 @@ internal actual object ThemeSettingsStorage { actual fun replaceFromSyncPayload(payload: JsonObject) { profileScopedSyncKeys.forEach { DesktopPreferences.remove(preferencesName, ProfileScopedKey.of(it)) } - globalSyncKeys.forEach { DesktopPreferences.remove(preferencesName, it) } + globalSyncKeys + .filter(payload::containsKey) + .forEach { DesktopPreferences.remove(preferencesName, it) } payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme) payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled) diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/settings/ThemeSettingsStorage.ios.kt index f66f8b8c..8fa4d2ed 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 @@ -95,7 +95,7 @@ actual object ThemeSettingsStorage { profileScopedSyncKeys.forEach { key -> NSUserDefaults.standardUserDefaults.removeObjectForKey(ProfileScopedKey.of(key)) } - globalSyncKeys.forEach { key -> + globalSyncKeys.filter(payload::containsKey).forEach { key -> NSUserDefaults.standardUserDefaults.removeObjectForKey(key) }