mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-17 04:48:37 +00:00
fix: persist server addon names across restarts
This commit is contained in:
parent
0ac38dce43
commit
8c7a0b624c
7 changed files with 113 additions and 3 deletions
|
|
@ -26,6 +26,7 @@ actual object AddonStorage {
|
|||
private const val preferencesName = "nuvio_addons"
|
||||
private const val addonUrlsKey = "installed_manifest_urls"
|
||||
private const val addonEnabledStatesKey = "installed_manifest_enabled_states"
|
||||
private const val addonNameOverridesKey = "installed_manifest_name_overrides"
|
||||
|
||||
private var preferences: SharedPreferences? = null
|
||||
|
||||
|
|
@ -66,6 +67,18 @@ actual object AddonStorage {
|
|||
?.putString("${addonEnabledStatesKey}_$profileId", payload)
|
||||
?.apply()
|
||||
}
|
||||
|
||||
actual fun loadAddonNameOverridesPayload(profileId: Int): String =
|
||||
preferences
|
||||
?.getString("${addonNameOverridesKey}_$profileId", null)
|
||||
.orEmpty()
|
||||
|
||||
actual fun saveAddonNameOverridesPayload(profileId: Int, payload: String) {
|
||||
preferences
|
||||
?.edit()
|
||||
?.putString("${addonNameOverridesKey}_$profileId", payload)
|
||||
?.apply()
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseEnabledStateLine(line: String): Pair<String, Boolean>? {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,24 @@
|
|||
package com.nuvio.app.features.addons
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import nuvio.composeapp.generated.resources.Res
|
||||
import nuvio.composeapp.generated.resources.generic_addon
|
||||
import org.jetbrains.compose.resources.getString
|
||||
|
||||
@Serializable
|
||||
private data class StoredAddonNameOverride(
|
||||
val url: String,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
private val addonNameOverridesJson = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
data class AddonManifest(
|
||||
val id: String,
|
||||
val name: String,
|
||||
|
|
@ -85,6 +99,28 @@ internal fun List<ManagedAddon>.toOverview(): AddonOverview =
|
|||
internal fun List<ManagedAddon>.enabledAddons(): List<ManagedAddon> =
|
||||
filter { it.enabled }
|
||||
|
||||
internal fun List<ManagedAddon>.encodeNameOverrides(): String =
|
||||
addonNameOverridesJson.encodeToString(
|
||||
mapNotNull { addon ->
|
||||
addon.userSetName
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { name -> StoredAddonNameOverride(url = addon.manifestUrl, name = name) }
|
||||
},
|
||||
)
|
||||
|
||||
internal fun decodeAddonNameOverrides(payload: String): Map<String, String> {
|
||||
if (payload.isBlank()) return emptyMap()
|
||||
return runCatching {
|
||||
addonNameOverridesJson.decodeFromString<List<StoredAddonNameOverride>>(payload)
|
||||
}.getOrDefault(emptyList())
|
||||
.mapNotNull { override ->
|
||||
val url = override.url.trim().takeIf { it.isNotEmpty() } ?: return@mapNotNull null
|
||||
val name = override.name.takeIf { it.isNotBlank() } ?: return@mapNotNull null
|
||||
url to name
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
sealed interface AddAddonResult {
|
||||
data class Success(val manifest: AddonManifest) : AddAddonResult
|
||||
data class Error(val message: String) : AddAddonResult
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ internal expect object AddonStorage {
|
|||
fun saveInstalledAddonUrls(profileId: Int, urls: List<String>)
|
||||
fun loadAddonEnabledStates(profileId: Int): Map<String, Boolean>
|
||||
fun saveAddonEnabledStates(profileId: Int, states: Map<String, Boolean>)
|
||||
fun loadAddonNameOverridesPayload(profileId: Int): String
|
||||
fun saveAddonNameOverridesPayload(profileId: Int, payload: String)
|
||||
}
|
||||
|
||||
data class RawHttpResponse(
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ object AddonRepository {
|
|||
|
||||
val storedUrls = dedupeManifestUrls(AddonStorage.loadInstalledAddonUrls(currentProfileId))
|
||||
val enabledByUrl = loadLocalEnabledStates()
|
||||
val namesByUrl = loadLocalNameOverrides()
|
||||
log.d { "initialize() — local addon count: ${storedUrls.size}" }
|
||||
if (storedUrls.isEmpty()) return
|
||||
|
||||
|
|
@ -73,6 +74,7 @@ object AddonRepository {
|
|||
addons = storedUrls.map { manifestUrl ->
|
||||
existingByUrl[manifestUrl].toPendingAddon(
|
||||
manifestUrl = manifestUrl,
|
||||
userSetName = namesByUrl[manifestUrl],
|
||||
enabled = enabledByUrl[manifestUrl],
|
||||
)
|
||||
},
|
||||
|
|
@ -137,12 +139,14 @@ object AddonRepository {
|
|||
initialize()
|
||||
pulledFromServer = true
|
||||
val enabledByUrl = loadLocalEnabledStates()
|
||||
val namesByUrl = loadLocalNameOverrides()
|
||||
val addons = localUrls.mapIndexed { index, addonUrl ->
|
||||
val manifestUrl = ensureManifestSuffix(addonUrl)
|
||||
AddonPushItem(
|
||||
url = manifestUrl,
|
||||
name = _uiState.value.addons
|
||||
.find { it.manifestUrl == manifestUrl }?.manifest?.name ?: "",
|
||||
name = namesByUrl[manifestUrl]
|
||||
?: _uiState.value.addons.find { it.manifestUrl == manifestUrl }?.manifest?.name
|
||||
?: "",
|
||||
enabled = enabledByUrl[manifestUrl]
|
||||
?: _uiState.value.addons.find { it.manifestUrl == manifestUrl }?.enabled
|
||||
?: true,
|
||||
|
|
@ -165,11 +169,13 @@ object AddonRepository {
|
|||
if (localUrls.isNotEmpty()) {
|
||||
log.w { "pullFromServer() — remote empty while local has ${localUrls.size} addons; preserving local addons" }
|
||||
val enabledByUrl = loadLocalEnabledStates()
|
||||
val namesByUrl = loadLocalNameOverrides()
|
||||
val existingByUrl = _uiState.value.addons.associateBy(ManagedAddon::manifestUrl)
|
||||
_uiState.value = AddonsUiState(
|
||||
addons = localUrls.map { url ->
|
||||
existingByUrl[url].toPendingAddon(
|
||||
manifestUrl = url,
|
||||
userSetName = namesByUrl[url],
|
||||
enabled = enabledByUrl[url],
|
||||
)
|
||||
},
|
||||
|
|
@ -435,12 +441,20 @@ object AddonRepository {
|
|||
currentProfileId,
|
||||
addons.associate { it.manifestUrl to it.enabled },
|
||||
)
|
||||
AddonStorage.saveAddonNameOverridesPayload(
|
||||
currentProfileId,
|
||||
addons.encodeNameOverrides(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadLocalEnabledStates(): Map<String, Boolean> =
|
||||
AddonStorage.loadAddonEnabledStates(currentProfileId)
|
||||
.mapKeys { (url, _) -> ensureManifestSuffix(url) }
|
||||
|
||||
private fun loadLocalNameOverrides(): Map<String, String> =
|
||||
decodeAddonNameOverrides(AddonStorage.loadAddonNameOverridesPayload(currentProfileId))
|
||||
.mapKeys { (url, _) -> ensureManifestSuffix(url) }
|
||||
|
||||
private fun cancelActiveRefreshes() {
|
||||
activeRefreshJobs.values.forEach(Job::cancel)
|
||||
activeRefreshJobs.clear()
|
||||
|
|
@ -457,7 +471,7 @@ object AddonRepository {
|
|||
}
|
||||
}
|
||||
|
||||
private fun ManagedAddon?.toPendingAddon(
|
||||
internal fun ManagedAddon?.toPendingAddon(
|
||||
manifestUrl: String,
|
||||
userSetName: String? = null,
|
||||
enabled: Boolean? = null,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,37 @@ class AddonModelsTest {
|
|||
assertEquals(listOf(enabled), listOf(enabled, disabled).enabledAddons())
|
||||
assertTrue(enabled.isActive)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `addon name overrides survive local storage round trip`() {
|
||||
val manifestUrl = "https://example.test/configured/user\tvalue/manifest.json"
|
||||
val customName = "My renamed addon\nwith a second line"
|
||||
val payload = listOf(
|
||||
ManagedAddon(
|
||||
manifestUrl = manifestUrl,
|
||||
userSetName = customName,
|
||||
),
|
||||
).encodeNameOverrides()
|
||||
|
||||
assertEquals(mapOf(manifestUrl to customName), decodeAddonNameOverrides(payload))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pending addon restores its locally stored server name`() {
|
||||
val addon = null.toPendingAddon(
|
||||
manifestUrl = "https://example.test/manifest.json",
|
||||
userSetName = "Renamed on the website",
|
||||
enabled = true,
|
||||
)
|
||||
|
||||
assertEquals("Renamed on the website", addon.userSetName)
|
||||
assertEquals("Renamed on the website", addon.displayTitle)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `malformed addon name cache is ignored`() {
|
||||
assertTrue(decodeAddonNameOverrides("not-json").isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
private fun manifest(id: String = "addon") = AddonManifest(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ internal actual object PlatformLocalAccountDataCleaner {
|
|||
private val profilePinCachePrefixes = listOf("profile_pin_cache_")
|
||||
private val profileIndexedPrefixes = listOf(
|
||||
"installed_manifest_urls_",
|
||||
"installed_manifest_name_overrides_",
|
||||
"plugins_state_",
|
||||
"library_payload_",
|
||||
"watched_payload_",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import platform.Foundation.NSUserDefaults
|
|||
actual object AddonStorage {
|
||||
private const val addonUrlsKey = "installed_manifest_urls"
|
||||
private const val addonEnabledStatesKey = "installed_manifest_enabled_states"
|
||||
private const val addonNameOverridesKey = "installed_manifest_name_overrides"
|
||||
|
||||
actual fun loadInstalledAddonUrls(profileId: Int): List<String> =
|
||||
NSUserDefaults.standardUserDefaults
|
||||
|
|
@ -59,6 +60,18 @@ actual object AddonStorage {
|
|||
forKey = "${addonEnabledStatesKey}_$profileId",
|
||||
)
|
||||
}
|
||||
|
||||
actual fun loadAddonNameOverridesPayload(profileId: Int): String =
|
||||
NSUserDefaults.standardUserDefaults
|
||||
.stringForKey("${addonNameOverridesKey}_$profileId")
|
||||
.orEmpty()
|
||||
|
||||
actual fun saveAddonNameOverridesPayload(profileId: Int, payload: String) {
|
||||
NSUserDefaults.standardUserDefaults.setObject(
|
||||
payload,
|
||||
forKey = "${addonNameOverridesKey}_$profileId",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseEnabledStateLine(line: String): Pair<String, Boolean>? {
|
||||
|
|
|
|||
Loading…
Reference in a new issue