mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 07:21:58 +00:00
feat: avatar storage and caching functionality
This commit is contained in:
parent
bd0540df72
commit
161e5d81bb
12 changed files with 208 additions and 35 deletions
|
|
@ -25,6 +25,7 @@ import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsStorage
|
||||||
import com.nuvio.app.features.player.PlayerSettingsStorage
|
import com.nuvio.app.features.player.PlayerSettingsStorage
|
||||||
import com.nuvio.app.features.player.PlayerPictureInPictureManager
|
import com.nuvio.app.features.player.PlayerPictureInPictureManager
|
||||||
import com.nuvio.app.features.plugins.PluginStorage
|
import com.nuvio.app.features.plugins.PluginStorage
|
||||||
|
import com.nuvio.app.features.profiles.AvatarStorage
|
||||||
import com.nuvio.app.features.profiles.ProfileStorage
|
import com.nuvio.app.features.profiles.ProfileStorage
|
||||||
import com.nuvio.app.features.details.SeasonViewModeStorage
|
import com.nuvio.app.features.details.SeasonViewModeStorage
|
||||||
import com.nuvio.app.features.search.SearchHistoryStorage
|
import com.nuvio.app.features.search.SearchHistoryStorage
|
||||||
|
|
@ -59,6 +60,7 @@ class MainActivity : ComponentActivity() {
|
||||||
HomeCatalogSettingsStorage.initialize(applicationContext)
|
HomeCatalogSettingsStorage.initialize(applicationContext)
|
||||||
PlayerSettingsStorage.initialize(applicationContext)
|
PlayerSettingsStorage.initialize(applicationContext)
|
||||||
ProfileStorage.initialize(applicationContext)
|
ProfileStorage.initialize(applicationContext)
|
||||||
|
AvatarStorage.initialize(applicationContext)
|
||||||
SearchHistoryStorage.initialize(applicationContext)
|
SearchHistoryStorage.initialize(applicationContext)
|
||||||
SeasonViewModeStorage.initialize(applicationContext)
|
SeasonViewModeStorage.initialize(applicationContext)
|
||||||
ThemeSettingsStorage.initialize(applicationContext)
|
ThemeSettingsStorage.initialize(applicationContext)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ internal actual object PlatformLocalAccountDataCleaner {
|
||||||
"nuvio_home_catalog_settings",
|
"nuvio_home_catalog_settings",
|
||||||
"nuvio_player_settings",
|
"nuvio_player_settings",
|
||||||
"nuvio_profile_cache",
|
"nuvio_profile_cache",
|
||||||
|
"nuvio_avatar_cache",
|
||||||
"nuvio_theme_settings",
|
"nuvio_theme_settings",
|
||||||
"nuvio_poster_card_style",
|
"nuvio_poster_card_style",
|
||||||
"nuvio_mdblist_settings",
|
"nuvio_mdblist_settings",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.nuvio.app.features.profiles
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
actual object AvatarStorage {
|
||||||
|
private const val preferencesName = "nuvio_avatar_cache"
|
||||||
|
private const val payloadKey = "avatar_catalog_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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -278,40 +278,81 @@ fun App() {
|
||||||
AuthRepository.initialize()
|
AuthRepository.initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
NetworkStatusRepository.ensureStarted()
|
||||||
|
ProfileRepository.loadCachedProfiles()
|
||||||
|
}
|
||||||
|
|
||||||
val authState by AuthRepository.state.collectAsStateWithLifecycle()
|
val authState by AuthRepository.state.collectAsStateWithLifecycle()
|
||||||
val profileState by ProfileRepository.state.collectAsStateWithLifecycle()
|
val profileState by ProfileRepository.state.collectAsStateWithLifecycle()
|
||||||
|
val networkStatusUiState by remember {
|
||||||
|
NetworkStatusRepository.uiState
|
||||||
|
}.collectAsStateWithLifecycle()
|
||||||
var gateScreen by rememberSaveable { mutableStateOf(AppGateScreen.Loading.name) }
|
var gateScreen by rememberSaveable { mutableStateOf(AppGateScreen.Loading.name) }
|
||||||
var editingProfile by remember { mutableStateOf<NuvioProfile?>(null) }
|
var editingProfile by remember { mutableStateOf<NuvioProfile?>(null) }
|
||||||
var isNewProfile by remember { mutableStateOf(false) }
|
var isNewProfile by remember { mutableStateOf(false) }
|
||||||
var autoSkipProfileSelection by rememberSaveable { mutableStateOf(false) }
|
var autoSkipProfileSelection by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(authState) {
|
fun enterProfileGate(profiles: List<NuvioProfile>, syncOnEnter: Boolean) {
|
||||||
|
if (profiles.isEmpty()) {
|
||||||
|
autoSkipProfileSelection = true
|
||||||
|
gateScreen = AppGateScreen.ProfileSelection.name
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
autoSkipProfileSelection = true
|
||||||
|
if (profiles.size == 1) {
|
||||||
|
val onlyProfile = profiles.first()
|
||||||
|
ProfileRepository.selectProfile(onlyProfile.profileIndex)
|
||||||
|
if (syncOnEnter) {
|
||||||
|
SyncManager.pullAllForProfile(onlyProfile.profileIndex)
|
||||||
|
}
|
||||||
|
gateScreen = AppGateScreen.Main.name
|
||||||
|
autoSkipProfileSelection = false
|
||||||
|
} else {
|
||||||
|
gateScreen = AppGateScreen.ProfileSelection.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(authState, networkStatusUiState.condition, profileState.profiles) {
|
||||||
|
val cachedProfiles = profileState.profiles
|
||||||
|
val allowOfflineProfileAccess =
|
||||||
|
cachedProfiles.isNotEmpty() &&
|
||||||
|
authState !is AuthState.Authenticated &&
|
||||||
|
networkStatusUiState.condition != NetworkCondition.Online
|
||||||
|
|
||||||
when (authState) {
|
when (authState) {
|
||||||
is AuthState.Loading -> gateScreen = AppGateScreen.Loading.name
|
is AuthState.Loading -> {
|
||||||
|
if (allowOfflineProfileAccess) {
|
||||||
|
enterProfileGate(cachedProfiles, syncOnEnter = false)
|
||||||
|
} else {
|
||||||
|
gateScreen = AppGateScreen.Loading.name
|
||||||
|
}
|
||||||
|
}
|
||||||
is AuthState.Unauthenticated -> {
|
is AuthState.Unauthenticated -> {
|
||||||
ProfileRepository.clearInMemory()
|
if (allowOfflineProfileAccess) {
|
||||||
gateScreen = AppGateScreen.Auth.name
|
enterProfileGate(cachedProfiles, syncOnEnter = false)
|
||||||
|
} else {
|
||||||
|
ProfileRepository.clearInMemory()
|
||||||
|
gateScreen = AppGateScreen.Auth.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is AuthState.Authenticated -> {
|
is AuthState.Authenticated -> {
|
||||||
val authenticatedState = authState as AuthState.Authenticated
|
val authenticatedState = authState as AuthState.Authenticated
|
||||||
ProfileRepository.ensureLoaded(authenticatedState.userId)
|
ProfileRepository.ensureLoaded(authenticatedState.userId)
|
||||||
if (gateScreen == AppGateScreen.Loading.name || gateScreen == AppGateScreen.Auth.name) {
|
if (gateScreen == AppGateScreen.Loading.name || gateScreen == AppGateScreen.Auth.name) {
|
||||||
autoSkipProfileSelection = true
|
enterProfileGate(ProfileRepository.state.value.profiles, syncOnEnter = true)
|
||||||
val cachedProfiles = ProfileRepository.state.value.profiles
|
|
||||||
if (cachedProfiles.size == 1) {
|
|
||||||
val onlyProfile = cachedProfiles.first()
|
|
||||||
ProfileRepository.selectProfile(onlyProfile.profileIndex)
|
|
||||||
SyncManager.pullAllForProfile(onlyProfile.profileIndex)
|
|
||||||
gateScreen = AppGateScreen.Main.name
|
|
||||||
autoSkipProfileSelection = false
|
|
||||||
} else {
|
|
||||||
gateScreen = AppGateScreen.ProfileSelection.name
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect((authState as? AuthState.Authenticated)?.userId) {
|
||||||
|
val authenticatedState = authState as? AuthState.Authenticated ?: return@LaunchedEffect
|
||||||
|
ProfileRepository.ensureLoaded(authenticatedState.userId)
|
||||||
|
ProfileRepository.pullProfiles()
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(gateScreen, autoSkipProfileSelection, profileState.profiles) {
|
LaunchedEffect(gateScreen, autoSkipProfileSelection, profileState.profiles) {
|
||||||
if (
|
if (
|
||||||
autoSkipProfileSelection &&
|
autoSkipProfileSelection &&
|
||||||
|
|
@ -352,7 +393,9 @@ fun App() {
|
||||||
ProfileSelectionScreen(
|
ProfileSelectionScreen(
|
||||||
onProfileSelected = { profile ->
|
onProfileSelected = { profile ->
|
||||||
ProfileRepository.selectProfile(profile.profileIndex)
|
ProfileRepository.selectProfile(profile.profileIndex)
|
||||||
SyncManager.pullAllForProfile(profile.profileIndex)
|
if (authState is AuthState.Authenticated) {
|
||||||
|
SyncManager.pullAllForProfile(profile.profileIndex)
|
||||||
|
}
|
||||||
gateScreen = AppGateScreen.Main.name
|
gateScreen = AppGateScreen.Main.name
|
||||||
},
|
},
|
||||||
onEditProfile = { profile ->
|
onEditProfile = { profile ->
|
||||||
|
|
|
||||||
|
|
@ -7,34 +7,78 @@ import io.github.jan.supabase.postgrest.rpc
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class StoredAvatarCatalogPayload(
|
||||||
|
val items: List<AvatarCatalogItem> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
object AvatarRepository {
|
object AvatarRepository {
|
||||||
private val log = Logger.withTag("AvatarRepository")
|
private val log = Logger.withTag("AvatarRepository")
|
||||||
|
private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true }
|
||||||
|
|
||||||
private val _avatars = MutableStateFlow<List<AvatarCatalogItem>>(emptyList())
|
private val _avatars = MutableStateFlow<List<AvatarCatalogItem>>(emptyList())
|
||||||
val avatars: StateFlow<List<AvatarCatalogItem>> = _avatars.asStateFlow()
|
val avatars: StateFlow<List<AvatarCatalogItem>> = _avatars.asStateFlow()
|
||||||
|
|
||||||
private var loaded = false
|
private var loaded = false
|
||||||
|
private var cacheHydrated = false
|
||||||
|
private var fetchInFlight = false
|
||||||
|
|
||||||
suspend fun fetchAvatars() {
|
suspend fun fetchAvatars() {
|
||||||
|
hydrateFromCacheIfNeeded()
|
||||||
if (loaded && _avatars.value.isNotEmpty()) return
|
if (loaded && _avatars.value.isNotEmpty()) return
|
||||||
doFetch()
|
doFetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun refreshAvatars() {
|
suspend fun refreshAvatars() {
|
||||||
|
hydrateFromCacheIfNeeded()
|
||||||
doFetch()
|
doFetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hydrateFromCacheIfNeeded() {
|
||||||
|
if (cacheHydrated) return
|
||||||
|
cacheHydrated = true
|
||||||
|
|
||||||
|
val payload = AvatarStorage.loadPayload().orEmpty().trim()
|
||||||
|
if (payload.isEmpty()) return
|
||||||
|
|
||||||
|
val stored = runCatching {
|
||||||
|
json.decodeFromString<StoredAvatarCatalogPayload>(payload)
|
||||||
|
}.getOrNull() ?: return
|
||||||
|
|
||||||
|
val items = stored.items
|
||||||
|
.filter { it.isActive }
|
||||||
|
.sortedWith(compareBy({ it.category }, { it.sortOrder }))
|
||||||
|
if (items.isEmpty()) return
|
||||||
|
|
||||||
|
_avatars.value = items
|
||||||
|
loaded = true
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun doFetch() {
|
private suspend fun doFetch() {
|
||||||
|
if (fetchInFlight) return
|
||||||
|
fetchInFlight = true
|
||||||
runCatching {
|
runCatching {
|
||||||
val result = SupabaseProvider.client.postgrest.rpc("get_avatar_catalog")
|
val result = SupabaseProvider.client.postgrest.rpc("get_avatar_catalog")
|
||||||
val items = result.decodeList<AvatarCatalogItem>()
|
val items = result.decodeList<AvatarCatalogItem>()
|
||||||
_avatars.value = items.filter { it.isActive }.sortedWith(
|
val activeItems = items.filter { it.isActive }.sortedWith(
|
||||||
compareBy({ it.category }, { it.sortOrder }),
|
compareBy({ it.category }, { it.sortOrder }),
|
||||||
)
|
)
|
||||||
|
_avatars.value = activeItems
|
||||||
loaded = true
|
loaded = true
|
||||||
|
AvatarStorage.savePayload(
|
||||||
|
json.encodeToString(
|
||||||
|
StoredAvatarCatalogPayload(items = activeItems),
|
||||||
|
),
|
||||||
|
)
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
log.e(e) { "Failed to fetch avatar catalog" }
|
log.e(e) { "Failed to fetch avatar catalog" }
|
||||||
|
}.also {
|
||||||
|
fetchInFlight = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.nuvio.app.features.profiles
|
||||||
|
|
||||||
|
internal expect object AvatarStorage {
|
||||||
|
fun loadPayload(): String?
|
||||||
|
fun savePayload(payload: String)
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,8 @@ import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import com.nuvio.app.core.auth.AuthRepository
|
||||||
|
import com.nuvio.app.core.auth.AuthState
|
||||||
import com.nuvio.app.core.ui.NuvioInputField
|
import com.nuvio.app.core.ui.NuvioInputField
|
||||||
import com.nuvio.app.core.ui.NuvioPrimaryButton
|
import com.nuvio.app.core.ui.NuvioPrimaryButton
|
||||||
import com.nuvio.app.core.ui.NuvioScreen
|
import com.nuvio.app.core.ui.NuvioScreen
|
||||||
|
|
@ -79,9 +81,13 @@ fun ProfileEditScreen(
|
||||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||||
var showPinSetup by remember { mutableStateOf(false) }
|
var showPinSetup by remember { mutableStateOf(false) }
|
||||||
var showPinClear by remember { mutableStateOf(false) }
|
var showPinClear by remember { mutableStateOf(false) }
|
||||||
|
val authState by AuthRepository.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val avatars by AvatarRepository.avatars.collectAsStateWithLifecycle()
|
val avatars by AvatarRepository.avatars.collectAsStateWithLifecycle()
|
||||||
LaunchedEffect(Unit) { AvatarRepository.fetchAvatars() }
|
LaunchedEffect(Unit) {
|
||||||
|
AvatarRepository.fetchAvatars()
|
||||||
|
AvatarRepository.refreshAvatars()
|
||||||
|
}
|
||||||
LaunchedEffect(isNew, avatars, selectedAvatarId) {
|
LaunchedEffect(isNew, avatars, selectedAvatarId) {
|
||||||
if (isNew && selectedAvatarId == null && avatars.isNotEmpty()) {
|
if (isNew && selectedAvatarId == null && avatars.isNotEmpty()) {
|
||||||
selectedAvatarId = avatars.first().id
|
selectedAvatarId = avatars.first().id
|
||||||
|
|
@ -272,7 +278,11 @@ fun ProfileEditScreen(
|
||||||
hasExistingPin = currentProfile.pinEnabled,
|
hasExistingPin = currentProfile.pinEnabled,
|
||||||
onDone = {
|
onDone = {
|
||||||
showPinSetup = false
|
showPinSetup = false
|
||||||
scope.launch { ProfileRepository.pullProfiles() }
|
scope.launch {
|
||||||
|
if (authState is AuthState.Authenticated) {
|
||||||
|
ProfileRepository.pullProfiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDismiss = { showPinSetup = false },
|
onDismiss = { showPinSetup = false },
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -60,35 +60,31 @@ object ProfileRepository {
|
||||||
|
|
||||||
val activeProfileId: Int get() = activeProfileIndex
|
val activeProfileId: Int get() = activeProfileIndex
|
||||||
|
|
||||||
|
fun loadCachedProfiles(): Boolean {
|
||||||
|
val stored = decodeStoredPayload() ?: return false
|
||||||
|
loadedCacheForUserId = stored.userId
|
||||||
|
applyStoredPayload(stored)
|
||||||
|
return _state.value.profiles.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
fun ensureLoaded(userId: String) {
|
fun ensureLoaded(userId: String) {
|
||||||
if (loadedCacheForUserId == userId && _state.value.isLoaded) return
|
if (loadedCacheForUserId == userId && _state.value.isLoaded) return
|
||||||
|
|
||||||
|
val stored = decodeStoredPayload()
|
||||||
loadedCacheForUserId = userId
|
loadedCacheForUserId = userId
|
||||||
val payload = ProfileStorage.loadPayload().orEmpty().trim()
|
if (stored == null) {
|
||||||
if (payload.isEmpty()) {
|
|
||||||
_state.value = ProfileState()
|
_state.value = ProfileState()
|
||||||
activeProfileIndex = 1
|
activeProfileIndex = 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val stored = runCatching {
|
|
||||||
json.decodeFromString<StoredProfilePayload>(payload)
|
|
||||||
}.getOrNull() ?: return
|
|
||||||
|
|
||||||
if (stored.userId != userId) {
|
if (stored.userId != userId) {
|
||||||
_state.value = ProfileState()
|
_state.value = ProfileState()
|
||||||
activeProfileIndex = 1
|
activeProfileIndex = 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val profiles = stored.profiles.sortedBy { it.profileIndex }
|
applyStoredPayload(stored)
|
||||||
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() {
|
fun clearInMemory() {
|
||||||
|
|
@ -346,6 +342,26 @@ object ProfileRepository {
|
||||||
persist()
|
persist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun decodeStoredPayload(): StoredProfilePayload? {
|
||||||
|
val payload = ProfileStorage.loadPayload().orEmpty().trim()
|
||||||
|
if (payload.isEmpty()) return null
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
json.decodeFromString<StoredProfilePayload>(payload)
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyStoredPayload(stored: StoredProfilePayload) {
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
private fun persist() {
|
private fun persist() {
|
||||||
val authState = AuthRepository.state.value as? AuthState.Authenticated ?: return
|
val authState = AuthRepository.state.value as? AuthState.Authenticated ?: return
|
||||||
ProfileStorage.savePayload(
|
ProfileStorage.savePayload(
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,8 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import com.nuvio.app.core.auth.AuthRepository
|
||||||
|
import com.nuvio.app.core.auth.AuthState
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
@ -67,6 +69,7 @@ fun ProfileSelectionScreen(
|
||||||
onAddProfile: () -> Unit,
|
onAddProfile: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val authState by AuthRepository.state.collectAsStateWithLifecycle()
|
||||||
val profileState by ProfileRepository.state.collectAsStateWithLifecycle()
|
val profileState by ProfileRepository.state.collectAsStateWithLifecycle()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var pinDialogProfile by remember { mutableStateOf<NuvioProfile?>(null) }
|
var pinDialogProfile by remember { mutableStateOf<NuvioProfile?>(null) }
|
||||||
|
|
@ -77,11 +80,16 @@ fun ProfileSelectionScreen(
|
||||||
val manageAlpha = remember { Animatable(0f) }
|
val manageAlpha = remember { Animatable(0f) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
ProfileRepository.pullProfiles()
|
|
||||||
AvatarRepository.fetchAvatars()
|
AvatarRepository.fetchAvatars()
|
||||||
AvatarRepository.refreshAvatars()
|
AvatarRepository.refreshAvatars()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(authState) {
|
||||||
|
if (authState is AuthState.Authenticated) {
|
||||||
|
ProfileRepository.pullProfiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
launch { titleAlpha.animateTo(1f, tween(600, easing = FastOutSlowInEasing)) }
|
launch { titleAlpha.animateTo(1f, tween(600, easing = FastOutSlowInEasing)) }
|
||||||
launch { titleOffset.animateTo(0f, tween(600, easing = FastOutSlowInEasing)) }
|
launch { titleOffset.animateTo(0f, tween(600, easing = FastOutSlowInEasing)) }
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ fun ProfileSwitcherTab(
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
AvatarRepository.fetchAvatars()
|
AvatarRepository.fetchAvatars()
|
||||||
|
AvatarRepository.refreshAvatars()
|
||||||
}
|
}
|
||||||
|
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ package com.nuvio.app.core.storage
|
||||||
import platform.Foundation.NSUserDefaults
|
import platform.Foundation.NSUserDefaults
|
||||||
|
|
||||||
internal actual object PlatformLocalAccountDataCleaner {
|
internal actual object PlatformLocalAccountDataCleaner {
|
||||||
private val plainKeys = listOf("profile_payload")
|
private val plainKeys = listOf(
|
||||||
|
"profile_payload",
|
||||||
|
"avatar_catalog_payload",
|
||||||
|
)
|
||||||
private val profileIndexedPrefixes = listOf(
|
private val profileIndexedPrefixes = listOf(
|
||||||
"installed_manifest_urls_",
|
"installed_manifest_urls_",
|
||||||
"plugins_state_",
|
"plugins_state_",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.nuvio.app.features.profiles
|
||||||
|
|
||||||
|
import platform.Foundation.NSUserDefaults
|
||||||
|
|
||||||
|
actual object AvatarStorage {
|
||||||
|
private const val payloadKey = "avatar_catalog_payload"
|
||||||
|
|
||||||
|
actual fun loadPayload(): String? =
|
||||||
|
NSUserDefaults.standardUserDefaults.stringForKey(payloadKey)
|
||||||
|
|
||||||
|
actual fun savePayload(payload: String) {
|
||||||
|
NSUserDefaults.standardUserDefaults.setObject(payload, forKey = payloadKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue