diff --git a/shared/src/iosMain/kotlin/com/fluxa/app/shared/FluxaApple.kt b/shared/src/iosMain/kotlin/com/fluxa/app/shared/FluxaApple.kt index d57c129..5876e87 100644 --- a/shared/src/iosMain/kotlin/com/fluxa/app/shared/FluxaApple.kt +++ b/shared/src/iosMain/kotlin/com/fluxa/app/shared/FluxaApple.kt @@ -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) diff --git a/shared/src/iosMain/kotlin/com/fluxa/app/shared/platform/ApplePluginsDataSource.kt b/shared/src/iosMain/kotlin/com/fluxa/app/shared/platform/ApplePluginsDataSource.kt index dd8fe61..4e50725 100644 --- a/shared/src/iosMain/kotlin/com/fluxa/app/shared/platform/ApplePluginsDataSource.kt +++ b/shared/src/iosMain/kotlin/com/fluxa/app/shared/platform/ApplePluginsDataSource.kt @@ -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 = emptyList() +) + +data class ApplePluginScraperSnapshot( + val id: String, + val name: String, + val repositoryUrl: String, + val enabled: Boolean, + val supportedTypes: List, + val hasSettings: Boolean, + val settingsJson: String +) + +data class ApplePluginScraperSettingsSnapshot( + val scraper: ApplePluginScraperSnapshot, + val loading: Boolean, + val fields: List = emptyList() +) + +data class ApplePluginsSnapshot( + val repositories: List = emptyList(), + val scrapers: List = 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 = + runCatching { Json.parseToJsonElement(json).jsonObject.mapValues { jsonElementToAny(it.value) } } + .getOrDefault(emptyMap()) + +private fun encodeSettingsJson(values: Map): 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) { - 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) }