From 1e776bbb8a338d5232a429ff950738698117c4ee Mon Sep 17 00:00:00 2001 From: KhooLy <73142442+KhooLy@users.noreply.github.com> Date: Mon, 20 Jul 2026 02:54:53 +0300 Subject: [PATCH] Add per-scraper settings UI backed by onSettings() JS export Wire executePluginScraper's updated scraper_id/scraper_settings_json params through FluxaCoreUniFfi and PluginRepositoryManager, add getSettingsLayout() to fetch and parse a scraper's onSettings() field layout into small UI models, and add a gear-icon-triggered bottom sheet (PluginScraperSettingsSheet) rendering header/info/text/select/ toggle fields, matching Nuvio's real settings schema. --- .../app/plugins/PluginRepositoryManager.kt | 76 +++++- .../ui/settings/PluginScraperSettingsSheet.kt | 254 ++++++++++++++++++ .../app/ui/settings/PluginsSettingsScreen.kt | 50 +++- .../ui/settings/PluginsSettingsViewModel.kt | 33 +++ .../commonMain/resources/i18n/english_us.json | 6 + core/src/commonMain/resources/i18n/tr_tr.json | 6 + .../fluxa/app/core/rust/FluxaCoreUniFfi.kt | 17 +- 7 files changed, 438 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/fluxa/app/ui/settings/PluginScraperSettingsSheet.kt diff --git a/app/src/main/java/com/fluxa/app/plugins/PluginRepositoryManager.kt b/app/src/main/java/com/fluxa/app/plugins/PluginRepositoryManager.kt index f189e9b..2d6cc97 100644 --- a/app/src/main/java/com/fluxa/app/plugins/PluginRepositoryManager.kt +++ b/app/src/main/java/com/fluxa/app/plugins/PluginRepositoryManager.kt @@ -54,6 +54,23 @@ data class PluginsUiState( val error: String? = null ) +data class PluginSettingsOptionUiModel( + val label: String, + val value: String +) + +data class PluginSettingsFieldUiModel( + val key: String, + val type: String, + val label: String, + val description: String? = null, + val placeholder: String? = null, + val isPassword: Boolean = false, + val defaultValue: String? = null, + val defaultBoolean: Boolean = false, + val options: List = emptyList() +) + private data class PersistedScraperOverride( val enabled: Boolean = true, val settings: Map = emptyMap() @@ -156,7 +173,16 @@ class PluginRepositoryManager @Inject constructor( ): List = withContext(Dispatchers.IO) { val code = fetchScraperCode(scraper) ?: return@withContext emptyList() try { - val rawJson = FluxaCoreUniFfi.executePluginScraper(pluginHttpClient, code, tmdbId, mediaType, season, episode) + val rawJson = FluxaCoreUniFfi.executePluginScraper( + pluginHttpClient, + code, + scraper.id, + gson.toJson(scraper.settings), + tmdbId, + mediaType, + season, + episode + ) val normalized = FluxaCoreUniFfi.coreInvokeValue("pluginStreamResultsToStreams", rawJson) val streams: List = gson.fromJson(normalized, streamListType) ?: emptyList() streams.map { it.copy(addonName = scraper.name) } @@ -166,6 +192,54 @@ class PluginRepositoryManager @Inject constructor( } } + suspend fun getSettingsLayout(scraper: PluginScraperUiModel): List = + withContext(Dispatchers.IO) { + val code = fetchScraperCode(scraper) ?: return@withContext emptyList() + try { + val layoutJson = FluxaCoreUniFfi.getPluginScraperSettingsLayout(code, scraper.id) + parseSettingsLayout(layoutJson) + } catch (e: Exception) { + Log.w("PluginRepositoryManager", "settings layout for ${scraper.id} failed", e) + emptyList() + } + } + + private fun parseSettingsLayout(layoutJson: String): List { + val elements = try { + com.google.gson.JsonParser.parseString(layoutJson).asJsonArray + } catch (e: Exception) { + return emptyList() + } + return elements.mapNotNull { element -> + val field = element.asJsonObject + val type = field.get("type")?.asString ?: return@mapNotNull null + val key = field.get("key")?.asString ?: "" + val label = field.get("label")?.asString ?: "" + val optionsArray = field.get("options")?.takeIf { it.isJsonArray }?.asJsonArray + val options = optionsArray?.map { option -> + val optionObject = option.asJsonObject + PluginSettingsOptionUiModel( + label = optionObject.get("label")?.asString ?: "", + value = optionObject.get("value")?.asString ?: "" + ) + } ?: emptyList() + val defaultValueElement = field.get("defaultValue") + val defaultIsBoolean = defaultValueElement?.isJsonPrimitive == true && + defaultValueElement.asJsonPrimitive.isBoolean + PluginSettingsFieldUiModel( + key = key, + type = type, + label = label, + description = field.get("description")?.asString, + placeholder = field.get("placeholder")?.asString, + isPassword = field.get("isPassword")?.asBoolean ?: false, + defaultValue = if (!defaultIsBoolean) defaultValueElement?.takeIf { !it.isJsonNull }?.asString else null, + defaultBoolean = if (defaultIsBoolean) defaultValueElement.asBoolean else false, + options = options + ) + } + } + private suspend fun fetchScraperCode(scraper: PluginScraperUiModel): String? = codeCacheMutex.withLock { codeCache[scraper.id]?.let { return@withLock it } val url = resolveScraperUrl(scraper.repositoryUrl, scraper.filename) diff --git a/app/src/main/java/com/fluxa/app/ui/settings/PluginScraperSettingsSheet.kt b/app/src/main/java/com/fluxa/app/ui/settings/PluginScraperSettingsSheet.kt new file mode 100644 index 0000000..6ab888a --- /dev/null +++ b/app/src/main/java/com/fluxa/app/ui/settings/PluginScraperSettingsSheet.kt @@ -0,0 +1,254 @@ +package com.fluxa.app.ui.settings + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowDropDown +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.fluxa.app.common.AppStrings +import com.fluxa.app.plugins.PluginSettingsFieldUiModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PluginScraperSettingsSheet( + scraperName: String, + language: String?, + loading: Boolean, + fields: List, + initialValues: Map, + onDismiss: () -> Unit, + onSave: (Map) -> Unit +) { + val values = remember(fields, initialValues) { + val map = mutableStateMapOf() + fields.forEach { field -> + if (field.key.isEmpty()) return@forEach + map[field.key] = initialValues[field.key] ?: when (field.type) { + "toggle" -> field.defaultBoolean + else -> field.defaultValue + } + } + map + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + containerColor = Color(0xFF111318), + contentColor = Color.White + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 22.dp, vertical = 10.dp) + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + AppStrings.format(language, "settings.plugins.settings_title", scraperName), + color = Color.White, + fontSize = 20.sp, + fontWeight = FontWeight.Black + ) + + when { + loading -> { + Box(modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp), contentAlignment = Alignment.Center) { + CircularProgressIndicator(color = Color.White, strokeWidth = 2.dp, modifier = Modifier.size(24.dp)) + } + } + fields.isEmpty() -> { + Text( + AppStrings.t(language, "settings.plugins.settings_empty"), + color = Color.White.copy(alpha = 0.5f), + modifier = Modifier.padding(vertical = 12.dp) + ) + } + else -> { + fields.forEach { field -> + PluginSettingsField( + field = field, + language = language, + value = values[field.key], + onValueChange = { values[field.key] = it } + ) + } + } + } + + if (!loading && fields.isNotEmpty()) { + Button( + onClick = { onSave(values.toMap()) }, + modifier = Modifier.fillMaxWidth().height(52.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color.White, contentColor = Color.Black), + shape = RoundedCornerShape(12.dp) + ) { + Text(AppStrings.t(language, "settings.plugins.settings_save"), fontWeight = FontWeight.Black) + } + } + } + } +} + +@Composable +private fun PluginSettingsField( + field: PluginSettingsFieldUiModel, + language: String?, + value: Any?, + onValueChange: (Any?) -> Unit +) { + when (field.type) { + "header" -> { + Text( + field.label, + color = Color.White.copy(alpha = 0.6f), + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(top = 8.dp) + ) + } + "info" -> { + Text( + field.label, + color = Color.White.copy(alpha = 0.5f), + fontSize = 13.sp + ) + } + "text" -> { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text(field.label, color = Color.White, fontSize = 13.sp, fontWeight = FontWeight.SemiBold) + TextField( + value = value as? String ?: "", + onValueChange = onValueChange, + placeholder = { field.placeholder?.let { Text(it, color = Color.White.copy(alpha = 0.35f)) } }, + singleLine = true, + visualTransformation = if (field.isPassword) PasswordVisualTransformation() else VisualTransformation.None, + keyboardOptions = KeyboardOptions(keyboardType = if (field.isPassword) KeyboardType.Password else KeyboardType.Text), + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.White.copy(alpha = 0.05f), + unfocusedContainerColor = Color.White.copy(alpha = 0.05f), + focusedTextColor = Color.White, + unfocusedTextColor = Color.White + ) + ) + field.description?.let { + Text(it, color = Color.White.copy(alpha = 0.4f), fontSize = 11.sp) + } + } + } + "select" -> { + var expanded by remember { mutableStateOf(false) } + val currentValue = value as? String ?: field.defaultValue ?: "" + val selectedLabel = field.options.find { it.value == currentValue }?.label + ?: currentValue.ifBlank { AppStrings.t(language, "settings.plugins.select_placeholder") } + + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text(field.label, color = Color.White, fontSize = 13.sp, fontWeight = FontWeight.SemiBold) + Box { + OutlinedButton( + onClick = { expanded = true }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + contentPadding = PaddingValues(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(selectedLabel, color = Color.White) + Icon(Icons.Rounded.ArrowDropDown, contentDescription = null, tint = Color.White.copy(alpha = 0.6f)) + } + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.fillMaxWidth(0.85f) + ) { + field.options.forEach { option -> + DropdownMenuItem( + text = { Text(option.label) }, + onClick = { + onValueChange(option.value) + expanded = false + } + ) + } + } + } + field.description?.let { + Text(it, color = Color.White.copy(alpha = 0.4f), fontSize = 11.sp) + } + } + } + "toggle" -> { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text(field.label, color = Color.White, fontSize = 14.sp) + field.description?.let { + Text(it, color = Color.White.copy(alpha = 0.4f), fontSize = 11.sp) + } + } + Switch( + checked = value as? Boolean ?: field.defaultBoolean, + onCheckedChange = onValueChange, + colors = SwitchDefaults.colors( + checkedTrackColor = Color.White, + checkedThumbColor = Color.Black, + uncheckedTrackColor = Color.White.copy(alpha = 0.15f), + uncheckedThumbColor = Color.White.copy(alpha = 0.7f) + ) + ) + } + } + } +} diff --git a/app/src/main/java/com/fluxa/app/ui/settings/PluginsSettingsScreen.kt b/app/src/main/java/com/fluxa/app/ui/settings/PluginsSettingsScreen.kt index 5d5e4c0..06457fa 100644 --- a/app/src/main/java/com/fluxa/app/ui/settings/PluginsSettingsScreen.kt +++ b/app/src/main/java/com/fluxa/app/ui/settings/PluginsSettingsScreen.kt @@ -38,6 +38,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Settings +import androidx.compose.material3.Icon import com.fluxa.app.common.AppStrings import com.fluxa.app.plugins.PluginRepositoryUiModel import com.fluxa.app.plugins.PluginScraperUiModel @@ -57,8 +60,22 @@ fun PluginsSettingsRoute( onAddRepository = viewModel::addRepository, onRemoveRepository = viewModel::removeRepository, onToggleScraper = viewModel::toggleScraper, + onOpenScraperSettings = viewModel::openScraperSettings, onBackRequested = onBackRequested ) + + val settingsSheet = viewModel.settingsSheet + if (settingsSheet != null) { + PluginScraperSettingsSheet( + scraperName = settingsSheet.scraper.name, + language = language, + loading = settingsSheet.loading, + fields = settingsSheet.fields, + initialValues = settingsSheet.scraper.settings, + onDismiss = viewModel::dismissScraperSettings, + onSave = { values -> viewModel.saveScraperSettings(settingsSheet.scraper.id, values) } + ) + } } @Composable @@ -68,6 +85,7 @@ fun PluginsSettingsScreen( onAddRepository: (String) -> Unit, onRemoveRepository: (String) -> Unit, onToggleScraper: (String, Boolean) -> Unit, + onOpenScraperSettings: (PluginScraperUiModel) -> Unit, onBackRequested: () -> Unit, modifier: Modifier = Modifier ) { @@ -123,7 +141,12 @@ fun PluginsSettingsScreen( ) } else { state.scrapers.forEach { scraper -> - PluginScraperRow(scraper = scraper, onToggle = { enabled -> onToggleScraper(scraper.id, enabled) }) + PluginScraperRow( + scraper = scraper, + language = language, + onToggle = { enabled -> onToggleScraper(scraper.id, enabled) }, + onOpenSettings = { onOpenScraperSettings(scraper) } + ) } } Spacer(Modifier.height(120.dp)) @@ -214,7 +237,12 @@ private fun PluginRepositoryRow( } @Composable -private fun PluginScraperRow(scraper: PluginScraperUiModel, onToggle: (Boolean) -> Unit) { +private fun PluginScraperRow( + scraper: PluginScraperUiModel, + language: String?, + onToggle: (Boolean) -> Unit, + onOpenSettings: () -> Unit +) { Row( modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, @@ -224,6 +252,24 @@ private fun PluginScraperRow(scraper: PluginScraperUiModel, onToggle: (Boolean) Text(scraper.name, color = Color.White) Text(scraper.supportedTypes.joinToString(", "), color = Color.White.copy(alpha = 0.4f), fontSize = 11.sp) } + if (scraper.hasSettings) { + Box( + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + .background(Color.White.copy(alpha = 0.05f)) + .clickable(onClick = onOpenSettings), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Rounded.Settings, + contentDescription = AppStrings.t(language, "settings.plugins.settings"), + tint = Color.White.copy(alpha = 0.7f), + modifier = Modifier.size(18.dp) + ) + } + Spacer(Modifier.width(8.dp)) + } Switch( checked = scraper.enabled, onCheckedChange = onToggle, diff --git a/app/src/main/java/com/fluxa/app/ui/settings/PluginsSettingsViewModel.kt b/app/src/main/java/com/fluxa/app/ui/settings/PluginsSettingsViewModel.kt index b696131..7aa7c8e 100644 --- a/app/src/main/java/com/fluxa/app/ui/settings/PluginsSettingsViewModel.kt +++ b/app/src/main/java/com/fluxa/app/ui/settings/PluginsSettingsViewModel.kt @@ -1,8 +1,13 @@ package com.fluxa.app.ui.settings +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.fluxa.app.plugins.PluginRepositoryManager +import com.fluxa.app.plugins.PluginScraperUiModel +import com.fluxa.app.plugins.PluginSettingsFieldUiModel import com.fluxa.app.plugins.PluginsUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted @@ -11,6 +16,12 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject +data class PluginScraperSettingsUiState( + val scraper: PluginScraperUiModel, + val loading: Boolean, + val fields: List +) + @HiltViewModel class PluginsSettingsViewModel @Inject constructor( private val manager: PluginRepositoryManager @@ -19,6 +30,9 @@ class PluginsSettingsViewModel @Inject constructor( val state: StateFlow = manager.state .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), PluginsUiState()) + var settingsSheet by mutableStateOf(null) + private set + fun addRepository(url: String) { val trimmed = url.trim() if (trimmed.isEmpty()) return @@ -32,4 +46,23 @@ class PluginsSettingsViewModel @Inject constructor( fun toggleScraper(scraperId: String, enabled: Boolean) { viewModelScope.launch { manager.toggleScraper(scraperId, enabled) } } + + fun openScraperSettings(scraper: PluginScraperUiModel) { + settingsSheet = PluginScraperSettingsUiState(scraper = scraper, loading = true, fields = emptyList()) + viewModelScope.launch { + val fields = manager.getSettingsLayout(scraper) + if (settingsSheet?.scraper?.id == scraper.id) { + settingsSheet = settingsSheet?.copy(loading = false, fields = fields) + } + } + } + + fun dismissScraperSettings() { + settingsSheet = null + } + + fun saveScraperSettings(scraperId: String, values: Map) { + viewModelScope.launch { manager.updateScraperSettings(scraperId, values) } + settingsSheet = null + } } diff --git a/core/src/commonMain/resources/i18n/english_us.json b/core/src/commonMain/resources/i18n/english_us.json index 2d52be3..ee5bc12 100644 --- a/core/src/commonMain/resources/i18n/english_us.json +++ b/core/src/commonMain/resources/i18n/english_us.json @@ -298,6 +298,12 @@ "settings.plugins.scraper_count": "%s scrapers", "settings.plugins.adding": "Adding repository…", "settings.plugins.error": "Failed to add repository: %s", + "settings.plugins.settings": "Settings", + "settings.plugins.settings_title": "%s Settings", + "settings.plugins.settings_save": "Save", + "settings.plugins.settings_loading": "Loading settings…", + "settings.plugins.settings_empty": "This scraper has no configurable settings.", + "settings.plugins.select_placeholder": "Select an option", "settings.torrent_wifi_only": "Wi-Fi Only", "settings.torrent_wifi_only_desc": "Only stream torrents when connected to Wi-Fi.", "auto.stremio_like_speed_presets": "Stremio-like speed presets.", diff --git a/core/src/commonMain/resources/i18n/tr_tr.json b/core/src/commonMain/resources/i18n/tr_tr.json index b583792..598c82b 100644 --- a/core/src/commonMain/resources/i18n/tr_tr.json +++ b/core/src/commonMain/resources/i18n/tr_tr.json @@ -298,6 +298,12 @@ "settings.plugins.scraper_count": "%s kazıyıcı", "settings.plugins.adding": "Depo ekleniyor…", "settings.plugins.error": "Depo eklenemedi: %s", + "settings.plugins.settings": "Ayarlar", + "settings.plugins.settings_title": "%s Ayarları", + "settings.plugins.settings_save": "Kaydet", + "settings.plugins.settings_loading": "Ayarlar yükleniyor…", + "settings.plugins.settings_empty": "Bu kazıyıcının yapılandırılabilir ayarı yok.", + "settings.plugins.select_placeholder": "Bir seçenek seçin", "settings.torrent_wifi_only": "Yalnızca Wi-Fi", "settings.torrent_wifi_only_desc": "Yalnızca Wi-Fi'ye bağlıyken torrent akışı yapar.", "auto.stremio_like_speed_presets": "Stremio benzeri hazır hız profilleri.", diff --git a/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreUniFfi.kt b/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreUniFfi.kt index 42c4430..a4f8a48 100644 --- a/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreUniFfi.kt +++ b/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreUniFfi.kt @@ -10,6 +10,7 @@ import com.fluxa.core.uniffi.destroyAppCoreStateJson import com.fluxa.core.uniffi.destroyHeadlessEngineJson import com.fluxa.core.uniffi.executePluginScraper as executePluginScraperUniFfi import com.fluxa.core.uniffi.fluxaCoreVersion +import com.fluxa.core.uniffi.getPluginScraperSettingsLayout as getPluginScraperSettingsLayoutUniFfi import com.fluxa.core.uniffi.headlessEngineCompleteEffectJson import com.fluxa.core.uniffi.headlessEngineDispatchJson import com.fluxa.core.uniffi.headlessEngineSnapshotJson @@ -91,9 +92,23 @@ object FluxaCoreUniFfi { fun executePluginScraper( client: PluginHttpClient, code: String, + scraperId: String, + scraperSettingsJson: String, tmdbId: String, mediaType: String, season: Int?, episode: Int? - ): String = executePluginScraperUniFfi(client, code, tmdbId, mediaType, season, episode) + ): String = executePluginScraperUniFfi( + client, + code, + scraperId, + scraperSettingsJson, + tmdbId, + mediaType, + season, + episode + ) + + fun getPluginScraperSettingsLayout(code: String, scraperId: String): String = + getPluginScraperSettingsLayoutUniFfi(code, scraperId) }