fix: localising desktop

This commit is contained in:
tapframe 2026-05-13 00:03:01 +05:30
parent b30ad31b37
commit b1b5a82fce
5 changed files with 56 additions and 7 deletions

View file

@ -100,7 +100,9 @@ actual object ThemeSettingsStorage {
actual fun replaceFromSyncPayload(payload: JsonObject) { actual fun replaceFromSyncPayload(payload: JsonObject) {
preferences?.edit()?.apply { preferences?.edit()?.apply {
profileScopedSyncKeys.forEach { remove(ProfileScopedKey.of(it)) } profileScopedSyncKeys.forEach { remove(ProfileScopedKey.of(it)) }
globalSyncKeys.forEach { remove(it) } globalSyncKeys
.filter(payload::containsKey)
.forEach { remove(it) }
}?.apply() }?.apply()
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme) payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)

View file

@ -55,6 +55,7 @@ import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
private const val PUSH_DEBOUNCE_MS = 1500L private const val PUSH_DEBOUNCE_MS = 1500L
private const val APP_LANGUAGE_SYNC_KEY = "selected_app_language"
object ProfileSettingsSync { object ProfileSettingsSync {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
@ -74,6 +75,9 @@ object ProfileSettingsSync {
@Volatile @Volatile
private var skipNextPushSignature: String? = null private var skipNextPushSignature: String? = null
@Volatile
private var pendingLocalAppLanguageCode: String? = null
private var observeJob: Job? = null private var observeJob: Job? = null
fun startObserving() { fun startObserving() {
@ -121,7 +125,7 @@ object ProfileSettingsSync {
return@withLock false return@withLock false
} }
applyRemoteBlob(remoteBlob) applyRemoteBlob(remoteBlob.withPendingLocalAppLanguage())
skipNextPushSignature = currentObservedStateSignature() skipNextPushSignature = currentObservedStateSignature()
} finally { } finally {
isApplyingRemoteBlob = false isApplyingRemoteBlob = false
@ -140,21 +144,31 @@ object ProfileSettingsSync {
suspend fun pushCurrentProfileToRemote() { suspend fun pushCurrentProfileToRemote() {
ensureRepositoriesLoaded() ensureRepositoriesLoaded()
val authState = AuthRepository.state.value
if (authState !is AuthState.Authenticated || authState.isAnonymous) return
syncMutex.withLock { syncMutex.withLock {
runCatching { runCatching {
pushToRemoteLocked(ProfileRepository.activeProfileId, exportSettingsBlob()) pushToRemoteLocked(ProfileRepository.activeProfileId, exportSettingsBlob())
if (pendingLocalAppLanguageCode == ThemeSettingsRepository.selectedAppLanguage.value.code) {
pendingLocalAppLanguageCode = null
}
}.onFailure { error -> }.onFailure { error ->
log.e(error) { "pushCurrentProfileToRemote() — FAILED" } log.e(error) { "pushCurrentProfileToRemote() — FAILED" }
} }
} }
} }
fun markAppLanguageChanged() {
pendingLocalAppLanguageCode = ThemeSettingsRepository.selectedAppLanguage.value.code
}
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
private fun observeLocalChangesAndPush() { private fun observeLocalChangesAndPush() {
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" }, ThemeSettingsRepository.liquidGlassNativeTabBarEnabled.map { "liquid_glass_tab_bar" },
ThemeSettingsRepository.selectedAppLanguage.map { "app_language" },
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" },
@ -275,6 +289,7 @@ object ProfileSettingsSync {
"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}", "liquid_glass_tab_bar=${ThemeSettingsRepository.liquidGlassNativeTabBarEnabled.value}",
"app_language=${ThemeSettingsRepository.selectedAppLanguage.value.code}",
"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}",
@ -286,6 +301,18 @@ object ProfileSettingsSync {
"trakt_comments=${TraktCommentsSettings.enabled.value}", "trakt_comments=${TraktCommentsSettings.enabled.value}",
"episode_release_alerts=${EpisodeReleaseNotificationsRepository.uiState.value.isEnabled}", "episode_release_alerts=${EpisodeReleaseNotificationsRepository.uiState.value.isEnabled}",
).joinToString(separator = "||") ).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 @Serializable

View file

@ -47,6 +47,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp 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.sync.ProfileSettingsSync
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.LocalNuvioBottomNavigationOverlayPadding
import com.nuvio.app.core.ui.NuvioScreen import com.nuvio.app.core.ui.NuvioScreen
@ -119,6 +120,16 @@ fun SettingsScreen(
}.collectAsStateWithLifecycle() }.collectAsStateWithLifecycle()
val liquidGlassNativeTabBarSupported = remember { isLiquidGlassNativeTabBarSupported() } val liquidGlassNativeTabBarSupported = remember { isLiquidGlassNativeTabBarSupported() }
val selectedAppLanguage by remember { ThemeSettingsRepository.selectedAppLanguage }.collectAsStateWithLifecycle() 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 { val tmdbSettings by remember {
TmdbSettingsRepository.ensureLoaded() TmdbSettingsRepository.ensureLoaded()
TmdbSettingsRepository.uiState TmdbSettingsRepository.uiState
@ -228,7 +239,7 @@ fun SettingsScreen(
liquidGlassNativeTabBarEnabled = liquidGlassNativeTabBarEnabled, liquidGlassNativeTabBarEnabled = liquidGlassNativeTabBarEnabled,
onLiquidGlassNativeTabBarToggle = ThemeSettingsRepository::setLiquidGlassNativeTabBar, onLiquidGlassNativeTabBarToggle = ThemeSettingsRepository::setLiquidGlassNativeTabBar,
selectedAppLanguage = selectedAppLanguage, selectedAppLanguage = selectedAppLanguage,
onAppLanguageSelected = ThemeSettingsRepository::setAppLanguage, onAppLanguageSelected = onAppLanguageSelected,
episodeReleaseNotificationsUiState = episodeReleaseNotificationsUiState, episodeReleaseNotificationsUiState = episodeReleaseNotificationsUiState,
tmdbSettings = tmdbSettings, tmdbSettings = tmdbSettings,
mdbListSettings = mdbListSettings, mdbListSettings = mdbListSettings,
@ -275,7 +286,7 @@ fun SettingsScreen(
liquidGlassNativeTabBarEnabled = liquidGlassNativeTabBarEnabled, liquidGlassNativeTabBarEnabled = liquidGlassNativeTabBarEnabled,
onLiquidGlassNativeTabBarToggle = ThemeSettingsRepository::setLiquidGlassNativeTabBar, onLiquidGlassNativeTabBarToggle = ThemeSettingsRepository::setLiquidGlassNativeTabBar,
selectedAppLanguage = selectedAppLanguage, selectedAppLanguage = selectedAppLanguage,
onAppLanguageSelected = ThemeSettingsRepository::setAppLanguage, onAppLanguageSelected = onAppLanguageSelected,
episodeReleaseNotificationsUiState = episodeReleaseNotificationsUiState, episodeReleaseNotificationsUiState = episodeReleaseNotificationsUiState,
tmdbSettings = tmdbSettings, tmdbSettings = tmdbSettings,
mdbListSettings = mdbListSettings, mdbListSettings = mdbListSettings,

View file

@ -18,6 +18,7 @@ import nuvio.composeapp.generated.resources.mdblist_logo
import nuvio.composeapp.generated.resources.rating_tmdb import nuvio.composeapp.generated.resources.rating_tmdb
import nuvio.composeapp.generated.resources.trakt_tv_favicon import nuvio.composeapp.generated.resources.trakt_tv_favicon
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import java.util.Locale
internal actual object ThemeSettingsStorage { internal actual object ThemeSettingsStorage {
private const val preferencesName = "nuvio_theme_settings" private const val preferencesName = "nuvio_theme_settings"
@ -65,7 +66,13 @@ internal actual object ThemeSettingsStorage {
DesktopPreferences.putString(preferencesName, selectedAppLanguageKey, languageCode) 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 { actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) } loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) }
@ -76,7 +83,9 @@ internal actual object ThemeSettingsStorage {
actual fun replaceFromSyncPayload(payload: JsonObject) { actual fun replaceFromSyncPayload(payload: JsonObject) {
profileScopedSyncKeys.forEach { DesktopPreferences.remove(preferencesName, ProfileScopedKey.of(it)) } 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.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled) payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)

View file

@ -95,7 +95,7 @@ actual object ThemeSettingsStorage {
profileScopedSyncKeys.forEach { key -> profileScopedSyncKeys.forEach { key ->
NSUserDefaults.standardUserDefaults.removeObjectForKey(ProfileScopedKey.of(key)) NSUserDefaults.standardUserDefaults.removeObjectForKey(ProfileScopedKey.of(key))
} }
globalSyncKeys.forEach { key -> globalSyncKeys.filter(payload::containsKey).forEach { key ->
NSUserDefaults.standardUserDefaults.removeObjectForKey(key) NSUserDefaults.standardUserDefaults.removeObjectForKey(key)
} }