diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt index 9734a2387..c53c76d3d 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/MainActivity.kt @@ -8,6 +8,7 @@ import com.nuvio.app.features.addons.AddonStorage import com.nuvio.app.features.library.LibraryStorage import com.nuvio.app.features.home.HomeCatalogSettingsStorage import com.nuvio.app.features.player.PlayerSettingsStorage +import com.nuvio.app.features.profiles.ProfileStorage import com.nuvio.app.features.watched.WatchedStorage import com.nuvio.app.features.watchprogress.ContinueWatchingPreferencesStorage import com.nuvio.app.features.watchprogress.WatchProgressStorage @@ -21,6 +22,7 @@ class MainActivity : ComponentActivity() { WatchedStorage.initialize(applicationContext) HomeCatalogSettingsStorage.initialize(applicationContext) PlayerSettingsStorage.initialize(applicationContext) + ProfileStorage.initialize(applicationContext) ContinueWatchingPreferencesStorage.initialize(applicationContext) WatchProgressStorage.initialize(applicationContext) diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/profiles/ProfileStorage.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/profiles/ProfileStorage.android.kt new file mode 100644 index 000000000..a530dbe52 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/profiles/ProfileStorage.android.kt @@ -0,0 +1,25 @@ +package com.nuvio.app.features.profiles + +import android.content.Context +import android.content.SharedPreferences + +actual object ProfileStorage { + private const val preferencesName = "nuvio_profile_cache" + private const val payloadKey = "profile_payload" + + private var preferences: SharedPreferences? = null + + fun initialize(context: Context) { + preferences = context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE) + } + + actual fun loadPayload(): String? = + preferences?.getString(payloadKey, null) + + actual fun savePayload(payload: String) { + preferences + ?.edit() + ?.putString(payloadKey, payload) + ?.apply() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index dd89c35ee..dbe4e3cd2 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -156,8 +156,13 @@ fun App() { LaunchedEffect(authState) { when (authState) { is AuthState.Loading -> gateScreen = AppGateScreen.Loading.name - is AuthState.Unauthenticated -> gateScreen = AppGateScreen.Auth.name + is AuthState.Unauthenticated -> { + ProfileRepository.clearInMemory() + gateScreen = AppGateScreen.Auth.name + } is AuthState.Authenticated -> { + val authenticatedState = authState as AuthState.Authenticated + ProfileRepository.ensureLoaded(authenticatedState.userId) if (gateScreen == AppGateScreen.Loading.name || gateScreen == AppGateScreen.Auth.name) { gateScreen = AppGateScreen.ProfileSelection.name } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt index a708d20c7..6e9e00491 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileRepository.kt @@ -1,6 +1,8 @@ package com.nuvio.app.features.profiles 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 io.github.jan.supabase.postgrest.postgrest import io.github.jan.supabase.postgrest.rpc @@ -11,11 +13,21 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.put +@Serializable +private data class StoredProfilePayload( + val userId: String, + val activeProfileIndex: Int = 1, + val profiles: List = emptyList(), +) + object ProfileRepository { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val log = Logger.withTag("ProfileRepository") @@ -25,9 +37,47 @@ object ProfileRepository { val state: StateFlow = _state.asStateFlow() private var activeProfileIndex: Int = 1 + private var loadedCacheForUserId: String? = null val activeProfileId: Int get() = activeProfileIndex + fun ensureLoaded(userId: String) { + if (loadedCacheForUserId == userId && _state.value.isLoaded) return + + loadedCacheForUserId = userId + val payload = ProfileStorage.loadPayload().orEmpty().trim() + if (payload.isEmpty()) { + _state.value = ProfileState() + activeProfileIndex = 1 + return + } + + val stored = runCatching { + json.decodeFromString(payload) + }.getOrNull() ?: return + + if (stored.userId != userId) { + _state.value = ProfileState() + activeProfileIndex = 1 + return + } + + val profiles = stored.profiles.sortedBy { it.profileIndex } + activeProfileIndex = stored.activeProfileIndex + _state.value = ProfileState( + profiles = profiles, + activeProfile = profiles.find { it.profileIndex == activeProfileIndex } ?: profiles.firstOrNull(), + isLoaded = profiles.isNotEmpty(), + ) + _state.value.activeProfile?.let { activeProfileIndex = it.profileIndex } + } + + fun clearInMemory() { + loadedCacheForUserId = null + activeProfileIndex = 1 + _state.value = ProfileState() + } + suspend fun pullProfiles() { runCatching { val result = SupabaseProvider.client.postgrest.rpc("sync_pull_profiles") @@ -41,6 +91,7 @@ object ProfileRepository { if (_state.value.activeProfile != null) { activeProfileIndex = _state.value.activeProfile!!.profileIndex } + persist() }.onFailure { e -> log.e(e) { "Failed to pull profiles" } if (!_state.value.isLoaded) { @@ -54,6 +105,7 @@ object ProfileRepository { _state.value = _state.value.copy( activeProfile = _state.value.profiles.find { it.profileIndex == profileIndex }, ) + persist() } suspend fun pushProfiles(profiles: List) { @@ -201,6 +253,19 @@ object ProfileRepository { emptyList() } } + + private fun persist() { + val authState = AuthRepository.state.value as? AuthState.Authenticated ?: return + ProfileStorage.savePayload( + json.encodeToString( + StoredProfilePayload( + userId = authState.userId, + activeProfileIndex = activeProfileIndex, + profiles = _state.value.profiles, + ), + ), + ) + } } @kotlinx.serialization.Serializable diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileStorage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileStorage.kt new file mode 100644 index 000000000..6864505b4 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileStorage.kt @@ -0,0 +1,6 @@ +package com.nuvio.app.features.profiles + +internal expect object ProfileStorage { + fun loadPayload(): String? + fun savePayload(payload: String) +} \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/profiles/ProfileStorage.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/profiles/ProfileStorage.ios.kt new file mode 100644 index 000000000..bded3fdbb --- /dev/null +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/profiles/ProfileStorage.ios.kt @@ -0,0 +1,14 @@ +package com.nuvio.app.features.profiles + +import platform.Foundation.NSUserDefaults + +actual object ProfileStorage { + private const val payloadKey = "profile_payload" + + actual fun loadPayload(): String? = + NSUserDefaults.standardUserDefaults.stringForKey(payloadKey) + + actual fun savePayload(payload: String) { + NSUserDefaults.standardUserDefaults.setObject(payload, forKey = payloadKey) + } +} \ No newline at end of file