mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-06 19:38:58 +00:00
feat: implement HomeCatalogSettings sync with cloud
This commit is contained in:
parent
24363f0a22
commit
4f38d3713b
5 changed files with 221 additions and 2 deletions
|
|
@ -123,6 +123,7 @@ import com.nuvio.app.features.collection.CollectionManagementScreen
|
|||
import com.nuvio.app.features.collection.CollectionEditorScreen
|
||||
import com.nuvio.app.features.collection.CollectionEditorRepository
|
||||
import com.nuvio.app.features.collection.CollectionSyncService
|
||||
import com.nuvio.app.features.home.HomeCatalogSettingsSyncService
|
||||
import com.nuvio.app.features.collection.FolderDetailScreen
|
||||
import com.nuvio.app.features.collection.FolderDetailRepository
|
||||
import com.nuvio.app.features.streams.StreamContext
|
||||
|
|
@ -394,6 +395,9 @@ private fun MainAppContent(
|
|||
remember {
|
||||
CollectionSyncService.startObserving()
|
||||
}
|
||||
remember {
|
||||
HomeCatalogSettingsSyncService.startObserving()
|
||||
}
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var selectedTab by rememberSaveable { mutableStateOf(AppScreenTab.Home) }
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.nuvio.app.core.auth.AuthRepository
|
|||
import com.nuvio.app.core.auth.AuthState
|
||||
import com.nuvio.app.features.addons.AddonRepository
|
||||
import com.nuvio.app.features.collection.CollectionSyncService
|
||||
import com.nuvio.app.features.home.HomeCatalogSettingsSyncService
|
||||
import com.nuvio.app.features.plugins.PluginRepository
|
||||
import com.nuvio.app.features.library.LibraryRepository
|
||||
import com.nuvio.app.features.profiles.ProfileRepository
|
||||
|
|
@ -61,6 +62,10 @@ object SyncManager {
|
|||
runCatching { CollectionSyncService.pullFromServer(profileId) }
|
||||
.onFailure { log.e(it) { "Collections pull failed" } }
|
||||
}
|
||||
launch {
|
||||
runCatching { HomeCatalogSettingsSyncService.pullFromServer(profileId) }
|
||||
.onFailure { log.e(it) { "HomeCatalogSettings pull failed" } }
|
||||
}
|
||||
|
||||
log.i { "pullAllForProfile($profileId) — all pulls launched" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -378,6 +378,60 @@ object HomeCatalogSettingsRepository {
|
|||
HomeRepository.applyCurrentSettings()
|
||||
}
|
||||
|
||||
fun exportToSyncPayload(): SyncHomeCatalogPayload {
|
||||
ensureLoaded()
|
||||
val items = preferences.values.sortedBy { it.order }.map { pref ->
|
||||
val parts = pref.key.split(":")
|
||||
val isCollection = pref.key.startsWith("collection_")
|
||||
if (isCollection) {
|
||||
SyncCatalogItem(
|
||||
addonId = "",
|
||||
type = "",
|
||||
catalogId = "",
|
||||
enabled = pref.enabled,
|
||||
order = pref.order,
|
||||
customTitle = pref.customTitle,
|
||||
isCollection = true,
|
||||
collectionId = pref.key.removePrefix("collection_"),
|
||||
)
|
||||
} else {
|
||||
SyncCatalogItem(
|
||||
addonId = parts.getOrElse(0) { "" },
|
||||
type = parts.getOrElse(1) { "" },
|
||||
catalogId = parts.getOrElse(2) { "" },
|
||||
enabled = pref.enabled,
|
||||
order = pref.order,
|
||||
customTitle = pref.customTitle,
|
||||
isCollection = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
return SyncHomeCatalogPayload(items = items)
|
||||
}
|
||||
|
||||
fun applyFromRemote(payload: SyncHomeCatalogPayload) {
|
||||
ensureLoaded()
|
||||
val existingHeroState = preferences.mapValues { it.value.heroSourceEnabled }
|
||||
preferences = payload.items.associate { item ->
|
||||
val key = if (item.isCollection) {
|
||||
"collection_${item.collectionId}"
|
||||
} else {
|
||||
"${item.addonId}:${item.type}:${item.catalogId}"
|
||||
}
|
||||
key to StoredHomeCatalogPreference(
|
||||
key = key,
|
||||
customTitle = item.customTitle,
|
||||
enabled = item.enabled,
|
||||
heroSourceEnabled = existingHeroState[key] ?: true,
|
||||
order = item.order,
|
||||
)
|
||||
}.toMutableMap()
|
||||
hasLoaded = true
|
||||
publish()
|
||||
persist()
|
||||
HomeRepository.applyCurrentSettings()
|
||||
}
|
||||
|
||||
private fun allOrderedKeys(): List<String> {
|
||||
val catalogKeys = definitions.map { it.key }
|
||||
val collectionKeys = collectionDefinitions.map { it.key }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
package com.nuvio.app.features.home
|
||||
|
||||
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.features.profiles.ProfileRepository
|
||||
import io.github.jan.supabase.postgrest.postgrest
|
||||
import io.github.jan.supabase.postgrest.rpc
|
||||
import kotlin.concurrent.Volatile
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
@Serializable
|
||||
data class SyncCatalogItem(
|
||||
@SerialName("addon_id") val addonId: String,
|
||||
val type: String,
|
||||
@SerialName("catalog_id") val catalogId: String,
|
||||
val enabled: Boolean = true,
|
||||
val order: Int = 0,
|
||||
@SerialName("custom_title") val customTitle: String = "",
|
||||
@SerialName("is_collection") val isCollection: Boolean = false,
|
||||
@SerialName("collection_id") val collectionId: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SyncHomeCatalogPayload(
|
||||
val items: List<SyncCatalogItem> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class SupabaseHomeCatalogSettingsBlob(
|
||||
@SerialName("profile_id") val profileId: Int = 1,
|
||||
@SerialName("settings_json") val settingsJson: JsonObject = buildJsonObject { },
|
||||
@SerialName("updated_at") val updatedAt: String? = null,
|
||||
)
|
||||
|
||||
object HomeCatalogSettingsSyncService {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val log = Logger.withTag("HomeCatalogSettingsSyncService")
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
private const val PUSH_DEBOUNCE_MS = 1500L
|
||||
|
||||
@Volatile
|
||||
var isSyncingFromRemote: Boolean = false
|
||||
|
||||
private var pushJob: Job? = null
|
||||
private var observeJob: Job? = null
|
||||
|
||||
fun startObserving() {
|
||||
if (observeJob?.isActive == true) return
|
||||
observeLocalChangesAndPush()
|
||||
}
|
||||
|
||||
suspend fun pullFromServer(profileId: Int) {
|
||||
runCatching {
|
||||
val params = buildJsonObject {
|
||||
put("p_profile_id", profileId)
|
||||
}
|
||||
val result = SupabaseProvider.client.postgrest.rpc("sync_pull_home_catalog_settings", params)
|
||||
val blobs = result.decodeList<SupabaseHomeCatalogSettingsBlob>()
|
||||
val blob = blobs.firstOrNull()
|
||||
|
||||
if (blob == null) {
|
||||
log.i { "pullFromServer — no remote home catalog settings found" }
|
||||
return
|
||||
}
|
||||
|
||||
val remotePayload = runCatching {
|
||||
json.decodeFromJsonElement(SyncHomeCatalogPayload.serializer(), blob.settingsJson)
|
||||
}.getOrNull()
|
||||
|
||||
if (remotePayload == null) {
|
||||
log.w { "pullFromServer — failed to parse remote home catalog settings" }
|
||||
return
|
||||
}
|
||||
|
||||
if (remotePayload.items.isEmpty()) {
|
||||
log.i { "pullFromServer — remote has empty items, preserving local" }
|
||||
return
|
||||
}
|
||||
|
||||
isSyncingFromRemote = true
|
||||
HomeCatalogSettingsRepository.applyFromRemote(remotePayload)
|
||||
isSyncingFromRemote = false
|
||||
log.i { "pullFromServer — applied ${remotePayload.items.size} items from remote" }
|
||||
}.onFailure { e ->
|
||||
isSyncingFromRemote = false
|
||||
log.e(e) { "pullFromServer — FAILED" }
|
||||
}
|
||||
}
|
||||
|
||||
fun triggerPush() {
|
||||
pushJob?.cancel()
|
||||
pushJob = scope.launch {
|
||||
delay(500)
|
||||
if (isSyncingFromRemote) return@launch
|
||||
val authState = AuthRepository.state.value
|
||||
if (authState !is AuthState.Authenticated || authState.isAnonymous) return@launch
|
||||
pushToRemote()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun pushToRemote() {
|
||||
runCatching {
|
||||
val profileId = ProfileRepository.activeProfileId
|
||||
val payload = HomeCatalogSettingsRepository.exportToSyncPayload()
|
||||
val jsonElement = json.encodeToJsonElement(SyncHomeCatalogPayload.serializer(), payload)
|
||||
|
||||
val params = buildJsonObject {
|
||||
put("p_profile_id", profileId)
|
||||
put("p_settings_json", jsonElement)
|
||||
}
|
||||
SupabaseProvider.client.postgrest.rpc("sync_push_home_catalog_settings", params)
|
||||
log.d { "pushToRemote — success" }
|
||||
}.onFailure { e ->
|
||||
log.e(e) { "pushToRemote — FAILED" }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun observeLocalChangesAndPush() {
|
||||
observeJob = scope.launch {
|
||||
HomeCatalogSettingsRepository.uiState
|
||||
.map { it.signature }
|
||||
.drop(1)
|
||||
.distinctUntilChanged()
|
||||
.debounce(PUSH_DEBOUNCE_MS)
|
||||
.collect {
|
||||
if (isSyncingFromRemote) return@collect
|
||||
val authState = AuthRepository.state.value
|
||||
if (authState !is AuthState.Authenticated || authState.isAnonymous) return@collect
|
||||
pushToRemote()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
CURRENT_PROJECT_VERSION=12
|
||||
MARKETING_VERSION=0.1.0-alpha12
|
||||
CURRENT_PROJECT_VERSION=14
|
||||
MARKETING_VERSION=0.1.0-alpha14
|
||||
Loading…
Reference in a new issue