feat: Implement sync payload encoding and decoding for various settings

This commit is contained in:
tapframe 2026-04-09 00:16:04 +05:30
parent 26b7edee2a
commit 9de7f8a34d
17 changed files with 737 additions and 473 deletions

View file

@ -2,7 +2,14 @@ package com.nuvio.app.features.mdblist
import android.content.Context
import android.content.SharedPreferences
import com.nuvio.app.core.sync.decodeSyncBoolean
import com.nuvio.app.core.sync.decodeSyncString
import com.nuvio.app.core.sync.encodeSyncBoolean
import com.nuvio.app.core.sync.encodeSyncString
import com.nuvio.app.core.storage.ProfileScopedKey
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
actual object MdbListSettingsStorage {
private const val preferencesName = "nuvio_mdblist_settings"
@ -15,6 +22,17 @@ actual object MdbListSettingsStorage {
private const val useTraktKey = "mdblist_use_trakt"
private const val useLetterboxdKey = "mdblist_use_letterboxd"
private const val useAudienceKey = "mdblist_use_audience"
private val syncKeys = listOf(
enabledKey,
apiKey,
useImdbKey,
useTmdbKey,
useTomatoesKey,
useMetacriticKey,
useTraktKey,
useLetterboxdKey,
useAudienceKey,
)
private var preferences: SharedPreferences? = null
@ -96,4 +114,32 @@ actual object MdbListSettingsStorage {
?.putBoolean(ProfileScopedKey.of(key), enabled)
?.apply()
}
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
loadEnabled()?.let { put(enabledKey, encodeSyncBoolean(it)) }
loadApiKey()?.let { put(apiKey, encodeSyncString(it)) }
loadUseImdb()?.let { put(useImdbKey, encodeSyncBoolean(it)) }
loadUseTmdb()?.let { put(useTmdbKey, encodeSyncBoolean(it)) }
loadUseTomatoes()?.let { put(useTomatoesKey, encodeSyncBoolean(it)) }
loadUseMetacritic()?.let { put(useMetacriticKey, encodeSyncBoolean(it)) }
loadUseTrakt()?.let { put(useTraktKey, encodeSyncBoolean(it)) }
loadUseLetterboxd()?.let { put(useLetterboxdKey, encodeSyncBoolean(it)) }
loadUseAudience()?.let { put(useAudienceKey, encodeSyncBoolean(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
preferences?.edit()?.apply {
syncKeys.forEach { remove(ProfileScopedKey.of(it)) }
}?.apply()
payload.decodeSyncBoolean(enabledKey)?.let(::saveEnabled)
payload.decodeSyncString(apiKey)?.let(::saveApiKey)
payload.decodeSyncBoolean(useImdbKey)?.let(::saveUseImdb)
payload.decodeSyncBoolean(useTmdbKey)?.let(::saveUseTmdb)
payload.decodeSyncBoolean(useTomatoesKey)?.let(::saveUseTomatoes)
payload.decodeSyncBoolean(useMetacriticKey)?.let(::saveUseMetacritic)
payload.decodeSyncBoolean(useTraktKey)?.let(::saveUseTrakt)
payload.decodeSyncBoolean(useLetterboxdKey)?.let(::saveUseLetterboxd)
payload.decodeSyncBoolean(useAudienceKey)?.let(::saveUseAudience)
}
}

View file

@ -2,7 +2,20 @@ package com.nuvio.app.features.player
import android.content.Context
import android.content.SharedPreferences
import com.nuvio.app.core.sync.decodeSyncBoolean
import com.nuvio.app.core.sync.decodeSyncFloat
import com.nuvio.app.core.sync.decodeSyncInt
import com.nuvio.app.core.sync.decodeSyncString
import com.nuvio.app.core.sync.decodeSyncStringSet
import com.nuvio.app.core.sync.encodeSyncBoolean
import com.nuvio.app.core.sync.encodeSyncFloat
import com.nuvio.app.core.sync.encodeSyncInt
import com.nuvio.app.core.sync.encodeSyncString
import com.nuvio.app.core.sync.encodeSyncStringSet
import com.nuvio.app.core.storage.ProfileScopedKey
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
actual object PlayerSettingsStorage {
private const val preferencesName = "nuvio_player_settings"
@ -36,6 +49,38 @@ actual object PlayerSettingsStorage {
private const val nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
private const val useLibassKey = "use_libass"
private const val libassRenderTypeKey = "libass_render_type"
private val syncKeys = listOf(
showLoadingOverlayKey,
preferredAudioLanguageKey,
secondaryPreferredAudioLanguageKey,
preferredSubtitleLanguageKey,
secondaryPreferredSubtitleLanguageKey,
subtitleTextColorKey,
subtitleOutlineEnabledKey,
subtitleFontSizeSpKey,
subtitleBottomOffsetKey,
streamReuseLastLinkEnabledKey,
streamReuseLastLinkCacheHoursKey,
decoderPriorityKey,
mapDV7ToHevcKey,
tunnelingEnabledKey,
streamAutoPlayModeKey,
streamAutoPlaySourceKey,
streamAutoPlaySelectedAddonsKey,
streamAutoPlaySelectedPluginsKey,
streamAutoPlayRegexKey,
streamAutoPlayTimeoutSecondsKey,
skipIntroEnabledKey,
animeSkipEnabledKey,
animeSkipClientIdKey,
streamAutoPlayNextEpisodeEnabledKey,
streamAutoPlayPreferBingeGroupKey,
nextEpisodeThresholdModeKey,
nextEpisodeThresholdPercentKey,
nextEpisodeThresholdMinutesBeforeEndKey,
useLibassKey,
libassRenderTypeKey,
)
private var preferences: SharedPreferences? = null
@ -489,4 +534,74 @@ actual object PlayerSettingsStorage {
?.putString(ProfileScopedKey.of(libassRenderTypeKey), renderType)
?.apply()
}
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
loadShowLoadingOverlay()?.let { put(showLoadingOverlayKey, encodeSyncBoolean(it)) }
loadPreferredAudioLanguage()?.let { put(preferredAudioLanguageKey, encodeSyncString(it)) }
loadSecondaryPreferredAudioLanguage()?.let { put(secondaryPreferredAudioLanguageKey, encodeSyncString(it)) }
loadPreferredSubtitleLanguage()?.let { put(preferredSubtitleLanguageKey, encodeSyncString(it)) }
loadSecondaryPreferredSubtitleLanguage()?.let { put(secondaryPreferredSubtitleLanguageKey, encodeSyncString(it)) }
loadSubtitleTextColor()?.let { put(subtitleTextColorKey, encodeSyncString(it)) }
loadSubtitleOutlineEnabled()?.let { put(subtitleOutlineEnabledKey, encodeSyncBoolean(it)) }
loadSubtitleFontSizeSp()?.let { put(subtitleFontSizeSpKey, encodeSyncInt(it)) }
loadSubtitleBottomOffset()?.let { put(subtitleBottomOffsetKey, encodeSyncInt(it)) }
loadStreamReuseLastLinkEnabled()?.let { put(streamReuseLastLinkEnabledKey, encodeSyncBoolean(it)) }
loadStreamReuseLastLinkCacheHours()?.let { put(streamReuseLastLinkCacheHoursKey, encodeSyncInt(it)) }
loadDecoderPriority()?.let { put(decoderPriorityKey, encodeSyncInt(it)) }
loadMapDV7ToHevc()?.let { put(mapDV7ToHevcKey, encodeSyncBoolean(it)) }
loadTunnelingEnabled()?.let { put(tunnelingEnabledKey, encodeSyncBoolean(it)) }
loadStreamAutoPlayMode()?.let { put(streamAutoPlayModeKey, encodeSyncString(it)) }
loadStreamAutoPlaySource()?.let { put(streamAutoPlaySourceKey, encodeSyncString(it)) }
loadStreamAutoPlaySelectedAddons()?.let { put(streamAutoPlaySelectedAddonsKey, encodeSyncStringSet(it)) }
loadStreamAutoPlaySelectedPlugins()?.let { put(streamAutoPlaySelectedPluginsKey, encodeSyncStringSet(it)) }
loadStreamAutoPlayRegex()?.let { put(streamAutoPlayRegexKey, encodeSyncString(it)) }
loadStreamAutoPlayTimeoutSeconds()?.let { put(streamAutoPlayTimeoutSecondsKey, encodeSyncInt(it)) }
loadSkipIntroEnabled()?.let { put(skipIntroEnabledKey, encodeSyncBoolean(it)) }
loadAnimeSkipEnabled()?.let { put(animeSkipEnabledKey, encodeSyncBoolean(it)) }
loadAnimeSkipClientId()?.let { put(animeSkipClientIdKey, encodeSyncString(it)) }
loadStreamAutoPlayNextEpisodeEnabled()?.let { put(streamAutoPlayNextEpisodeEnabledKey, encodeSyncBoolean(it)) }
loadStreamAutoPlayPreferBingeGroup()?.let { put(streamAutoPlayPreferBingeGroupKey, encodeSyncBoolean(it)) }
loadNextEpisodeThresholdMode()?.let { put(nextEpisodeThresholdModeKey, encodeSyncString(it)) }
loadNextEpisodeThresholdPercent()?.let { put(nextEpisodeThresholdPercentKey, encodeSyncFloat(it)) }
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
loadUseLibass()?.let { put(useLibassKey, encodeSyncBoolean(it)) }
loadLibassRenderType()?.let { put(libassRenderTypeKey, encodeSyncString(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
preferences?.edit()?.apply {
syncKeys.forEach { remove(ProfileScopedKey.of(it)) }
}?.apply()
payload.decodeSyncBoolean(showLoadingOverlayKey)?.let(::saveShowLoadingOverlay)
payload.decodeSyncString(preferredAudioLanguageKey)?.let(::savePreferredAudioLanguage)
payload.decodeSyncString(secondaryPreferredAudioLanguageKey)?.let(::saveSecondaryPreferredAudioLanguage)
payload.decodeSyncString(preferredSubtitleLanguageKey)?.let(::savePreferredSubtitleLanguage)
payload.decodeSyncString(secondaryPreferredSubtitleLanguageKey)?.let(::saveSecondaryPreferredSubtitleLanguage)
payload.decodeSyncString(subtitleTextColorKey)?.let(::saveSubtitleTextColor)
payload.decodeSyncBoolean(subtitleOutlineEnabledKey)?.let(::saveSubtitleOutlineEnabled)
payload.decodeSyncInt(subtitleFontSizeSpKey)?.let(::saveSubtitleFontSizeSp)
payload.decodeSyncInt(subtitleBottomOffsetKey)?.let(::saveSubtitleBottomOffset)
payload.decodeSyncBoolean(streamReuseLastLinkEnabledKey)?.let(::saveStreamReuseLastLinkEnabled)
payload.decodeSyncInt(streamReuseLastLinkCacheHoursKey)?.let(::saveStreamReuseLastLinkCacheHours)
payload.decodeSyncInt(decoderPriorityKey)?.let(::saveDecoderPriority)
payload.decodeSyncBoolean(mapDV7ToHevcKey)?.let(::saveMapDV7ToHevc)
payload.decodeSyncBoolean(tunnelingEnabledKey)?.let(::saveTunnelingEnabled)
payload.decodeSyncString(streamAutoPlayModeKey)?.let(::saveStreamAutoPlayMode)
payload.decodeSyncString(streamAutoPlaySourceKey)?.let(::saveStreamAutoPlaySource)
payload.decodeSyncStringSet(streamAutoPlaySelectedAddonsKey)?.let(::saveStreamAutoPlaySelectedAddons)
payload.decodeSyncStringSet(streamAutoPlaySelectedPluginsKey)?.let(::saveStreamAutoPlaySelectedPlugins)
payload.decodeSyncString(streamAutoPlayRegexKey)?.let(::saveStreamAutoPlayRegex)
payload.decodeSyncInt(streamAutoPlayTimeoutSecondsKey)?.let(::saveStreamAutoPlayTimeoutSeconds)
payload.decodeSyncBoolean(skipIntroEnabledKey)?.let(::saveSkipIntroEnabled)
payload.decodeSyncBoolean(animeSkipEnabledKey)?.let(::saveAnimeSkipEnabled)
payload.decodeSyncString(animeSkipClientIdKey)?.let(::saveAnimeSkipClientId)
payload.decodeSyncBoolean(streamAutoPlayNextEpisodeEnabledKey)?.let(::saveStreamAutoPlayNextEpisodeEnabled)
payload.decodeSyncBoolean(streamAutoPlayPreferBingeGroupKey)?.let(::saveStreamAutoPlayPreferBingeGroup)
payload.decodeSyncString(nextEpisodeThresholdModeKey)?.let(::saveNextEpisodeThresholdMode)
payload.decodeSyncFloat(nextEpisodeThresholdPercentKey)?.let(::saveNextEpisodeThresholdPercent)
payload.decodeSyncFloat(nextEpisodeThresholdMinutesBeforeEndKey)?.let(::saveNextEpisodeThresholdMinutesBeforeEnd)
payload.decodeSyncBoolean(useLibassKey)?.let(::saveUseLibass)
payload.decodeSyncString(libassRenderTypeKey)?.let(::saveLibassRenderType)
}
}

View file

@ -2,12 +2,20 @@ package com.nuvio.app.features.settings
import android.content.Context
import android.content.SharedPreferences
import com.nuvio.app.core.sync.decodeSyncBoolean
import com.nuvio.app.core.sync.decodeSyncString
import com.nuvio.app.core.sync.encodeSyncBoolean
import com.nuvio.app.core.sync.encodeSyncString
import com.nuvio.app.core.storage.ProfileScopedKey
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
actual object ThemeSettingsStorage {
private const val preferencesName = "nuvio_theme_settings"
private const val selectedThemeKey = "selected_theme"
private const val amoledEnabledKey = "amoled_enabled"
private val syncKeys = listOf(selectedThemeKey, amoledEnabledKey)
private var preferences: SharedPreferences? = null
@ -37,4 +45,18 @@ actual object ThemeSettingsStorage {
?.putBoolean(ProfileScopedKey.of(amoledEnabledKey), enabled)
?.apply()
}
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) }
loadAmoledEnabled()?.let { put(amoledEnabledKey, encodeSyncBoolean(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
preferences?.edit()?.apply {
syncKeys.forEach { remove(ProfileScopedKey.of(it)) }
}?.apply()
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)
}
}

View file

@ -2,7 +2,14 @@ package com.nuvio.app.features.tmdb
import android.content.Context
import android.content.SharedPreferences
import com.nuvio.app.core.sync.decodeSyncBoolean
import com.nuvio.app.core.sync.decodeSyncString
import com.nuvio.app.core.sync.encodeSyncBoolean
import com.nuvio.app.core.sync.encodeSyncString
import com.nuvio.app.core.storage.ProfileScopedKey
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
actual object TmdbSettingsStorage {
private const val preferencesName = "nuvio_tmdb_settings"
@ -20,6 +27,22 @@ actual object TmdbSettingsStorage {
private const val useSeasonPostersKey = "tmdb_use_season_posters"
private const val useMoreLikeThisKey = "tmdb_use_more_like_this"
private const val useCollectionsKey = "tmdb_use_collections"
private val syncKeys = listOf(
enabledKey,
apiKeyKey,
languageKey,
useTrailersKey,
useArtworkKey,
useBasicInfoKey,
useDetailsKey,
useCreditsKey,
useProductionsKey,
useNetworksKey,
useEpisodesKey,
useSeasonPostersKey,
useMoreLikeThisKey,
useCollectionsKey,
)
private var preferences: SharedPreferences? = null
@ -135,4 +158,42 @@ actual object TmdbSettingsStorage {
?.putBoolean(ProfileScopedKey.of(key), enabled)
?.apply()
}
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
loadEnabled()?.let { put(enabledKey, encodeSyncBoolean(it)) }
loadApiKey()?.let { put(apiKeyKey, encodeSyncString(it)) }
loadLanguage()?.let { put(languageKey, encodeSyncString(it)) }
loadUseTrailers()?.let { put(useTrailersKey, encodeSyncBoolean(it)) }
loadUseArtwork()?.let { put(useArtworkKey, encodeSyncBoolean(it)) }
loadUseBasicInfo()?.let { put(useBasicInfoKey, encodeSyncBoolean(it)) }
loadUseDetails()?.let { put(useDetailsKey, encodeSyncBoolean(it)) }
loadUseCredits()?.let { put(useCreditsKey, encodeSyncBoolean(it)) }
loadUseProductions()?.let { put(useProductionsKey, encodeSyncBoolean(it)) }
loadUseNetworks()?.let { put(useNetworksKey, encodeSyncBoolean(it)) }
loadUseEpisodes()?.let { put(useEpisodesKey, encodeSyncBoolean(it)) }
loadUseSeasonPosters()?.let { put(useSeasonPostersKey, encodeSyncBoolean(it)) }
loadUseMoreLikeThis()?.let { put(useMoreLikeThisKey, encodeSyncBoolean(it)) }
loadUseCollections()?.let { put(useCollectionsKey, encodeSyncBoolean(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
preferences?.edit()?.apply {
syncKeys.forEach { remove(ProfileScopedKey.of(it)) }
}?.apply()
payload.decodeSyncBoolean(enabledKey)?.let(::saveEnabled)
payload.decodeSyncString(apiKeyKey)?.let(::saveApiKey)
payload.decodeSyncString(languageKey)?.let(::saveLanguage)
payload.decodeSyncBoolean(useTrailersKey)?.let(::saveUseTrailers)
payload.decodeSyncBoolean(useArtworkKey)?.let(::saveUseArtwork)
payload.decodeSyncBoolean(useBasicInfoKey)?.let(::saveUseBasicInfo)
payload.decodeSyncBoolean(useDetailsKey)?.let(::saveUseDetails)
payload.decodeSyncBoolean(useCreditsKey)?.let(::saveUseCredits)
payload.decodeSyncBoolean(useProductionsKey)?.let(::saveUseProductions)
payload.decodeSyncBoolean(useNetworksKey)?.let(::saveUseNetworks)
payload.decodeSyncBoolean(useEpisodesKey)?.let(::saveUseEpisodes)
payload.decodeSyncBoolean(useSeasonPostersKey)?.let(::saveUseSeasonPosters)
payload.decodeSyncBoolean(useMoreLikeThisKey)?.let(::saveUseMoreLikeThis)
payload.decodeSyncBoolean(useCollectionsKey)?.let(::saveUseCollections)
}
}

View file

@ -2,11 +2,17 @@ package com.nuvio.app.features.trakt
import android.content.Context
import android.content.SharedPreferences
import com.nuvio.app.core.sync.decodeSyncBoolean
import com.nuvio.app.core.sync.encodeSyncBoolean
import com.nuvio.app.core.storage.ProfileScopedKey
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
internal actual object TraktCommentsStorage {
private const val preferencesName = "nuvio_trakt_comments"
private const val enabledKey = "comments_enabled"
private val syncKeys = listOf(enabledKey)
private var preferences: SharedPreferences? = null
@ -26,4 +32,16 @@ internal actual object TraktCommentsStorage {
?.putBoolean(ProfileScopedKey.of(enabledKey), enabled)
?.apply()
}
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
loadEnabled()?.let { put(enabledKey, encodeSyncBoolean(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
preferences?.edit()?.apply {
syncKeys.forEach { remove(ProfileScopedKey.of(it)) }
}?.apply()
payload.decodeSyncBoolean(enabledKey)?.let(::saveEnabled)
}
}

View file

@ -1,29 +1,26 @@
package com.nuvio.app.core.sync
import androidx.compose.ui.graphics.Color
import co.touchlab.kermit.Logger
import com.nuvio.app.core.auth.AuthRepository
import com.nuvio.app.core.auth.AuthState
import com.nuvio.app.core.network.SupabaseProvider
import com.nuvio.app.core.ui.AppTheme
import com.nuvio.app.features.details.MetaScreenSectionItem
import com.nuvio.app.features.details.MetaScreenSectionKey
import com.nuvio.app.features.details.MetaScreenSettingsStorage
import com.nuvio.app.features.details.MetaScreenSettingsRepository
import com.nuvio.app.features.mdblist.MdbListMetadataService
import com.nuvio.app.features.mdblist.MdbListSettingsStorage
import com.nuvio.app.features.mdblist.MdbListSettingsRepository
import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsRepository
import com.nuvio.app.features.player.PlayerSettingsStorage
import com.nuvio.app.features.player.PlayerSettingsRepository
import com.nuvio.app.features.player.SubtitleStyleState
import com.nuvio.app.features.player.subtitleColorFromStorage
import com.nuvio.app.features.player.toStorageHexString
import com.nuvio.app.features.player.skip.NextEpisodeThresholdMode
import com.nuvio.app.features.profiles.ProfileRepository
import com.nuvio.app.features.settings.ThemeSettingsStorage
import com.nuvio.app.features.settings.ThemeSettingsRepository
import com.nuvio.app.features.streams.StreamAutoPlayMode
import com.nuvio.app.features.streams.StreamAutoPlaySource
import com.nuvio.app.features.tmdb.TmdbSettingsStorage
import com.nuvio.app.features.tmdb.TmdbSettingsRepository
import com.nuvio.app.features.trakt.TraktCommentsStorage
import com.nuvio.app.features.trakt.TraktCommentsSettings
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesStorage
import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesRepository
import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle
import io.github.jan.supabase.postgrest.postgrest
import io.github.jan.supabase.postgrest.rpc
import kotlin.concurrent.Volatile
@ -103,23 +100,23 @@ object ProfileSettingsSync {
return@withLock false
}
val remoteBlob = runCatching {
json.decodeFromJsonElement(ProfileSettingsBlob.serializer(), remoteJson)
}.getOrElse { error ->
log.e(error) { "pull(profileId=$profileId) — failed to decode remote settings blob" }
return@withLock false
}
val remoteSignature = buildSignature(remoteBlob)
if (remoteSignature == localSignature) {
log.d { "pull(profileId=$profileId) — remote matches local" }
return@withLock false
}
isApplyingRemoteBlob = true
try {
val remoteBlob = runCatching {
json.decodeFromJsonElement(MobileProfileSettingsBlob.serializer(), remoteJson)
}.getOrElse { error ->
log.e(error) { "pull(profileId=$profileId) — failed to decode remote settings blob" }
return@withLock false
}
val remoteSignature = buildSignature(remoteBlob)
if (remoteSignature == localSignature) {
log.d { "pull(profileId=$profileId) — remote matches local" }
return@withLock false
}
applyRemoteBlob(remoteBlob)
skipNextPushSignature = remoteSignature
skipNextPushSignature = currentObservedStateSignature()
} finally {
isApplyingRemoteBlob = false
}
@ -149,21 +146,19 @@ object ProfileSettingsSync {
@OptIn(FlowPreview::class)
private fun observeLocalChangesAndPush() {
val signatureFlows = listOf(
ThemeSettingsRepository.selectedTheme.map { "theme=${it.name}" },
ThemeSettingsRepository.amoledEnabled.map { "amoled=$it" },
PlayerSettingsRepository.uiState.map { buildPlayerSignature(it) },
TmdbSettingsRepository.uiState.map { buildTmdbSignature(it) },
MdbListSettingsRepository.uiState.map { buildMdbListSignature(it) },
MetaScreenSettingsRepository.uiState.map { buildMetaScreenSignature(it) },
ContinueWatchingPreferencesRepository.uiState.map { buildContinueWatchingSignature(it) },
TraktCommentsSettings.enabled.map { "trakt_comments=$it" },
EpisodeReleaseNotificationsRepository.uiState.map { "episode_release_alerts=${it.isEnabled}" },
ThemeSettingsRepository.selectedTheme.map { "theme" },
ThemeSettingsRepository.amoledEnabled.map { "amoled" },
PlayerSettingsRepository.uiState.map { "player" },
TmdbSettingsRepository.uiState.map { "tmdb" },
MdbListSettingsRepository.uiState.map { "mdblist" },
MetaScreenSettingsRepository.uiState.map { "meta" },
ContinueWatchingPreferencesRepository.uiState.map { "continue_watching" },
TraktCommentsSettings.enabled.map { "trakt_comments" },
EpisodeReleaseNotificationsRepository.uiState.map { "episode_release_alerts" },
)
observeJob = scope.launch {
combine(signatureFlows) { parts ->
parts.joinToString(separator = "||")
}
combine(signatureFlows) { currentObservedStateSignature() }
.drop(1)
.distinctUntilChanged()
.debounce(PUSH_DEBOUNCE_MS)
@ -180,243 +175,58 @@ object ProfileSettingsSync {
}
}
private suspend fun pushToRemoteLocked(profileId: Int, blob: ProfileSettingsBlob) {
private suspend fun pushToRemoteLocked(profileId: Int, blob: MobileProfileSettingsBlob) {
val params = buildJsonObject {
put("p_profile_id", profileId)
put("p_platform", MOBILE_SYNC_PLATFORM)
put("p_settings_json", json.encodeToJsonElement(ProfileSettingsBlob.serializer(), blob))
put("p_settings_json", json.encodeToJsonElement(MobileProfileSettingsBlob.serializer(), blob))
}
SupabaseProvider.client.postgrest.rpc("sync_push_profile_settings_blob", params)
log.d { "pushToRemoteLocked(profileId=$profileId) — success" }
}
private fun exportSettingsBlob(): ProfileSettingsBlob {
private fun exportSettingsBlob(): MobileProfileSettingsBlob {
ensureRepositoriesLoaded()
val themePayload = ThemeSettingsPayload(
selectedTheme = ThemeSettingsRepository.selectedTheme.value.name,
amoledEnabled = ThemeSettingsRepository.amoledEnabled.value,
)
val playerState = PlayerSettingsRepository.uiState.value
val playerPayload = PlayerSettingsPayload(
showLoadingOverlay = playerState.showLoadingOverlay,
preferredAudioLanguage = playerState.preferredAudioLanguage,
secondaryPreferredAudioLanguage = playerState.secondaryPreferredAudioLanguage,
preferredSubtitleLanguage = playerState.preferredSubtitleLanguage,
secondaryPreferredSubtitleLanguage = playerState.secondaryPreferredSubtitleLanguage,
subtitleStyle = SubtitleStylePayload(
textColorHex = playerState.subtitleStyle.textColor.toStorageHexString(),
outlineEnabled = playerState.subtitleStyle.outlineEnabled,
fontSizeSp = playerState.subtitleStyle.fontSizeSp,
bottomOffset = playerState.subtitleStyle.bottomOffset,
),
streamReuseLastLinkEnabled = playerState.streamReuseLastLinkEnabled,
streamReuseLastLinkCacheHours = playerState.streamReuseLastLinkCacheHours,
decoderPriority = playerState.decoderPriority,
mapDV7ToHevc = playerState.mapDV7ToHevc,
tunnelingEnabled = playerState.tunnelingEnabled,
streamAutoPlayMode = playerState.streamAutoPlayMode.name,
streamAutoPlaySource = playerState.streamAutoPlaySource.name,
streamAutoPlaySelectedAddons = playerState.streamAutoPlaySelectedAddons.sorted(),
streamAutoPlaySelectedPlugins = playerState.streamAutoPlaySelectedPlugins.sorted(),
streamAutoPlayRegex = playerState.streamAutoPlayRegex,
streamAutoPlayTimeoutSeconds = playerState.streamAutoPlayTimeoutSeconds,
skipIntroEnabled = playerState.skipIntroEnabled,
animeSkipEnabled = playerState.animeSkipEnabled,
animeSkipClientId = playerState.animeSkipClientId,
streamAutoPlayNextEpisodeEnabled = playerState.streamAutoPlayNextEpisodeEnabled,
streamAutoPlayPreferBingeGroup = playerState.streamAutoPlayPreferBingeGroup,
nextEpisodeThresholdMode = playerState.nextEpisodeThresholdMode.name,
nextEpisodeThresholdPercent = playerState.nextEpisodeThresholdPercent,
nextEpisodeThresholdMinutesBeforeEnd = playerState.nextEpisodeThresholdMinutesBeforeEnd,
useLibass = playerState.useLibass,
libassRenderType = playerState.libassRenderType,
)
val tmdbState = TmdbSettingsRepository.snapshot()
val tmdbPayload = TmdbSettingsPayload(
enabled = tmdbState.enabled,
apiKey = tmdbState.apiKey,
language = tmdbState.language,
useTrailers = tmdbState.useTrailers,
useArtwork = tmdbState.useArtwork,
useBasicInfo = tmdbState.useBasicInfo,
useDetails = tmdbState.useDetails,
useCredits = tmdbState.useCredits,
useProductions = tmdbState.useProductions,
useNetworks = tmdbState.useNetworks,
useEpisodes = tmdbState.useEpisodes,
useSeasonPosters = tmdbState.useSeasonPosters,
useMoreLikeThis = tmdbState.useMoreLikeThis,
useCollections = tmdbState.useCollections,
)
val mdbListState = MdbListSettingsRepository.snapshot()
val mdbListPayload = MdbListSettingsPayload(
enabled = mdbListState.enabled,
apiKey = mdbListState.apiKey,
useImdb = mdbListState.useImdb,
useTmdb = mdbListState.useTmdb,
useTomatoes = mdbListState.useTomatoes,
useMetacritic = mdbListState.useMetacritic,
useTrakt = mdbListState.useTrakt,
useLetterboxd = mdbListState.useLetterboxd,
useAudience = mdbListState.useAudience,
)
val metaState = MetaScreenSettingsRepository.uiState.value
val metaPayload = MetaScreenSettingsPayload(
items = metaState.items
.sortedBy { it.order }
.map { item ->
MetaScreenItemPayload(
key = item.key.name,
enabled = item.enabled,
order = item.order,
tabGroup = item.tabGroup,
)
},
cinematicBackground = metaState.cinematicBackground,
tabLayout = metaState.tabLayout,
)
val continueWatchingState = ContinueWatchingPreferencesRepository.uiState.value
val continueWatchingPayload = ContinueWatchingSettingsPayload(
isVisible = continueWatchingState.isVisible,
style = continueWatchingState.style.name,
upNextFromFurthestEpisode = continueWatchingState.upNextFromFurthestEpisode,
dismissedNextUpKeys = continueWatchingState.dismissedNextUpKeys.sorted(),
)
val traktCommentsPayload = TraktCommentsPayload(
enabled = TraktCommentsSettings.enabled.value,
)
val notificationsPayload = EpisodeReleaseNotificationsPayload(
alertsEnabled = EpisodeReleaseNotificationsRepository.uiState.value.isEnabled,
)
return ProfileSettingsBlob(
features = ProfileSettingsFeatures(
theme = themePayload,
player = playerPayload,
tmdb = tmdbPayload,
mdbList = mdbListPayload,
metaScreen = metaPayload,
continueWatching = continueWatchingPayload,
traktComments = traktCommentsPayload,
episodeReleaseNotifications = notificationsPayload,
return MobileProfileSettingsBlob(
features = MobileProfileSettingsFeatures(
themeSettings = ThemeSettingsStorage.exportToSyncPayload(),
playerSettings = PlayerSettingsStorage.exportToSyncPayload(),
tmdbSettings = TmdbSettingsStorage.exportToSyncPayload(),
mdbListSettings = MdbListSettingsStorage.exportToSyncPayload(),
metaScreenSettingsPayload = MetaScreenSettingsStorage.loadPayload().orEmpty().trim(),
continueWatchingSettingsPayload = ContinueWatchingPreferencesStorage.loadPayload().orEmpty().trim(),
traktCommentsSettings = TraktCommentsStorage.exportToSyncPayload(),
notificationsSettings = NotificationsSettingsPayload(
episodeReleaseAlertsEnabled = EpisodeReleaseNotificationsRepository.uiState.value.isEnabled,
),
),
)
}
private fun applyRemoteBlob(blob: ProfileSettingsBlob) {
val theme = blob.features.theme
ThemeSettingsRepository.setTheme(
runCatching { AppTheme.valueOf(theme.selectedTheme) }.getOrDefault(AppTheme.WHITE),
)
ThemeSettingsRepository.setAmoled(theme.amoledEnabled)
private fun applyRemoteBlob(blob: MobileProfileSettingsBlob) {
ThemeSettingsStorage.replaceFromSyncPayload(blob.features.themeSettings)
ThemeSettingsRepository.onProfileChanged()
val player = blob.features.player
PlayerSettingsRepository.setShowLoadingOverlay(player.showLoadingOverlay)
PlayerSettingsRepository.setPreferredAudioLanguage(player.preferredAudioLanguage)
PlayerSettingsRepository.setSecondaryPreferredAudioLanguage(player.secondaryPreferredAudioLanguage)
PlayerSettingsRepository.setPreferredSubtitleLanguage(player.preferredSubtitleLanguage)
PlayerSettingsRepository.setSecondaryPreferredSubtitleLanguage(player.secondaryPreferredSubtitleLanguage)
PlayerSettingsRepository.setSubtitleStyle(
SubtitleStyleState(
textColor = subtitleColorFromStorage(player.subtitleStyle.textColorHex) ?: Color.White,
outlineEnabled = player.subtitleStyle.outlineEnabled,
fontSizeSp = player.subtitleStyle.fontSizeSp,
bottomOffset = player.subtitleStyle.bottomOffset,
),
)
PlayerSettingsRepository.setStreamReuseLastLinkEnabled(player.streamReuseLastLinkEnabled)
PlayerSettingsRepository.setStreamReuseLastLinkCacheHours(player.streamReuseLastLinkCacheHours)
PlayerSettingsRepository.setDecoderPriority(player.decoderPriority)
PlayerSettingsRepository.setMapDV7ToHevc(player.mapDV7ToHevc)
PlayerSettingsRepository.setTunnelingEnabled(player.tunnelingEnabled)
PlayerSettingsRepository.setStreamAutoPlayMode(
runCatching { StreamAutoPlayMode.valueOf(player.streamAutoPlayMode) }.getOrDefault(StreamAutoPlayMode.MANUAL),
)
PlayerSettingsRepository.setStreamAutoPlaySource(
runCatching { StreamAutoPlaySource.valueOf(player.streamAutoPlaySource) }
.getOrDefault(StreamAutoPlaySource.ALL_SOURCES),
)
PlayerSettingsRepository.setStreamAutoPlaySelectedAddons(player.streamAutoPlaySelectedAddons.toSet())
PlayerSettingsRepository.setStreamAutoPlaySelectedPlugins(player.streamAutoPlaySelectedPlugins.toSet())
PlayerSettingsRepository.setStreamAutoPlayRegex(player.streamAutoPlayRegex)
PlayerSettingsRepository.setStreamAutoPlayTimeoutSeconds(player.streamAutoPlayTimeoutSeconds)
PlayerSettingsRepository.setSkipIntroEnabled(player.skipIntroEnabled)
PlayerSettingsRepository.setAnimeSkipEnabled(player.animeSkipEnabled)
PlayerSettingsRepository.setAnimeSkipClientId(player.animeSkipClientId)
PlayerSettingsRepository.setStreamAutoPlayNextEpisodeEnabled(player.streamAutoPlayNextEpisodeEnabled)
PlayerSettingsRepository.setStreamAutoPlayPreferBingeGroup(player.streamAutoPlayPreferBingeGroup)
PlayerSettingsRepository.setNextEpisodeThresholdMode(
runCatching { NextEpisodeThresholdMode.valueOf(player.nextEpisodeThresholdMode) }
.getOrDefault(NextEpisodeThresholdMode.PERCENTAGE),
)
PlayerSettingsRepository.setNextEpisodeThresholdPercent(player.nextEpisodeThresholdPercent)
PlayerSettingsRepository.setNextEpisodeThresholdMinutesBeforeEnd(player.nextEpisodeThresholdMinutesBeforeEnd)
PlayerSettingsRepository.setUseLibass(player.useLibass)
PlayerSettingsRepository.setLibassRenderType(player.libassRenderType)
PlayerSettingsStorage.replaceFromSyncPayload(blob.features.playerSettings)
PlayerSettingsRepository.onProfileChanged()
val tmdb = blob.features.tmdb
TmdbSettingsRepository.setApiKey(tmdb.apiKey)
TmdbSettingsRepository.setLanguage(tmdb.language)
TmdbSettingsRepository.setUseTrailers(tmdb.useTrailers)
TmdbSettingsRepository.setUseArtwork(tmdb.useArtwork)
TmdbSettingsRepository.setUseBasicInfo(tmdb.useBasicInfo)
TmdbSettingsRepository.setUseDetails(tmdb.useDetails)
TmdbSettingsRepository.setUseCredits(tmdb.useCredits)
TmdbSettingsRepository.setUseProductions(tmdb.useProductions)
TmdbSettingsRepository.setUseNetworks(tmdb.useNetworks)
TmdbSettingsRepository.setUseEpisodes(tmdb.useEpisodes)
TmdbSettingsRepository.setUseSeasonPosters(tmdb.useSeasonPosters)
TmdbSettingsRepository.setUseMoreLikeThis(tmdb.useMoreLikeThis)
TmdbSettingsRepository.setUseCollections(tmdb.useCollections)
TmdbSettingsRepository.setEnabled(tmdb.enabled)
TmdbSettingsStorage.replaceFromSyncPayload(blob.features.tmdbSettings)
TmdbSettingsRepository.onProfileChanged()
val mdbList = blob.features.mdbList
MdbListSettingsRepository.setApiKey(mdbList.apiKey)
MdbListSettingsRepository.setProviderEnabled("imdb", mdbList.useImdb)
MdbListSettingsRepository.setProviderEnabled("tmdb", mdbList.useTmdb)
MdbListSettingsRepository.setProviderEnabled("tomatoes", mdbList.useTomatoes)
MdbListSettingsRepository.setProviderEnabled("metacritic", mdbList.useMetacritic)
MdbListSettingsRepository.setProviderEnabled("trakt", mdbList.useTrakt)
MdbListSettingsRepository.setProviderEnabled("letterboxd", mdbList.useLetterboxd)
MdbListSettingsRepository.setProviderEnabled("audience", mdbList.useAudience)
MdbListSettingsRepository.setEnabled(mdbList.enabled)
MdbListSettingsStorage.replaceFromSyncPayload(blob.features.mdbListSettings)
MdbListMetadataService.clearCache()
MdbListSettingsRepository.onProfileChanged()
val metaScreen = blob.features.metaScreen
MetaScreenSettingsRepository.applyFromSync(
items = metaScreen.items.map { item ->
MetaScreenSectionItem(
key = runCatching { MetaScreenSectionKey.valueOf(item.key) }
.getOrDefault(MetaScreenSectionKey.ACTIONS),
title = "",
description = "",
enabled = item.enabled,
order = item.order,
tabGroup = item.tabGroup,
)
},
cinematicBackground = metaScreen.cinematicBackground,
tabLayout = metaScreen.tabLayout,
)
MetaScreenSettingsStorage.savePayload(blob.features.metaScreenSettingsPayload)
MetaScreenSettingsRepository.onProfileChanged()
val continueWatching = blob.features.continueWatching
ContinueWatchingPreferencesRepository.applyFromSync(
isVisible = continueWatching.isVisible,
style = runCatching { ContinueWatchingSectionStyle.valueOf(continueWatching.style) }
.getOrDefault(ContinueWatchingSectionStyle.Wide),
upNextFromFurthestEpisode = continueWatching.upNextFromFurthestEpisode,
dismissedNextUpKeys = continueWatching.dismissedNextUpKeys.toSet(),
)
ContinueWatchingPreferencesStorage.savePayload(blob.features.continueWatchingSettingsPayload)
ContinueWatchingPreferencesRepository.onProfileChanged()
TraktCommentsSettings.setEnabled(blob.features.traktComments.enabled)
EpisodeReleaseNotificationsRepository.applyFromSyncEnabled(blob.features.episodeReleaseNotifications.alertsEnabled)
TraktCommentsStorage.replaceFromSyncPayload(blob.features.traktCommentsSettings)
TraktCommentsSettings.onProfileChanged()
EpisodeReleaseNotificationsRepository.applyFromSyncEnabled(blob.features.notificationsSettings.episodeReleaseAlertsEnabled)
}
private fun ensureRepositoriesLoaded() {
@ -430,232 +240,51 @@ object ProfileSettingsSync {
EpisodeReleaseNotificationsRepository.ensureLoaded()
}
private fun buildSignature(blob: ProfileSettingsBlob): String =
json.encodeToString(ProfileSettingsBlob.serializer(), blob)
private fun buildSignature(blob: MobileProfileSettingsBlob): String =
json.encodeToString(MobileProfileSettingsBlob.serializer(), blob)
private fun defaultSignature(): String =
buildSignature(ProfileSettingsBlob())
buildSignature(MobileProfileSettingsBlob())
private fun buildPlayerSignature(state: com.nuvio.app.features.player.PlayerSettingsUiState): String =
json.encodeToString(PlayerSettingsPayload.serializer(), PlayerSettingsPayload(
showLoadingOverlay = state.showLoadingOverlay,
preferredAudioLanguage = state.preferredAudioLanguage,
secondaryPreferredAudioLanguage = state.secondaryPreferredAudioLanguage,
preferredSubtitleLanguage = state.preferredSubtitleLanguage,
secondaryPreferredSubtitleLanguage = state.secondaryPreferredSubtitleLanguage,
subtitleStyle = SubtitleStylePayload(
textColorHex = state.subtitleStyle.textColor.toStorageHexString(),
outlineEnabled = state.subtitleStyle.outlineEnabled,
fontSizeSp = state.subtitleStyle.fontSizeSp,
bottomOffset = state.subtitleStyle.bottomOffset,
),
streamReuseLastLinkEnabled = state.streamReuseLastLinkEnabled,
streamReuseLastLinkCacheHours = state.streamReuseLastLinkCacheHours,
decoderPriority = state.decoderPriority,
mapDV7ToHevc = state.mapDV7ToHevc,
tunnelingEnabled = state.tunnelingEnabled,
streamAutoPlayMode = state.streamAutoPlayMode.name,
streamAutoPlaySource = state.streamAutoPlaySource.name,
streamAutoPlaySelectedAddons = state.streamAutoPlaySelectedAddons.sorted(),
streamAutoPlaySelectedPlugins = state.streamAutoPlaySelectedPlugins.sorted(),
streamAutoPlayRegex = state.streamAutoPlayRegex,
streamAutoPlayTimeoutSeconds = state.streamAutoPlayTimeoutSeconds,
skipIntroEnabled = state.skipIntroEnabled,
animeSkipEnabled = state.animeSkipEnabled,
animeSkipClientId = state.animeSkipClientId,
streamAutoPlayNextEpisodeEnabled = state.streamAutoPlayNextEpisodeEnabled,
streamAutoPlayPreferBingeGroup = state.streamAutoPlayPreferBingeGroup,
nextEpisodeThresholdMode = state.nextEpisodeThresholdMode.name,
nextEpisodeThresholdPercent = state.nextEpisodeThresholdPercent,
nextEpisodeThresholdMinutesBeforeEnd = state.nextEpisodeThresholdMinutesBeforeEnd,
useLibass = state.useLibass,
libassRenderType = state.libassRenderType,
))
private fun buildTmdbSignature(state: com.nuvio.app.features.tmdb.TmdbSettings): String =
json.encodeToString(TmdbSettingsPayload.serializer(), TmdbSettingsPayload(
enabled = state.enabled,
apiKey = state.apiKey,
language = state.language,
useTrailers = state.useTrailers,
useArtwork = state.useArtwork,
useBasicInfo = state.useBasicInfo,
useDetails = state.useDetails,
useCredits = state.useCredits,
useProductions = state.useProductions,
useNetworks = state.useNetworks,
useEpisodes = state.useEpisodes,
useSeasonPosters = state.useSeasonPosters,
useMoreLikeThis = state.useMoreLikeThis,
useCollections = state.useCollections,
))
private fun buildMdbListSignature(state: com.nuvio.app.features.mdblist.MdbListSettings): String =
json.encodeToString(MdbListSettingsPayload.serializer(), MdbListSettingsPayload(
enabled = state.enabled,
apiKey = state.apiKey,
useImdb = state.useImdb,
useTmdb = state.useTmdb,
useTomatoes = state.useTomatoes,
useMetacritic = state.useMetacritic,
useTrakt = state.useTrakt,
useLetterboxd = state.useLetterboxd,
useAudience = state.useAudience,
))
private fun buildMetaScreenSignature(state: com.nuvio.app.features.details.MetaScreenSettingsUiState): String =
json.encodeToString(MetaScreenSettingsPayload.serializer(), MetaScreenSettingsPayload(
items = state.items.sortedBy { it.order }.map { item ->
MetaScreenItemPayload(
key = item.key.name,
enabled = item.enabled,
order = item.order,
tabGroup = item.tabGroup,
)
},
cinematicBackground = state.cinematicBackground,
tabLayout = state.tabLayout,
))
private fun buildContinueWatchingSignature(state: com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesUiState): String =
json.encodeToString(ContinueWatchingSettingsPayload.serializer(), ContinueWatchingSettingsPayload(
isVisible = state.isVisible,
style = state.style.name,
upNextFromFurthestEpisode = state.upNextFromFurthestEpisode,
dismissedNextUpKeys = state.dismissedNextUpKeys.sorted(),
))
private fun currentObservedStateSignature(): String = listOf(
"theme=${ThemeSettingsRepository.selectedTheme.value.name}",
"amoled=${ThemeSettingsRepository.amoledEnabled.value}",
"player=${PlayerSettingsRepository.uiState.value}",
"tmdb=${TmdbSettingsRepository.uiState.value}",
"mdblist=${MdbListSettingsRepository.uiState.value}",
"meta=${MetaScreenSettingsRepository.uiState.value}",
"continue=${ContinueWatchingPreferencesRepository.uiState.value}",
"trakt_comments=${TraktCommentsSettings.enabled.value}",
"episode_release_alerts=${EpisodeReleaseNotificationsRepository.uiState.value.isEnabled}",
).joinToString(separator = "||")
}
@Serializable
private data class MobileProfileSettingsBlob(
val version: Int = 2,
val features: MobileProfileSettingsFeatures = MobileProfileSettingsFeatures(),
)
@Serializable
private data class MobileProfileSettingsFeatures(
@SerialName("theme_settings") val themeSettings: JsonObject = JsonObject(emptyMap()),
@SerialName("player_settings") val playerSettings: JsonObject = JsonObject(emptyMap()),
@SerialName("tmdb_settings") val tmdbSettings: JsonObject = JsonObject(emptyMap()),
@SerialName("mdblist_settings") val mdbListSettings: JsonObject = JsonObject(emptyMap()),
@SerialName("meta_screen_settings_payload") val metaScreenSettingsPayload: String = "",
@SerialName("continue_watching_settings_payload") val continueWatchingSettingsPayload: String = "",
@SerialName("trakt_comments_settings") val traktCommentsSettings: JsonObject = JsonObject(emptyMap()),
@SerialName("notifications_settings") val notificationsSettings: NotificationsSettingsPayload = NotificationsSettingsPayload(),
)
@Serializable
private data class NotificationsSettingsPayload(
@SerialName("episode_release_alerts_enabled") val episodeReleaseAlertsEnabled: Boolean = false,
)
@Serializable
private data class SettingsBlobResponse(
@SerialName("profile_id") val profileId: Int = 0,
@SerialName("settings_json") val settingsJson: JsonObject? = null,
@SerialName("updated_at") val updatedAt: String? = null,
)
@Serializable
private data class ProfileSettingsBlob(
val version: Int = 1,
val features: ProfileSettingsFeatures = ProfileSettingsFeatures(),
)
@Serializable
private data class ProfileSettingsFeatures(
val theme: ThemeSettingsPayload = ThemeSettingsPayload(),
val player: PlayerSettingsPayload = PlayerSettingsPayload(),
val tmdb: TmdbSettingsPayload = TmdbSettingsPayload(),
@SerialName("mdb_list") val mdbList: MdbListSettingsPayload = MdbListSettingsPayload(),
@SerialName("meta_screen") val metaScreen: MetaScreenSettingsPayload = MetaScreenSettingsPayload(),
@SerialName("continue_watching") val continueWatching: ContinueWatchingSettingsPayload = ContinueWatchingSettingsPayload(),
@SerialName("trakt_comments") val traktComments: TraktCommentsPayload = TraktCommentsPayload(),
@SerialName("episode_release_notifications") val episodeReleaseNotifications: EpisodeReleaseNotificationsPayload = EpisodeReleaseNotificationsPayload(),
)
@Serializable
private data class ThemeSettingsPayload(
val selectedTheme: String = AppTheme.WHITE.name,
val amoledEnabled: Boolean = false,
)
@Serializable
private data class SubtitleStylePayload(
val textColorHex: String = Color.White.toStorageHexString(),
val outlineEnabled: Boolean = false,
val fontSizeSp: Int = 18,
val bottomOffset: Int = 20,
)
@Serializable
private data class PlayerSettingsPayload(
val showLoadingOverlay: Boolean = true,
val preferredAudioLanguage: String = "device",
val secondaryPreferredAudioLanguage: String? = null,
val preferredSubtitleLanguage: String = "none",
val secondaryPreferredSubtitleLanguage: String? = null,
val subtitleStyle: SubtitleStylePayload = SubtitleStylePayload(),
val streamReuseLastLinkEnabled: Boolean = false,
val streamReuseLastLinkCacheHours: Int = 24,
val decoderPriority: Int = 1,
val mapDV7ToHevc: Boolean = false,
val tunnelingEnabled: Boolean = false,
val streamAutoPlayMode: String = StreamAutoPlayMode.MANUAL.name,
val streamAutoPlaySource: String = StreamAutoPlaySource.ALL_SOURCES.name,
val streamAutoPlaySelectedAddons: List<String> = emptyList(),
val streamAutoPlaySelectedPlugins: List<String> = emptyList(),
val streamAutoPlayRegex: String = "",
val streamAutoPlayTimeoutSeconds: Int = 3,
val skipIntroEnabled: Boolean = true,
val animeSkipEnabled: Boolean = false,
val animeSkipClientId: String = "",
val streamAutoPlayNextEpisodeEnabled: Boolean = false,
val streamAutoPlayPreferBingeGroup: Boolean = true,
val nextEpisodeThresholdMode: String = NextEpisodeThresholdMode.PERCENTAGE.name,
val nextEpisodeThresholdPercent: Float = 99f,
val nextEpisodeThresholdMinutesBeforeEnd: Float = 2f,
val useLibass: Boolean = false,
val libassRenderType: String = "CUES",
)
@Serializable
private data class TmdbSettingsPayload(
val enabled: Boolean = false,
val apiKey: String = "",
val language: String = "en",
val useTrailers: Boolean = true,
val useArtwork: Boolean = true,
val useBasicInfo: Boolean = true,
val useDetails: Boolean = true,
val useCredits: Boolean = true,
val useProductions: Boolean = true,
val useNetworks: Boolean = true,
val useEpisodes: Boolean = true,
val useSeasonPosters: Boolean = true,
val useMoreLikeThis: Boolean = true,
val useCollections: Boolean = true,
)
@Serializable
private data class MdbListSettingsPayload(
val enabled: Boolean = false,
val apiKey: String = "",
val useImdb: Boolean = true,
val useTmdb: Boolean = true,
val useTomatoes: Boolean = true,
val useMetacritic: Boolean = true,
val useTrakt: Boolean = true,
val useLetterboxd: Boolean = true,
val useAudience: Boolean = true,
)
@Serializable
private data class MetaScreenItemPayload(
val key: String,
val enabled: Boolean = true,
val order: Int = 0,
val tabGroup: Int? = null,
)
@Serializable
private data class MetaScreenSettingsPayload(
val items: List<MetaScreenItemPayload> = emptyList(),
val cinematicBackground: Boolean = false,
val tabLayout: Boolean = false,
)
@Serializable
private data class ContinueWatchingSettingsPayload(
val isVisible: Boolean = true,
val style: String = ContinueWatchingSectionStyle.Wide.name,
val upNextFromFurthestEpisode: Boolean = true,
val dismissedNextUpKeys: List<String> = emptyList(),
)
@Serializable
private data class TraktCommentsPayload(
val enabled: Boolean = true,
)
@Serializable
private data class EpisodeReleaseNotificationsPayload(
val alertsEnabled: Boolean = false,
)

View file

@ -0,0 +1,89 @@
package com.nuvio.app.core.sync
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.floatOrNull
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
private const val TYPE_KEY = "type"
private const val VALUE_KEY = "value"
private const val TYPE_STRING = "string"
private const val TYPE_BOOLEAN = "boolean"
private const val TYPE_INT = "int"
private const val TYPE_FLOAT = "float"
private const val TYPE_STRING_SET = "string_set"
internal fun encodeSyncString(value: String): JsonObject = buildJsonObject {
put(TYPE_KEY, TYPE_STRING)
put(VALUE_KEY, value)
}
internal fun encodeSyncBoolean(value: Boolean): JsonObject = buildJsonObject {
put(TYPE_KEY, TYPE_BOOLEAN)
put(VALUE_KEY, value)
}
internal fun encodeSyncInt(value: Int): JsonObject = buildJsonObject {
put(TYPE_KEY, TYPE_INT)
put(VALUE_KEY, value)
}
internal fun encodeSyncFloat(value: Float): JsonObject = buildJsonObject {
put(TYPE_KEY, TYPE_FLOAT)
put(VALUE_KEY, value)
}
internal fun encodeSyncStringSet(values: Set<String>): JsonObject = buildJsonObject {
put(TYPE_KEY, TYPE_STRING_SET)
put(VALUE_KEY, JsonArray(values.sorted().map(::JsonPrimitive)))
}
internal fun JsonObject.decodeSyncString(key: String): String? =
get(key)
?.jsonObject
?.takeIf { it[TYPE_KEY]?.jsonPrimitive?.contentOrNull == TYPE_STRING }
?.get(VALUE_KEY)
?.jsonPrimitive
?.contentOrNull
internal fun JsonObject.decodeSyncBoolean(key: String): Boolean? =
get(key)
?.jsonObject
?.takeIf { it[TYPE_KEY]?.jsonPrimitive?.contentOrNull == TYPE_BOOLEAN }
?.get(VALUE_KEY)
?.jsonPrimitive
?.booleanOrNull
internal fun JsonObject.decodeSyncInt(key: String): Int? =
get(key)
?.jsonObject
?.takeIf { it[TYPE_KEY]?.jsonPrimitive?.contentOrNull == TYPE_INT }
?.get(VALUE_KEY)
?.jsonPrimitive
?.intOrNull
internal fun JsonObject.decodeSyncFloat(key: String): Float? =
get(key)
?.jsonObject
?.takeIf { it[TYPE_KEY]?.jsonPrimitive?.contentOrNull == TYPE_FLOAT }
?.get(VALUE_KEY)
?.jsonPrimitive
?.floatOrNull
internal fun JsonObject.decodeSyncStringSet(key: String): Set<String>? =
get(key)
?.jsonObject
?.takeIf { it[TYPE_KEY]?.jsonPrimitive?.contentOrNull == TYPE_STRING_SET }
?.get(VALUE_KEY)
?.jsonArray
?.mapNotNull { it.jsonPrimitive.contentOrNull?.trim() }
?.filter(String::isNotBlank)
?.toSet()

View file

@ -1,5 +1,7 @@
package com.nuvio.app.features.mdblist
import kotlinx.serialization.json.JsonObject
internal expect object MdbListSettingsStorage {
fun loadEnabled(): Boolean?
fun saveEnabled(enabled: Boolean)
@ -19,4 +21,6 @@ internal expect object MdbListSettingsStorage {
fun saveUseLetterboxd(enabled: Boolean)
fun loadUseAudience(): Boolean?
fun saveUseAudience(enabled: Boolean)
fun exportToSyncPayload(): JsonObject
fun replaceFromSyncPayload(payload: JsonObject)
}

View file

@ -1,5 +1,7 @@
package com.nuvio.app.features.player
import kotlinx.serialization.json.JsonObject
internal expect object PlayerSettingsStorage {
fun loadShowLoadingOverlay(): Boolean?
fun saveShowLoadingOverlay(enabled: Boolean)
@ -61,4 +63,6 @@ internal expect object PlayerSettingsStorage {
fun saveUseLibass(enabled: Boolean)
fun loadLibassRenderType(): String?
fun saveLibassRenderType(renderType: String)
fun exportToSyncPayload(): JsonObject
fun replaceFromSyncPayload(payload: JsonObject)
}

View file

@ -1,8 +1,12 @@
package com.nuvio.app.features.settings
import kotlinx.serialization.json.JsonObject
internal expect object ThemeSettingsStorage {
fun loadSelectedTheme(): String?
fun saveSelectedTheme(themeName: String)
fun loadAmoledEnabled(): Boolean?
fun saveAmoledEnabled(enabled: Boolean)
fun exportToSyncPayload(): JsonObject
fun replaceFromSyncPayload(payload: JsonObject)
}

View file

@ -1,5 +1,7 @@
package com.nuvio.app.features.tmdb
import kotlinx.serialization.json.JsonObject
internal expect object TmdbSettingsStorage {
fun loadEnabled(): Boolean?
fun saveEnabled(enabled: Boolean)
@ -29,4 +31,6 @@ internal expect object TmdbSettingsStorage {
fun saveUseMoreLikeThis(enabled: Boolean)
fun loadUseCollections(): Boolean?
fun saveUseCollections(enabled: Boolean)
fun exportToSyncPayload(): JsonObject
fun replaceFromSyncPayload(payload: JsonObject)
}

View file

@ -1,6 +1,10 @@
package com.nuvio.app.features.trakt
import kotlinx.serialization.json.JsonObject
internal expect object TraktCommentsStorage {
fun loadEnabled(): Boolean?
fun saveEnabled(enabled: Boolean)
fun exportToSyncPayload(): JsonObject
fun replaceFromSyncPayload(payload: JsonObject)
}

View file

@ -1,6 +1,13 @@
package com.nuvio.app.features.mdblist
import com.nuvio.app.core.sync.decodeSyncBoolean
import com.nuvio.app.core.sync.decodeSyncString
import com.nuvio.app.core.sync.encodeSyncBoolean
import com.nuvio.app.core.sync.encodeSyncString
import com.nuvio.app.core.storage.ProfileScopedKey
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import platform.Foundation.NSUserDefaults
actual object MdbListSettingsStorage {
@ -13,6 +20,17 @@ actual object MdbListSettingsStorage {
private const val useTraktKey = "mdblist_use_trakt"
private const val useLetterboxdKey = "mdblist_use_letterboxd"
private const val useAudienceKey = "mdblist_use_audience"
private val syncKeys = listOf(
enabledKey,
apiKey,
useImdbKey,
useTmdbKey,
useTomatoesKey,
useMetacriticKey,
useTraktKey,
useLetterboxdKey,
useAudienceKey,
)
actual fun loadEnabled(): Boolean? = loadBoolean(enabledKey)
@ -82,4 +100,32 @@ actual object MdbListSettingsStorage {
private fun saveBoolean(key: String, enabled: Boolean) {
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(key))
}
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
loadEnabled()?.let { put(enabledKey, encodeSyncBoolean(it)) }
loadApiKey()?.let { put(apiKey, encodeSyncString(it)) }
loadUseImdb()?.let { put(useImdbKey, encodeSyncBoolean(it)) }
loadUseTmdb()?.let { put(useTmdbKey, encodeSyncBoolean(it)) }
loadUseTomatoes()?.let { put(useTomatoesKey, encodeSyncBoolean(it)) }
loadUseMetacritic()?.let { put(useMetacriticKey, encodeSyncBoolean(it)) }
loadUseTrakt()?.let { put(useTraktKey, encodeSyncBoolean(it)) }
loadUseLetterboxd()?.let { put(useLetterboxdKey, encodeSyncBoolean(it)) }
loadUseAudience()?.let { put(useAudienceKey, encodeSyncBoolean(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
syncKeys.forEach { key ->
NSUserDefaults.standardUserDefaults.removeObjectForKey(ProfileScopedKey.of(key))
}
payload.decodeSyncBoolean(enabledKey)?.let(::saveEnabled)
payload.decodeSyncString(apiKey)?.let(::saveApiKey)
payload.decodeSyncBoolean(useImdbKey)?.let(::saveUseImdb)
payload.decodeSyncBoolean(useTmdbKey)?.let(::saveUseTmdb)
payload.decodeSyncBoolean(useTomatoesKey)?.let(::saveUseTomatoes)
payload.decodeSyncBoolean(useMetacriticKey)?.let(::saveUseMetacritic)
payload.decodeSyncBoolean(useTraktKey)?.let(::saveUseTrakt)
payload.decodeSyncBoolean(useLetterboxdKey)?.let(::saveUseLetterboxd)
payload.decodeSyncBoolean(useAudienceKey)?.let(::saveUseAudience)
}
}

View file

@ -1,6 +1,19 @@
package com.nuvio.app.features.player
import com.nuvio.app.core.sync.decodeSyncBoolean
import com.nuvio.app.core.sync.decodeSyncFloat
import com.nuvio.app.core.sync.decodeSyncInt
import com.nuvio.app.core.sync.decodeSyncString
import com.nuvio.app.core.sync.decodeSyncStringSet
import com.nuvio.app.core.sync.encodeSyncBoolean
import com.nuvio.app.core.sync.encodeSyncFloat
import com.nuvio.app.core.sync.encodeSyncInt
import com.nuvio.app.core.sync.encodeSyncString
import com.nuvio.app.core.sync.encodeSyncStringSet
import com.nuvio.app.core.storage.ProfileScopedKey
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import platform.Foundation.NSUserDefaults
actual object PlayerSettingsStorage {
@ -32,6 +45,40 @@ actual object PlayerSettingsStorage {
private const val nextEpisodeThresholdModeKey = "next_episode_threshold_mode"
private const val nextEpisodeThresholdPercentKey = "next_episode_threshold_percent_v2"
private const val nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
private const val useLibassKey = "use_libass"
private const val libassRenderTypeKey = "libass_render_type"
private val syncKeys = listOf(
showLoadingOverlayKey,
preferredAudioLanguageKey,
secondaryPreferredAudioLanguageKey,
preferredSubtitleLanguageKey,
secondaryPreferredSubtitleLanguageKey,
subtitleTextColorKey,
subtitleOutlineEnabledKey,
subtitleFontSizeSpKey,
subtitleBottomOffsetKey,
streamReuseLastLinkEnabledKey,
streamReuseLastLinkCacheHoursKey,
decoderPriorityKey,
mapDV7ToHevcKey,
tunnelingEnabledKey,
streamAutoPlayModeKey,
streamAutoPlaySourceKey,
streamAutoPlaySelectedAddonsKey,
streamAutoPlaySelectedPluginsKey,
streamAutoPlayRegexKey,
streamAutoPlayTimeoutSecondsKey,
skipIntroEnabledKey,
animeSkipEnabledKey,
animeSkipClientIdKey,
streamAutoPlayNextEpisodeEnabledKey,
streamAutoPlayPreferBingeGroupKey,
nextEpisodeThresholdModeKey,
nextEpisodeThresholdPercentKey,
nextEpisodeThresholdMinutesBeforeEndKey,
useLibassKey,
libassRenderTypeKey,
)
actual fun loadShowLoadingOverlay(): Boolean? {
val defaults = NSUserDefaults.standardUserDefaults
@ -400,4 +447,74 @@ actual object PlayerSettingsStorage {
actual fun loadLibassRenderType(): String? = null
actual fun saveLibassRenderType(renderType: String) {}
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
loadShowLoadingOverlay()?.let { put(showLoadingOverlayKey, encodeSyncBoolean(it)) }
loadPreferredAudioLanguage()?.let { put(preferredAudioLanguageKey, encodeSyncString(it)) }
loadSecondaryPreferredAudioLanguage()?.let { put(secondaryPreferredAudioLanguageKey, encodeSyncString(it)) }
loadPreferredSubtitleLanguage()?.let { put(preferredSubtitleLanguageKey, encodeSyncString(it)) }
loadSecondaryPreferredSubtitleLanguage()?.let { put(secondaryPreferredSubtitleLanguageKey, encodeSyncString(it)) }
loadSubtitleTextColor()?.let { put(subtitleTextColorKey, encodeSyncString(it)) }
loadSubtitleOutlineEnabled()?.let { put(subtitleOutlineEnabledKey, encodeSyncBoolean(it)) }
loadSubtitleFontSizeSp()?.let { put(subtitleFontSizeSpKey, encodeSyncInt(it)) }
loadSubtitleBottomOffset()?.let { put(subtitleBottomOffsetKey, encodeSyncInt(it)) }
loadStreamReuseLastLinkEnabled()?.let { put(streamReuseLastLinkEnabledKey, encodeSyncBoolean(it)) }
loadStreamReuseLastLinkCacheHours()?.let { put(streamReuseLastLinkCacheHoursKey, encodeSyncInt(it)) }
loadDecoderPriority()?.let { put(decoderPriorityKey, encodeSyncInt(it)) }
loadMapDV7ToHevc()?.let { put(mapDV7ToHevcKey, encodeSyncBoolean(it)) }
loadTunnelingEnabled()?.let { put(tunnelingEnabledKey, encodeSyncBoolean(it)) }
loadStreamAutoPlayMode()?.let { put(streamAutoPlayModeKey, encodeSyncString(it)) }
loadStreamAutoPlaySource()?.let { put(streamAutoPlaySourceKey, encodeSyncString(it)) }
loadStreamAutoPlaySelectedAddons()?.let { put(streamAutoPlaySelectedAddonsKey, encodeSyncStringSet(it)) }
loadStreamAutoPlaySelectedPlugins()?.let { put(streamAutoPlaySelectedPluginsKey, encodeSyncStringSet(it)) }
loadStreamAutoPlayRegex()?.let { put(streamAutoPlayRegexKey, encodeSyncString(it)) }
loadStreamAutoPlayTimeoutSeconds()?.let { put(streamAutoPlayTimeoutSecondsKey, encodeSyncInt(it)) }
loadSkipIntroEnabled()?.let { put(skipIntroEnabledKey, encodeSyncBoolean(it)) }
loadAnimeSkipEnabled()?.let { put(animeSkipEnabledKey, encodeSyncBoolean(it)) }
loadAnimeSkipClientId()?.let { put(animeSkipClientIdKey, encodeSyncString(it)) }
loadStreamAutoPlayNextEpisodeEnabled()?.let { put(streamAutoPlayNextEpisodeEnabledKey, encodeSyncBoolean(it)) }
loadStreamAutoPlayPreferBingeGroup()?.let { put(streamAutoPlayPreferBingeGroupKey, encodeSyncBoolean(it)) }
loadNextEpisodeThresholdMode()?.let { put(nextEpisodeThresholdModeKey, encodeSyncString(it)) }
loadNextEpisodeThresholdPercent()?.let { put(nextEpisodeThresholdPercentKey, encodeSyncFloat(it)) }
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
loadUseLibass()?.let { put(useLibassKey, encodeSyncBoolean(it)) }
loadLibassRenderType()?.let { put(libassRenderTypeKey, encodeSyncString(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
syncKeys.forEach { key ->
NSUserDefaults.standardUserDefaults.removeObjectForKey(ProfileScopedKey.of(key))
}
payload.decodeSyncBoolean(showLoadingOverlayKey)?.let(::saveShowLoadingOverlay)
payload.decodeSyncString(preferredAudioLanguageKey)?.let(::savePreferredAudioLanguage)
payload.decodeSyncString(secondaryPreferredAudioLanguageKey)?.let(::saveSecondaryPreferredAudioLanguage)
payload.decodeSyncString(preferredSubtitleLanguageKey)?.let(::savePreferredSubtitleLanguage)
payload.decodeSyncString(secondaryPreferredSubtitleLanguageKey)?.let(::saveSecondaryPreferredSubtitleLanguage)
payload.decodeSyncString(subtitleTextColorKey)?.let(::saveSubtitleTextColor)
payload.decodeSyncBoolean(subtitleOutlineEnabledKey)?.let(::saveSubtitleOutlineEnabled)
payload.decodeSyncInt(subtitleFontSizeSpKey)?.let(::saveSubtitleFontSizeSp)
payload.decodeSyncInt(subtitleBottomOffsetKey)?.let(::saveSubtitleBottomOffset)
payload.decodeSyncBoolean(streamReuseLastLinkEnabledKey)?.let(::saveStreamReuseLastLinkEnabled)
payload.decodeSyncInt(streamReuseLastLinkCacheHoursKey)?.let(::saveStreamReuseLastLinkCacheHours)
payload.decodeSyncInt(decoderPriorityKey)?.let(::saveDecoderPriority)
payload.decodeSyncBoolean(mapDV7ToHevcKey)?.let(::saveMapDV7ToHevc)
payload.decodeSyncBoolean(tunnelingEnabledKey)?.let(::saveTunnelingEnabled)
payload.decodeSyncString(streamAutoPlayModeKey)?.let(::saveStreamAutoPlayMode)
payload.decodeSyncString(streamAutoPlaySourceKey)?.let(::saveStreamAutoPlaySource)
payload.decodeSyncStringSet(streamAutoPlaySelectedAddonsKey)?.let(::saveStreamAutoPlaySelectedAddons)
payload.decodeSyncStringSet(streamAutoPlaySelectedPluginsKey)?.let(::saveStreamAutoPlaySelectedPlugins)
payload.decodeSyncString(streamAutoPlayRegexKey)?.let(::saveStreamAutoPlayRegex)
payload.decodeSyncInt(streamAutoPlayTimeoutSecondsKey)?.let(::saveStreamAutoPlayTimeoutSeconds)
payload.decodeSyncBoolean(skipIntroEnabledKey)?.let(::saveSkipIntroEnabled)
payload.decodeSyncBoolean(animeSkipEnabledKey)?.let(::saveAnimeSkipEnabled)
payload.decodeSyncString(animeSkipClientIdKey)?.let(::saveAnimeSkipClientId)
payload.decodeSyncBoolean(streamAutoPlayNextEpisodeEnabledKey)?.let(::saveStreamAutoPlayNextEpisodeEnabled)
payload.decodeSyncBoolean(streamAutoPlayPreferBingeGroupKey)?.let(::saveStreamAutoPlayPreferBingeGroup)
payload.decodeSyncString(nextEpisodeThresholdModeKey)?.let(::saveNextEpisodeThresholdMode)
payload.decodeSyncFloat(nextEpisodeThresholdPercentKey)?.let(::saveNextEpisodeThresholdPercent)
payload.decodeSyncFloat(nextEpisodeThresholdMinutesBeforeEndKey)?.let(::saveNextEpisodeThresholdMinutesBeforeEnd)
payload.decodeSyncBoolean(useLibassKey)?.let(::saveUseLibass)
payload.decodeSyncString(libassRenderTypeKey)?.let(::saveLibassRenderType)
}
}

View file

@ -1,11 +1,19 @@
package com.nuvio.app.features.settings
import com.nuvio.app.core.sync.decodeSyncBoolean
import com.nuvio.app.core.sync.decodeSyncString
import com.nuvio.app.core.sync.encodeSyncBoolean
import com.nuvio.app.core.sync.encodeSyncString
import com.nuvio.app.core.storage.ProfileScopedKey
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import platform.Foundation.NSUserDefaults
actual object ThemeSettingsStorage {
private const val selectedThemeKey = "selected_theme"
private const val amoledEnabledKey = "amoled_enabled"
private val syncKeys = listOf(selectedThemeKey, amoledEnabledKey)
actual fun loadSelectedTheme(): String? =
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(selectedThemeKey))
@ -27,4 +35,18 @@ actual object ThemeSettingsStorage {
actual fun saveAmoledEnabled(enabled: Boolean) {
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(amoledEnabledKey))
}
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
loadSelectedTheme()?.let { put(selectedThemeKey, encodeSyncString(it)) }
loadAmoledEnabled()?.let { put(amoledEnabledKey, encodeSyncBoolean(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
syncKeys.forEach { key ->
NSUserDefaults.standardUserDefaults.removeObjectForKey(ProfileScopedKey.of(key))
}
payload.decodeSyncString(selectedThemeKey)?.let(::saveSelectedTheme)
payload.decodeSyncBoolean(amoledEnabledKey)?.let(::saveAmoledEnabled)
}
}

View file

@ -1,6 +1,13 @@
package com.nuvio.app.features.tmdb
import com.nuvio.app.core.sync.decodeSyncBoolean
import com.nuvio.app.core.sync.decodeSyncString
import com.nuvio.app.core.sync.encodeSyncBoolean
import com.nuvio.app.core.sync.encodeSyncString
import com.nuvio.app.core.storage.ProfileScopedKey
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import platform.Foundation.NSUserDefaults
actual object TmdbSettingsStorage {
@ -18,6 +25,22 @@ actual object TmdbSettingsStorage {
private const val useSeasonPostersKey = "tmdb_use_season_posters"
private const val useMoreLikeThisKey = "tmdb_use_more_like_this"
private const val useCollectionsKey = "tmdb_use_collections"
private val syncKeys = listOf(
enabledKey,
apiKeyKey,
languageKey,
useTrailersKey,
useArtworkKey,
useBasicInfoKey,
useDetailsKey,
useCreditsKey,
useProductionsKey,
useNetworksKey,
useEpisodesKey,
useSeasonPostersKey,
useMoreLikeThisKey,
useCollectionsKey,
)
actual fun loadEnabled(): Boolean? = loadBoolean(enabledKey)
@ -118,4 +141,42 @@ actual object TmdbSettingsStorage {
private fun saveBoolean(key: String, enabled: Boolean) {
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(key))
}
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
loadEnabled()?.let { put(enabledKey, encodeSyncBoolean(it)) }
loadApiKey()?.let { put(apiKeyKey, encodeSyncString(it)) }
loadLanguage()?.let { put(languageKey, encodeSyncString(it)) }
loadUseTrailers()?.let { put(useTrailersKey, encodeSyncBoolean(it)) }
loadUseArtwork()?.let { put(useArtworkKey, encodeSyncBoolean(it)) }
loadUseBasicInfo()?.let { put(useBasicInfoKey, encodeSyncBoolean(it)) }
loadUseDetails()?.let { put(useDetailsKey, encodeSyncBoolean(it)) }
loadUseCredits()?.let { put(useCreditsKey, encodeSyncBoolean(it)) }
loadUseProductions()?.let { put(useProductionsKey, encodeSyncBoolean(it)) }
loadUseNetworks()?.let { put(useNetworksKey, encodeSyncBoolean(it)) }
loadUseEpisodes()?.let { put(useEpisodesKey, encodeSyncBoolean(it)) }
loadUseSeasonPosters()?.let { put(useSeasonPostersKey, encodeSyncBoolean(it)) }
loadUseMoreLikeThis()?.let { put(useMoreLikeThisKey, encodeSyncBoolean(it)) }
loadUseCollections()?.let { put(useCollectionsKey, encodeSyncBoolean(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
syncKeys.forEach { key ->
NSUserDefaults.standardUserDefaults.removeObjectForKey(ProfileScopedKey.of(key))
}
payload.decodeSyncBoolean(enabledKey)?.let(::saveEnabled)
payload.decodeSyncString(apiKeyKey)?.let(::saveApiKey)
payload.decodeSyncString(languageKey)?.let(::saveLanguage)
payload.decodeSyncBoolean(useTrailersKey)?.let(::saveUseTrailers)
payload.decodeSyncBoolean(useArtworkKey)?.let(::saveUseArtwork)
payload.decodeSyncBoolean(useBasicInfoKey)?.let(::saveUseBasicInfo)
payload.decodeSyncBoolean(useDetailsKey)?.let(::saveUseDetails)
payload.decodeSyncBoolean(useCreditsKey)?.let(::saveUseCredits)
payload.decodeSyncBoolean(useProductionsKey)?.let(::saveUseProductions)
payload.decodeSyncBoolean(useNetworksKey)?.let(::saveUseNetworks)
payload.decodeSyncBoolean(useEpisodesKey)?.let(::saveUseEpisodes)
payload.decodeSyncBoolean(useSeasonPostersKey)?.let(::saveUseSeasonPosters)
payload.decodeSyncBoolean(useMoreLikeThisKey)?.let(::saveUseMoreLikeThis)
payload.decodeSyncBoolean(useCollectionsKey)?.let(::saveUseCollections)
}
}

View file

@ -1,10 +1,16 @@
package com.nuvio.app.features.trakt
import com.nuvio.app.core.sync.decodeSyncBoolean
import com.nuvio.app.core.sync.encodeSyncBoolean
import com.nuvio.app.core.storage.ProfileScopedKey
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import platform.Foundation.NSUserDefaults
internal actual object TraktCommentsStorage {
private const val enabledKey = "comments_enabled"
private val syncKeys = listOf(enabledKey)
actual fun loadEnabled(): Boolean? {
val defaults = NSUserDefaults.standardUserDefaults
@ -19,4 +25,16 @@ internal actual object TraktCommentsStorage {
actual fun saveEnabled(enabled: Boolean) {
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(enabledKey))
}
actual fun exportToSyncPayload(): JsonObject = buildJsonObject {
loadEnabled()?.let { put(enabledKey, encodeSyncBoolean(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
syncKeys.forEach { key ->
NSUserDefaults.standardUserDefaults.removeObjectForKey(ProfileScopedKey.of(key))
}
payload.decodeSyncBoolean(enabledKey)?.let(::saveEnabled)
}
}