mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
Merge 77b203b44e into 755f6ff9b8
This commit is contained in:
commit
1cf78fb86f
7 changed files with 258 additions and 52 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(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,27 @@ private data class AddonPushItem(
|
|||
@SerialName("sort_order") val sortOrder: Int = 0,
|
||||
)
|
||||
|
||||
internal data class AddonProfileOperation(
|
||||
val profileId: Int,
|
||||
val generation: Long,
|
||||
)
|
||||
|
||||
internal class AddonProfileOperationTracker(initialProfileId: Int = 1) {
|
||||
var profileId: Int = initialProfileId
|
||||
private set
|
||||
private var generation: Long = 0L
|
||||
|
||||
fun activate(profileId: Int) {
|
||||
generation += 1L
|
||||
this.profileId = profileId
|
||||
}
|
||||
|
||||
fun snapshot(): AddonProfileOperation = AddonProfileOperation(profileId, generation)
|
||||
|
||||
fun isCurrent(operation: AddonProfileOperation): Boolean =
|
||||
operation.profileId == profileId && operation.generation == generation
|
||||
}
|
||||
|
||||
object AddonRepository {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val log = Logger.withTag("AddonRepository")
|
||||
|
|
@ -53,18 +74,23 @@ object AddonRepository {
|
|||
|
||||
private var initialized = false
|
||||
private var pulledFromServer = false
|
||||
private var currentProfileId: Int = 1
|
||||
private val profileOperations = AddonProfileOperationTracker()
|
||||
private val currentProfileId: Int
|
||||
get() = profileOperations.profileId
|
||||
private val activeRefreshJobs = mutableMapOf<String, Job>()
|
||||
|
||||
fun initialize() {
|
||||
val effectiveProfileId = resolveEffectiveProfileId(ProfileRepository.activeProfileId)
|
||||
if (effectiveProfileId != currentProfileId) {
|
||||
activateProfile(effectiveProfileId)
|
||||
}
|
||||
if (initialized) return
|
||||
initialized = true
|
||||
currentProfileId = effectiveProfileId
|
||||
log.d { "initialize() — loading local addons for profile $currentProfileId" }
|
||||
|
||||
val storedUrls = dedupeManifestUrls(AddonStorage.loadInstalledAddonUrls(currentProfileId))
|
||||
val enabledByUrl = loadLocalEnabledStates()
|
||||
val enabledByUrl = loadLocalEnabledStates(currentProfileId)
|
||||
val namesByUrl = loadLocalNameOverrides(currentProfileId)
|
||||
log.d { "initialize() — local addon count: ${storedUrls.size}" }
|
||||
if (storedUrls.isEmpty()) return
|
||||
|
||||
|
|
@ -73,6 +99,7 @@ object AddonRepository {
|
|||
addons = storedUrls.map { manifestUrl ->
|
||||
existingByUrl[manifestUrl].toPendingAddon(
|
||||
manifestUrl = manifestUrl,
|
||||
userSetName = namesByUrl[manifestUrl],
|
||||
enabled = enabledByUrl[manifestUrl],
|
||||
)
|
||||
},
|
||||
|
|
@ -90,32 +117,29 @@ object AddonRepository {
|
|||
fun onProfileChanged(profileId: Int) {
|
||||
val effectiveProfileId = resolveEffectiveProfileId(profileId)
|
||||
if (effectiveProfileId == currentProfileId && initialized) return
|
||||
cancelActiveRefreshes()
|
||||
currentProfileId = effectiveProfileId
|
||||
initialized = false
|
||||
pulledFromServer = false
|
||||
_uiState.value = AddonsUiState()
|
||||
activateProfile(effectiveProfileId)
|
||||
}
|
||||
|
||||
fun clearLocalState() {
|
||||
cancelActiveRefreshes()
|
||||
currentProfileId = 1
|
||||
profileOperations.activate(1)
|
||||
initialized = false
|
||||
pulledFromServer = false
|
||||
_uiState.value = AddonsUiState()
|
||||
}
|
||||
|
||||
suspend fun pullFromServer(profileId: Int) {
|
||||
currentProfileId = resolveEffectiveProfileId(profileId)
|
||||
log.i { "pullFromServer() — profileId=$profileId, initialized=$initialized, pulledFromServer=$pulledFromServer" }
|
||||
val operation = beginProfileOperation(profileId) ?: return
|
||||
log.i { "pullFromServer() — profileId=${operation.profileId}, initialized=$initialized, pulledFromServer=$pulledFromServer" }
|
||||
runCatching {
|
||||
val rows = SupabaseProvider.client.postgrest
|
||||
.from("addons")
|
||||
.select {
|
||||
filter { eq("profile_id", currentProfileId) }
|
||||
filter { eq("profile_id", operation.profileId) }
|
||||
order("sort_order", Order.ASCENDING)
|
||||
}
|
||||
.decodeList<AddonRow>()
|
||||
if (!isCurrent(operation)) return
|
||||
|
||||
val rowsByUrl = linkedMapOf<String, AddonRow>()
|
||||
rows.forEach { row ->
|
||||
|
|
@ -130,19 +154,21 @@ object AddonRepository {
|
|||
urls.forEachIndexed { i, u -> log.d { " server[$i]: $u" } }
|
||||
|
||||
if (urls.isEmpty() && !pulledFromServer) {
|
||||
val localUrls = dedupeManifestUrls(AddonStorage.loadInstalledAddonUrls(currentProfileId))
|
||||
val localUrls = dedupeManifestUrls(AddonStorage.loadInstalledAddonUrls(operation.profileId))
|
||||
log.i { "pullFromServer() — server empty, local has ${localUrls.size} addons" }
|
||||
if (localUrls.isNotEmpty()) {
|
||||
log.i { "pullFromServer() — migrating local addons to server for profile $currentProfileId" }
|
||||
log.i { "pullFromServer() — migrating local addons to server for profile ${operation.profileId}" }
|
||||
initialize()
|
||||
pulledFromServer = true
|
||||
val enabledByUrl = loadLocalEnabledStates()
|
||||
if (!isCurrent(operation)) return
|
||||
val enabledByUrl = loadLocalEnabledStates(operation.profileId)
|
||||
val namesByUrl = loadLocalNameOverrides(operation.profileId)
|
||||
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,
|
||||
|
|
@ -150,31 +176,36 @@ object AddonRepository {
|
|||
)
|
||||
}
|
||||
val params = buildJsonObject {
|
||||
put("p_profile_id", currentProfileId)
|
||||
put("p_profile_id", operation.profileId)
|
||||
put("p_addons", json.encodeToJsonElement(addons))
|
||||
putSyncOriginClientId()
|
||||
}
|
||||
SupabaseProvider.client.postgrest.rpc("sync_push_addons", params)
|
||||
if (!isCurrent(operation)) return
|
||||
pulledFromServer = true
|
||||
log.i { "pullFromServer() — migration push done (${addons.size} addons)" }
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (urls.isEmpty()) {
|
||||
val localUrls = dedupeManifestUrls(AddonStorage.loadInstalledAddonUrls(currentProfileId))
|
||||
val localUrls = dedupeManifestUrls(AddonStorage.loadInstalledAddonUrls(operation.profileId))
|
||||
if (localUrls.isNotEmpty()) {
|
||||
log.w { "pullFromServer() — remote empty while local has ${localUrls.size} addons; preserving local addons" }
|
||||
val enabledByUrl = loadLocalEnabledStates()
|
||||
val enabledByUrl = loadLocalEnabledStates(operation.profileId)
|
||||
val namesByUrl = loadLocalNameOverrides(operation.profileId)
|
||||
val existingByUrl = _uiState.value.addons.associateBy(ManagedAddon::manifestUrl)
|
||||
if (!isCurrent(operation)) return
|
||||
_uiState.value = AddonsUiState(
|
||||
addons = localUrls.map { url ->
|
||||
existingByUrl[url].toPendingAddon(
|
||||
manifestUrl = url,
|
||||
userSetName = namesByUrl[url],
|
||||
enabled = enabledByUrl[url],
|
||||
)
|
||||
},
|
||||
)
|
||||
persist()
|
||||
persist(operation.profileId)
|
||||
localUrls.forEach { url ->
|
||||
val existing = existingByUrl[url]
|
||||
val addon = _uiState.value.addons.firstOrNull { it.manifestUrl == url }
|
||||
|
|
@ -189,17 +220,19 @@ object AddonRepository {
|
|||
}
|
||||
|
||||
val existingByUrl = _uiState.value.addons.associateBy(ManagedAddon::manifestUrl)
|
||||
if (!isCurrent(operation)) return
|
||||
_uiState.value = AddonsUiState(
|
||||
addons = urls.map { url ->
|
||||
val row = rowsByUrl[url]
|
||||
existingByUrl[url].toPendingAddon(
|
||||
manifestUrl = url,
|
||||
userSetName = row?.name?.takeIf { it.isNotBlank() },
|
||||
userSetName = row?.name?.trim()?.takeIf { it.isNotBlank() },
|
||||
replaceUserSetName = true,
|
||||
enabled = row?.enabled,
|
||||
)
|
||||
},
|
||||
)
|
||||
persist()
|
||||
persist(operation.profileId)
|
||||
urls.forEach { url ->
|
||||
val existing = existingByUrl[url]
|
||||
val addon = _uiState.value.addons.firstOrNull { it.manifestUrl == url }
|
||||
|
|
@ -328,6 +361,8 @@ object AddonRepository {
|
|||
}
|
||||
|
||||
fun refreshAddon(manifestUrl: String) {
|
||||
val operation = profileOperations.snapshot()
|
||||
if (!isCurrent(operation)) return
|
||||
val existingJob = activeRefreshJobs[manifestUrl]
|
||||
if (existingJob?.isActive == true) return
|
||||
|
||||
|
|
@ -343,6 +378,8 @@ object AddonRepository {
|
|||
)
|
||||
}
|
||||
|
||||
if (!isCurrent(operation)) return@launch
|
||||
|
||||
_uiState.update { current ->
|
||||
current.copy(
|
||||
addons = current.addons.map { addon ->
|
||||
|
|
@ -378,25 +415,24 @@ object AddonRepository {
|
|||
}
|
||||
|
||||
private fun pushToServer() {
|
||||
if (isUsingPrimaryAddonsFromSecondaryProfile()) return
|
||||
val operation = profileOperations.snapshot()
|
||||
val addons = _uiState.value.addons
|
||||
.distinctBy { it.manifestUrl }
|
||||
.mapIndexed { index, addon ->
|
||||
AddonPushItem(
|
||||
url = addon.manifestUrl,
|
||||
name = addon.userSetName?.takeIf { it.isNotBlank() } ?: addon.manifest?.name ?: "",
|
||||
enabled = addon.enabled,
|
||||
sortOrder = index,
|
||||
)
|
||||
}
|
||||
scope.launch {
|
||||
runCatching {
|
||||
if (isUsingPrimaryAddonsFromSecondaryProfile()) {
|
||||
return@runCatching
|
||||
}
|
||||
val profileId = currentProfileId
|
||||
val addons = _uiState.value.addons
|
||||
.distinctBy { it.manifestUrl }
|
||||
.mapIndexed { index, addon ->
|
||||
AddonPushItem(
|
||||
url = addon.manifestUrl,
|
||||
name = addon.userSetName?.takeIf { it.isNotBlank() } ?: addon.manifest?.name ?: "",
|
||||
enabled = addon.enabled,
|
||||
sortOrder = index,
|
||||
)
|
||||
}
|
||||
log.d { "pushToServer() — profileId=$profileId, pushing ${addons.size} addons" }
|
||||
if (!isCurrent(operation)) return@runCatching
|
||||
log.d { "pushToServer() — profileId=${operation.profileId}, pushing ${addons.size} addons" }
|
||||
val params = buildJsonObject {
|
||||
put("p_profile_id", profileId)
|
||||
put("p_profile_id", operation.profileId)
|
||||
put("p_addons", json.encodeToJsonElement(addons))
|
||||
putSyncOriginClientId()
|
||||
}
|
||||
|
|
@ -425,20 +461,28 @@ object AddonRepository {
|
|||
}
|
||||
}
|
||||
|
||||
private fun persist() {
|
||||
private fun persist(profileId: Int = currentProfileId) {
|
||||
val addons = _uiState.value.addons
|
||||
AddonStorage.saveInstalledAddonUrls(
|
||||
currentProfileId,
|
||||
profileId,
|
||||
dedupeManifestUrls(addons.map { it.manifestUrl }),
|
||||
)
|
||||
AddonStorage.saveAddonEnabledStates(
|
||||
currentProfileId,
|
||||
profileId,
|
||||
addons.associate { it.manifestUrl to it.enabled },
|
||||
)
|
||||
AddonStorage.saveAddonNameOverridesPayload(
|
||||
profileId,
|
||||
addons.encodeNameOverrides(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadLocalEnabledStates(): Map<String, Boolean> =
|
||||
AddonStorage.loadAddonEnabledStates(currentProfileId)
|
||||
private fun loadLocalEnabledStates(profileId: Int): Map<String, Boolean> =
|
||||
AddonStorage.loadAddonEnabledStates(profileId)
|
||||
.mapKeys { (url, _) -> ensureManifestSuffix(url) }
|
||||
|
||||
private fun loadLocalNameOverrides(profileId: Int): Map<String, String> =
|
||||
decodeAddonNameOverrides(AddonStorage.loadAddonNameOverridesPayload(profileId))
|
||||
.mapKeys { (url, _) -> ensureManifestSuffix(url) }
|
||||
|
||||
private fun cancelActiveRefreshes() {
|
||||
|
|
@ -446,6 +490,30 @@ object AddonRepository {
|
|||
activeRefreshJobs.clear()
|
||||
}
|
||||
|
||||
private fun activateProfile(profileId: Int) {
|
||||
cancelActiveRefreshes()
|
||||
profileOperations.activate(profileId)
|
||||
initialized = false
|
||||
pulledFromServer = false
|
||||
_uiState.value = AddonsUiState()
|
||||
}
|
||||
|
||||
private fun beginProfileOperation(profileId: Int): AddonProfileOperation? {
|
||||
val requestedProfileId = resolveEffectiveProfileId(profileId)
|
||||
val activeProfileId = resolveEffectiveProfileId(ProfileRepository.activeProfileId)
|
||||
if (requestedProfileId != activeProfileId) {
|
||||
log.w { "Ignoring stale addon pull for profile $requestedProfileId; active profile is $activeProfileId" }
|
||||
return null
|
||||
}
|
||||
if (requestedProfileId != currentProfileId) {
|
||||
activateProfile(requestedProfileId)
|
||||
}
|
||||
return profileOperations.snapshot()
|
||||
}
|
||||
|
||||
private fun isCurrent(operation: AddonProfileOperation): Boolean =
|
||||
profileOperations.isCurrent(operation)
|
||||
|
||||
private fun resolveEffectiveProfileId(profileId: Int): Int {
|
||||
val active = ProfileRepository.state.value.activeProfile
|
||||
return if (active != null && active.profileIndex != 1 && active.usesPrimaryAddons) 1 else profileId
|
||||
|
|
@ -457,37 +525,40 @@ object AddonRepository {
|
|||
}
|
||||
}
|
||||
|
||||
private fun ManagedAddon?.toPendingAddon(
|
||||
internal fun ManagedAddon?.toPendingAddon(
|
||||
manifestUrl: String,
|
||||
userSetName: String? = null,
|
||||
replaceUserSetName: Boolean = false,
|
||||
enabled: Boolean? = null,
|
||||
): ManagedAddon =
|
||||
when {
|
||||
): ManagedAddon {
|
||||
val resolvedUserSetName = if (replaceUserSetName) userSetName else userSetName ?: this?.userSetName
|
||||
return when {
|
||||
this == null -> ManagedAddon(
|
||||
manifestUrl = manifestUrl,
|
||||
isRefreshing = enabled ?: true,
|
||||
userSetName = userSetName,
|
||||
userSetName = resolvedUserSetName,
|
||||
enabled = enabled ?: true,
|
||||
)
|
||||
manifest != null -> copy(
|
||||
manifestUrl = manifestUrl,
|
||||
isRefreshing = false,
|
||||
userSetName = userSetName ?: this.userSetName,
|
||||
userSetName = resolvedUserSetName,
|
||||
enabled = enabled ?: this.enabled,
|
||||
)
|
||||
isRefreshing -> copy(
|
||||
manifestUrl = manifestUrl,
|
||||
userSetName = userSetName ?: this.userSetName,
|
||||
userSetName = resolvedUserSetName,
|
||||
enabled = enabled ?: this.enabled,
|
||||
)
|
||||
else -> copy(
|
||||
manifestUrl = manifestUrl,
|
||||
isRefreshing = enabled ?: this.enabled,
|
||||
errorMessage = null,
|
||||
userSetName = userSetName ?: this.userSetName,
|
||||
userSetName = resolvedUserSetName,
|
||||
enabled = enabled ?: this.enabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dedupeManifestUrls(urls: List<String>): List<String> =
|
||||
urls.map(::ensureManifestSuffix).distinct()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.nuvio.app.features.addons
|
|||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class AddonModelsTest {
|
||||
|
|
@ -36,6 +37,75 @@ 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 `blank server name clears a cached override`() {
|
||||
val addon = ManagedAddon(
|
||||
manifestUrl = "https://example.test/manifest.json",
|
||||
manifest = manifest(),
|
||||
userSetName = "Stale cached name",
|
||||
).toPendingAddon(
|
||||
manifestUrl = "https://example.test/manifest.json",
|
||||
userSetName = null,
|
||||
replaceUserSetName = true,
|
||||
enabled = true,
|
||||
)
|
||||
|
||||
assertNull(addon.userSetName)
|
||||
assertEquals("addon", addon.displayTitle)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `profile operation becomes stale when profile changes`() {
|
||||
val tracker = AddonProfileOperationTracker(initialProfileId = 1)
|
||||
val profileOnePull = tracker.snapshot()
|
||||
|
||||
tracker.activate(profileId = 2)
|
||||
|
||||
assertFalse(tracker.isCurrent(profileOnePull))
|
||||
assertTrue(tracker.isCurrent(tracker.snapshot()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `new generation invalidates work even for the same effective profile`() {
|
||||
val tracker = AddonProfileOperationTracker(initialProfileId = 1)
|
||||
val oldPull = tracker.snapshot()
|
||||
|
||||
tracker.activate(profileId = 1)
|
||||
|
||||
assertFalse(tracker.isCurrent(oldPull))
|
||||
}
|
||||
|
||||
@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