Wire ApplePluginsDataSource to a real Swift-driven state source

Adds ApplePluginsSnapshot and update() so Swift can push merged
repository/scraper/settings-sheet state back into the shared Compose
UI, plus FluxaApple.setPluginsActionHandler/updatePlugins following
the existing setSearchHandler/updateSearch pattern. Also fixes
saveScraperSettings to actually forward the settings values.
This commit is contained in:
KhooLy 2026-07-20 14:43:48 +03:00
parent 270d82d6cd
commit c35dd1148f
2 changed files with 168 additions and 1 deletions

View file

@ -16,7 +16,9 @@ import com.fluxa.app.shared.platform.AppleDetailSnapshot
import com.fluxa.app.shared.platform.AppleDetailStreamSnapshot
import com.fluxa.app.shared.platform.ApplePlaybackRequestSnapshot
import com.fluxa.app.shared.platform.AppleAddonStoreDataSource
import com.fluxa.app.shared.platform.ApplePluginsActionSnapshot
import com.fluxa.app.shared.platform.ApplePluginsDataSource
import com.fluxa.app.shared.platform.ApplePluginsSnapshot
import com.fluxa.app.shared.platform.AppleAuthDataSource
import com.fluxa.app.shared.platform.AppleCatalogHomeDataSource
import com.fluxa.app.shared.platform.AppleCalendarDataSource
@ -115,6 +117,14 @@ object FluxaApple {
libraryDataSource.update(snapshot)
}
fun setPluginsActionHandler(handler: (ApplePluginsActionSnapshot) -> Unit) {
pluginsDataSource.setOnActionRequested(handler)
}
fun updatePlugins(snapshot: ApplePluginsSnapshot) {
pluginsDataSource.update(snapshot)
}
fun parseAddonManifest(body: String): AppleAddonManifestSnapshot? = AppleStremioBridge.parseManifest(body)
fun normalizeAddonManifestUrl(rawUrl: String): String = AppleStremioBridge.normalizeManifestUrl(rawUrl)

View file

@ -1,10 +1,27 @@
package com.fluxa.app.shared.platform
import com.fluxa.app.shared.feature.plugins.PluginRepositoryUiModel
import com.fluxa.app.shared.feature.plugins.PluginScraperSettingsUiState
import com.fluxa.app.shared.feature.plugins.PluginScraperUiModel
import com.fluxa.app.shared.feature.plugins.PluginSettingsFieldUiModel
import com.fluxa.app.shared.feature.plugins.PluginSettingsOptionUiModel
import com.fluxa.app.shared.feature.plugins.PluginsDataSource
import com.fluxa.app.shared.feature.plugins.PluginsUiState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.longOrNull
data class ApplePluginsActionSnapshot(
val type: String,
@ -17,6 +34,121 @@ data class ApplePluginsActionSnapshot(
val internalName: String? = null
)
data class ApplePluginRepositorySnapshot(
val manifestUrl: String,
val name: String,
val description: String?,
val scraperCount: Int
)
data class ApplePluginSettingsOptionSnapshot(
val label: String,
val value: String
)
data class ApplePluginSettingsFieldSnapshot(
val key: String,
val type: String,
val label: String,
val description: String?,
val placeholder: String?,
val isPassword: Boolean,
val defaultValue: String?,
val defaultBoolean: Boolean,
val options: List<ApplePluginSettingsOptionSnapshot> = emptyList()
)
data class ApplePluginScraperSnapshot(
val id: String,
val name: String,
val repositoryUrl: String,
val enabled: Boolean,
val supportedTypes: List<String>,
val hasSettings: Boolean,
val settingsJson: String
)
data class ApplePluginScraperSettingsSnapshot(
val scraper: ApplePluginScraperSnapshot,
val loading: Boolean,
val fields: List<ApplePluginSettingsFieldSnapshot> = emptyList()
)
data class ApplePluginsSnapshot(
val repositories: List<ApplePluginRepositorySnapshot> = emptyList(),
val scrapers: List<ApplePluginScraperSnapshot> = emptyList(),
val addingRepositoryUrl: String? = null,
val repositoryError: String? = null,
val scraperSettingsSheet: ApplePluginScraperSettingsSnapshot? = null
)
private fun jsonElementToAny(element: JsonElement): Any? = when (element) {
is JsonNull -> null
is JsonArray -> element.map(::jsonElementToAny)
is JsonObject -> element.mapValues { jsonElementToAny(it.value) }
is JsonPrimitive -> when {
element.booleanOrNull != null -> element.boolean
element.longOrNull != null -> element.longOrNull
element.doubleOrNull != null -> element.doubleOrNull
else -> element.contentOrNull
}
}
private fun anyToJsonElement(value: Any?): JsonElement = when (value) {
null -> JsonNull
is String -> JsonPrimitive(value)
is Boolean -> JsonPrimitive(value)
is Number -> JsonPrimitive(value)
else -> JsonPrimitive(value.toString())
}
private fun parseSettingsJson(json: String): Map<String, Any?> =
runCatching { Json.parseToJsonElement(json).jsonObject.mapValues { jsonElementToAny(it.value) } }
.getOrDefault(emptyMap())
private fun encodeSettingsJson(values: Map<String, Any?>): String =
Json.encodeToString(JsonObject.serializer(), JsonObject(values.mapValues(::anyToJsonElement)))
private fun ApplePluginRepositorySnapshot.toUiModel(): PluginRepositoryUiModel = PluginRepositoryUiModel(
manifestUrl = manifestUrl,
name = name,
description = description,
scraperCount = scraperCount
)
private fun ApplePluginScraperSnapshot.toUiModel(): PluginScraperUiModel = PluginScraperUiModel(
id = id,
name = name,
repositoryUrl = repositoryUrl,
enabled = enabled,
supportedTypes = supportedTypes,
hasSettings = hasSettings,
settings = parseSettingsJson(settingsJson)
)
private fun ApplePluginSettingsOptionSnapshot.toUiModel(): PluginSettingsOptionUiModel = PluginSettingsOptionUiModel(
label = label,
value = value
)
private fun ApplePluginSettingsFieldSnapshot.toUiModel(): PluginSettingsFieldUiModel = PluginSettingsFieldUiModel(
key = key,
type = type,
label = label,
description = description,
placeholder = placeholder,
isPassword = isPassword,
defaultValue = defaultValue,
defaultBoolean = defaultBoolean,
options = options.map { it.toUiModel() }
)
private fun ApplePluginScraperSettingsSnapshot.toUiState(): PluginScraperSettingsUiState = PluginScraperSettingsUiState(
scraper = scraper.toUiModel(),
loading = loading,
fields = fields.map { it.toUiModel() }
)
class ApplePluginsDataSource : PluginsDataSource {
private val state = MutableStateFlow(PluginsUiState())
private var onActionRequested: (ApplePluginsActionSnapshot) -> Unit = {}
@ -44,6 +176,15 @@ class ApplePluginsDataSource : PluginsDataSource {
}
override suspend fun requestScraperSettings(scraperId: String) {
state.value.scrapers.find { it.id == scraperId }?.let { scraper ->
state.value = state.value.copy(
scraperSettingsSheet = PluginScraperSettingsUiState(
scraper = scraper,
loading = true,
fields = emptyList()
)
)
}
postAction(ApplePluginsActionSnapshot(type = "requestScraperSettings", scraperId = scraperId))
}
@ -52,7 +193,13 @@ class ApplePluginsDataSource : PluginsDataSource {
}
override suspend fun saveScraperSettings(scraperId: String, values: Map<String, Any?>) {
postAction(ApplePluginsActionSnapshot(type = "saveScraperSettings", scraperId = scraperId))
postAction(
ApplePluginsActionSnapshot(
type = "saveScraperSettings",
scraperId = scraperId,
settingsJson = encodeSettingsJson(values)
)
)
}
override suspend fun addCloudstreamRepository(url: String) {
@ -80,6 +227,16 @@ class ApplePluginsDataSource : PluginsDataSource {
onActionRequested = handler
}
fun update(snapshot: ApplePluginsSnapshot) {
state.value = state.value.copy(
repositories = snapshot.repositories.map { it.toUiModel() },
scrapers = snapshot.scrapers.map { it.toUiModel() },
addingRepositoryUrl = snapshot.addingRepositoryUrl,
repositoryError = snapshot.repositoryError,
scraperSettingsSheet = snapshot.scraperSettingsSheet?.toUiState()
)
}
private fun postAction(action: ApplePluginsActionSnapshot) {
onActionRequested(action)
}