mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-03 23:46:54 +00:00
Unify CloudStream and Nuvio plugin management into a shared Plugins feature
Addons now only manages Stremio addons. CloudStream3 repo/plugin state and the Android-only Nuvio scraper settings screen both move into a new shared KMP feature (feature/plugins) with a real Android data source combining PluginRepositoryManager and PluginManager, and an iOS stub data source pending a follow-up Swift implementation.
This commit is contained in:
parent
cfd7a4b32b
commit
114f36a590
21 changed files with 812 additions and 513 deletions
|
|
@ -18,6 +18,7 @@ import com.fluxa.app.ui.catalog.AppContainer
|
|||
import com.fluxa.app.ui.catalog.DetailViewModel
|
||||
import com.fluxa.app.ui.catalog.HomeViewModel
|
||||
import com.fluxa.app.ui.profile.AndroidProfileDataSource
|
||||
import com.fluxa.app.ui.settings.AndroidPluginsDataSource
|
||||
|
||||
class AndroidFluxaPlatformServices(
|
||||
homeViewModel: HomeViewModel,
|
||||
|
|
@ -45,13 +46,17 @@ class AndroidFluxaPlatformServices(
|
|||
override val detailDataSource = AndroidDetailDataSource(detailViewModel, activeProfile)
|
||||
override val profileDataSource = AndroidProfileDataSource(profileManager)
|
||||
override val addonStoreDataSource = AndroidAddonStoreDataSource(
|
||||
pluginManager = AppContainer.pluginManager,
|
||||
repository = AppContainer.repository,
|
||||
profileManager = profileManager,
|
||||
homeViewModel = homeViewModel,
|
||||
activeProfile = activeProfile,
|
||||
onProfileChanged = onActiveProfileChanged
|
||||
)
|
||||
override val pluginsDataSource = AndroidPluginsDataSource(
|
||||
pluginRepositoryManager = AppContainer.pluginRepositoryManager,
|
||||
pluginManager = AppContainer.pluginManager,
|
||||
language = { activeProfile()?.language ?: "en" }
|
||||
)
|
||||
override val authDataSource = AndroidAuthDataSource(
|
||||
authService = AppContainer.authService,
|
||||
nuvioCoordinator = AppContainer.nuvioImportCoordinator,
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ class MainActivity : FragmentActivity() {
|
|||
|
||||
@Inject lateinit var profileManager: ProfileManager
|
||||
@Inject lateinit var pluginManager: PluginManager
|
||||
@Inject lateinit var pluginRepositoryManager: com.fluxa.app.plugins.PluginRepositoryManager
|
||||
@Inject lateinit var stremioRepository: StremioRepository
|
||||
@Inject lateinit var authService: StremioService
|
||||
@Inject lateinit var nuvioImportCoordinator: com.fluxa.app.data.repository.NuvioAccountImportCoordinator
|
||||
|
|
@ -145,7 +146,7 @@ class MainActivity : FragmentActivity() {
|
|||
|
||||
handleIntent(intent)
|
||||
|
||||
AppContainer.initialize(profileManager, pluginManager, stremioRepository, authService, nuvioImportCoordinator)
|
||||
AppContainer.initialize(profileManager, pluginManager, pluginRepositoryManager, stremioRepository, authService, nuvioImportCoordinator)
|
||||
|
||||
setContent {
|
||||
AppTheme {
|
||||
|
|
|
|||
|
|
@ -7,15 +7,9 @@ import com.fluxa.app.data.local.ProfileManager
|
|||
import com.fluxa.app.data.local.UserProfile
|
||||
import com.fluxa.app.data.remote.AddonDescriptor
|
||||
import com.fluxa.app.data.repository.StremioRepository
|
||||
import com.fluxa.app.plugins.PluginManager
|
||||
import com.fluxa.app.plugins.cloudstream.InstalledPlugin
|
||||
import com.fluxa.app.plugins.cloudstream.PluginInfo
|
||||
import com.fluxa.app.plugins.cloudstream.PluginRepositoryEntry
|
||||
import com.fluxa.app.shared.feature.addonstore.AddonStoreDataSource
|
||||
import com.fluxa.app.shared.feature.addonstore.AddonStoreInputType
|
||||
import com.fluxa.app.shared.feature.addonstore.AddonStoreUiState
|
||||
import com.fluxa.app.shared.feature.addonstore.CloudstreamPluginUiModel
|
||||
import com.fluxa.app.shared.feature.addonstore.CloudstreamRepoUiModel
|
||||
import com.fluxa.app.shared.feature.addonstore.InstalledAddonUiModel
|
||||
import com.fluxa.app.ui.installLocalAddonForProfile
|
||||
import com.fluxa.app.ui.moveLocalAddonForProfile
|
||||
|
|
@ -27,7 +21,6 @@ import kotlinx.coroutines.flow.combine
|
|||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class AndroidAddonStoreDataSource(
|
||||
private val pluginManager: PluginManager,
|
||||
private val repository: StremioRepository,
|
||||
private val profileManager: ProfileManager,
|
||||
private val homeViewModel: HomeViewModel,
|
||||
|
|
@ -41,12 +34,6 @@ class AndroidAddonStoreDataSource(
|
|||
val isSubmittingInput: Boolean = false,
|
||||
val inputError: String? = null,
|
||||
val refreshingUrl: String? = null,
|
||||
val openRepoUrl: String? = null,
|
||||
val openRepoName: String? = null,
|
||||
val openRepoPlugins: List<PluginInfo> = emptyList(),
|
||||
val isLoadingRepoPlugins: Boolean = false,
|
||||
val installingPluginKeys: Set<String> = emptySet(),
|
||||
val repoDialogError: String? = null,
|
||||
val profileRevision: Long = 0L,
|
||||
val addedAddonName: String? = null
|
||||
)
|
||||
|
|
@ -56,23 +43,18 @@ class AndroidAddonStoreDataSource(
|
|||
|
||||
override fun detectInputType(text: String): AddonStoreInputType = when (FluxaCoreNative.addonStoreInputType(text)) {
|
||||
"stremio_manifest" -> AddonStoreInputType.STREMIO_MANIFEST
|
||||
"cloudstream_repo" -> AddonStoreInputType.CLOUDSTREAM_REPO
|
||||
"search_query" -> AddonStoreInputType.SEARCH_QUERY
|
||||
else -> AddonStoreInputType.UNKNOWN
|
||||
}
|
||||
|
||||
override fun observeAddonStore(): Flow<AddonStoreUiState> = combine(
|
||||
pluginManager.installedPlugins,
|
||||
pluginManager.repositories,
|
||||
installedUserAddons,
|
||||
extras
|
||||
) { installedPlugins, repos, userAddonsAndLoaded, ex ->
|
||||
buildState(installedPlugins, repos, userAddonsAndLoaded.first, userAddonsAndLoaded.second, ex)
|
||||
) { userAddonsAndLoaded, ex ->
|
||||
buildState(userAddonsAndLoaded.first, userAddonsAndLoaded.second, ex)
|
||||
}
|
||||
|
||||
private fun buildState(
|
||||
installedPlugins: List<InstalledPlugin>,
|
||||
repos: List<PluginRepositoryEntry>,
|
||||
userAddons: List<AddonDescriptor>,
|
||||
loaded: Boolean,
|
||||
ex: Extras
|
||||
|
|
@ -131,33 +113,14 @@ class AndroidAddonStoreDataSource(
|
|||
)
|
||||
}
|
||||
|
||||
val pluginModels = ex.openRepoPlugins.map { plugin ->
|
||||
val key = pluginManager.pluginInstallKey(ex.openRepoUrl, plugin.internalName)
|
||||
CloudstreamPluginUiModel(
|
||||
internalName = plugin.internalName,
|
||||
name = plugin.name,
|
||||
description = plugin.description,
|
||||
iconUrl = plugin.iconUrl,
|
||||
typesLabel = plugin.getDisplayTypes(),
|
||||
isInstalled = installedPlugins.any { pluginManager.pluginInstallKey(it.repositoryUrl, it.internalName) == key }
|
||||
)
|
||||
}
|
||||
|
||||
return AddonStoreUiState(
|
||||
installedAddons = merged,
|
||||
cloudstreamRepos = repos.map { CloudstreamRepoUiModel(name = it.name, url = it.url, iconUrl = it.iconUrl) },
|
||||
accentColorArgb = profile?.safeAccentColorArgb?.toLong()?.and(0xffffffffL) ?: 0xFF4CAF50L,
|
||||
isLoading = profile != null && !loaded,
|
||||
inputText = ex.inputText,
|
||||
inputDetectedType = ex.detectedType,
|
||||
isSubmittingInput = ex.isSubmittingInput,
|
||||
inputError = ex.inputError,
|
||||
openRepoUrl = ex.openRepoUrl,
|
||||
openRepoName = ex.openRepoName,
|
||||
openRepoPlugins = pluginModels,
|
||||
isLoadingRepoPlugins = ex.isLoadingRepoPlugins,
|
||||
installingPluginKeys = ex.installingPluginKeys,
|
||||
repoDialogError = ex.repoDialogError,
|
||||
addedAddonName = ex.addedAddonName
|
||||
)
|
||||
}
|
||||
|
|
@ -212,22 +175,6 @@ class AndroidAddonStoreDataSource(
|
|||
}
|
||||
}
|
||||
}
|
||||
AddonStoreInputType.CLOUDSTREAM_REPO -> {
|
||||
extras.update { it.copy(isSubmittingInput = true, inputError = null) }
|
||||
val normalizedUrl = FluxaCoreNative.normalizeCloudstreamRepoUrl(trimmed)
|
||||
val result = pluginManager.addRepository(normalizedUrl)
|
||||
extras.update {
|
||||
if (result.isSuccess) {
|
||||
it.copy(inputText = "", isSubmittingInput = false, detectedType = AddonStoreInputType.UNKNOWN)
|
||||
} else {
|
||||
it.copy(
|
||||
isSubmittingInput = false,
|
||||
inputError = result.exceptionOrNull()?.message
|
||||
?: AppStrings.t(activeProfile()?.language ?: "en", "addons.repository_add_failed")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -267,43 +214,6 @@ class AndroidAddonStoreDataSource(
|
|||
extras.update { it.copy(refreshingUrl = null) }
|
||||
}
|
||||
|
||||
override suspend fun openRepo(url: String) {
|
||||
val repoName = pluginManager.repositories.value.find { it.url == url }?.name?.takeIf { it.isNotBlank() }
|
||||
extras.update {
|
||||
it.copy(openRepoUrl = url, openRepoName = repoName, isLoadingRepoPlugins = true, openRepoPlugins = emptyList(), repoDialogError = null)
|
||||
}
|
||||
val plugins = pluginManager.getPluginsFromRepository(url)
|
||||
extras.update { it.copy(openRepoPlugins = plugins, isLoadingRepoPlugins = false) }
|
||||
}
|
||||
|
||||
override suspend fun dismissRepoDialog() {
|
||||
extras.update { it.copy(openRepoUrl = null, openRepoName = null, openRepoPlugins = emptyList(), repoDialogError = null) }
|
||||
}
|
||||
|
||||
override suspend fun removeRepo(url: String) {
|
||||
pluginManager.removeRepository(url)
|
||||
}
|
||||
|
||||
override suspend fun toggleRepoPlugin(repoUrl: String, internalName: String) {
|
||||
val key = pluginManager.pluginInstallKey(repoUrl, internalName)
|
||||
val plugin = extras.value.openRepoPlugins.find { it.internalName == internalName } ?: return
|
||||
val isInstalled = pluginManager.installedPlugins.value.any {
|
||||
pluginManager.pluginInstallKey(it.repositoryUrl, it.internalName) == key
|
||||
}
|
||||
extras.update { it.copy(installingPluginKeys = it.installingPluginKeys + key, repoDialogError = null) }
|
||||
if (isInstalled) {
|
||||
pluginManager.uninstallPlugin(repoUrl, internalName)
|
||||
} else {
|
||||
val result = pluginManager.installPlugin(plugin, repoUrl)
|
||||
if (result.isFailure) {
|
||||
extras.update {
|
||||
it.copy(repoDialogError = result.exceptionOrNull()?.message ?: AppStrings.t(activeProfile()?.language ?: "en", "auto.install_failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
extras.update { it.copy(installingPluginKeys = it.installingPluginKeys - key) }
|
||||
}
|
||||
|
||||
override suspend fun dismissAddedAddonDialog() {
|
||||
extras.update { it.copy(addedAddonName = null) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,15 @@ import com.fluxa.app.data.remote.StremioService
|
|||
import com.fluxa.app.data.repository.NuvioAccountImportCoordinator
|
||||
import com.fluxa.app.data.repository.StremioRepository
|
||||
import com.fluxa.app.plugins.PluginManager
|
||||
import com.fluxa.app.plugins.PluginRepositoryManager
|
||||
|
||||
object AppContainer {
|
||||
lateinit var profileManager: ProfileManager
|
||||
private set
|
||||
lateinit var pluginManager: PluginManager
|
||||
private set
|
||||
lateinit var pluginRepositoryManager: PluginRepositoryManager
|
||||
private set
|
||||
lateinit var repository: StremioRepository
|
||||
private set
|
||||
lateinit var authService: StremioService
|
||||
|
|
@ -21,12 +24,14 @@ object AppContainer {
|
|||
fun initialize(
|
||||
profileManager: ProfileManager,
|
||||
pluginManager: PluginManager,
|
||||
pluginRepositoryManager: PluginRepositoryManager,
|
||||
repository: StremioRepository,
|
||||
authService: StremioService,
|
||||
nuvioImportCoordinator: NuvioAccountImportCoordinator
|
||||
) {
|
||||
this.profileManager = profileManager
|
||||
this.pluginManager = pluginManager
|
||||
this.pluginRepositoryManager = pluginRepositoryManager
|
||||
this.repository = repository
|
||||
this.authService = authService
|
||||
this.nuvioImportCoordinator = nuvioImportCoordinator
|
||||
|
|
|
|||
|
|
@ -88,16 +88,6 @@ internal fun AppRoutesHost(
|
|||
}
|
||||
}
|
||||
|
||||
var showPluginsSettings by remember { mutableStateOf(false) }
|
||||
|
||||
if (showPluginsSettings) {
|
||||
com.fluxa.app.ui.settings.PluginsSettingsRoute(
|
||||
language = activeProfile?.language,
|
||||
onBackRequested = { showPluginsSettings = false }
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalHeroTrailerSurface provides { url, cues, onActiveSubtitleChanged, trailerModifier ->
|
||||
HeroTrailerVideoSurface(url, cues, onActiveSubtitleChanged, trailerModifier)
|
||||
|
|
@ -169,6 +159,7 @@ internal fun AppRoutesHost(
|
|||
context.startActivity(android.content.Intent(android.content.Intent.ACTION_VIEW, android.net.Uri.parse(url)))
|
||||
},
|
||||
onAddonStoreBackRequested = navigateBackSafely,
|
||||
onPluginsBackRequested = navigateBackSafely,
|
||||
onAuthBackRequested = navigateBackSafely,
|
||||
onAuthCompleted = { onNavigateToDestination(FluxaDestination.Home) },
|
||||
authStartOnNuvio = authStartOnNuvio,
|
||||
|
|
@ -236,7 +227,7 @@ internal fun AppRoutesHost(
|
|||
settingsPopRequestId = settingsPopRequestId,
|
||||
onSettingsCanPopChanged = onSettingsCanPopChanged,
|
||||
onManageAddonsRequested = { onNavigateToDestination(FluxaDestination.AddonStore) },
|
||||
onManagePluginsRequested = { showPluginsSettings = true },
|
||||
onManagePluginsRequested = { onNavigateToDestination(FluxaDestination.Plugins) },
|
||||
onConnectStremioRequested = {
|
||||
val profile = activeProfile
|
||||
if (profile?.authKey.isNullOrBlank()) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,217 @@
|
|||
package com.fluxa.app.ui.settings
|
||||
|
||||
import com.fluxa.app.common.AppStrings
|
||||
import com.fluxa.app.core.rust.FluxaCoreNative
|
||||
import com.fluxa.app.plugins.PluginManager
|
||||
import com.fluxa.app.plugins.PluginRepositoryManager
|
||||
import com.fluxa.app.plugins.PluginScraperUiModel as NuvioScraperUiModel
|
||||
import com.fluxa.app.plugins.cloudstream.InstalledPlugin
|
||||
import com.fluxa.app.plugins.cloudstream.PluginInfo
|
||||
import com.fluxa.app.shared.feature.plugins.CloudstreamPluginUiModel
|
||||
import com.fluxa.app.shared.feature.plugins.CloudstreamRepoUiModel
|
||||
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.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class AndroidPluginsDataSource(
|
||||
private val pluginRepositoryManager: PluginRepositoryManager,
|
||||
private val pluginManager: PluginManager,
|
||||
private val language: () -> String
|
||||
) : PluginsDataSource {
|
||||
|
||||
private data class Extras(
|
||||
val scraperSettingsSheet: PluginScraperSettingsUiState? = null,
|
||||
val isAddingCloudstreamRepo: Boolean = false,
|
||||
val cloudstreamRepoError: String? = null,
|
||||
val openRepoUrl: String? = null,
|
||||
val openRepoName: String? = null,
|
||||
val openRepoPlugins: List<PluginInfo> = emptyList(),
|
||||
val isLoadingRepoPlugins: Boolean = false,
|
||||
val installingPluginKeys: Set<String> = emptySet(),
|
||||
val repoDialogError: String? = null
|
||||
)
|
||||
|
||||
private val extras = MutableStateFlow(Extras())
|
||||
private var latestNuvioScrapers: List<NuvioScraperUiModel> = emptyList()
|
||||
|
||||
override fun observePlugins(): Flow<PluginsUiState> = combine(
|
||||
pluginRepositoryManager.state,
|
||||
pluginManager.installedPlugins,
|
||||
pluginManager.repositories,
|
||||
extras
|
||||
) { nuvio, installedPlugins, repos, ex ->
|
||||
latestNuvioScrapers = nuvio.scrapers
|
||||
PluginsUiState(
|
||||
repositories = nuvio.repositories.map {
|
||||
PluginRepositoryUiModel(
|
||||
manifestUrl = it.manifestUrl,
|
||||
name = it.name,
|
||||
description = it.description,
|
||||
scraperCount = it.scraperCount
|
||||
)
|
||||
},
|
||||
scrapers = nuvio.scrapers.map {
|
||||
PluginScraperUiModel(
|
||||
id = it.id,
|
||||
name = it.name,
|
||||
repositoryUrl = it.repositoryUrl,
|
||||
enabled = it.enabled,
|
||||
supportedTypes = it.supportedTypes,
|
||||
hasSettings = it.hasSettings,
|
||||
settings = it.settings
|
||||
)
|
||||
},
|
||||
addingRepositoryUrl = nuvio.addingRepositoryUrl,
|
||||
repositoryError = nuvio.error,
|
||||
scraperSettingsSheet = ex.scraperSettingsSheet,
|
||||
cloudstreamRepos = repos.map { CloudstreamRepoUiModel(name = it.name, url = it.url, iconUrl = it.iconUrl) },
|
||||
isAddingCloudstreamRepo = ex.isAddingCloudstreamRepo,
|
||||
cloudstreamRepoError = ex.cloudstreamRepoError,
|
||||
openRepoUrl = ex.openRepoUrl,
|
||||
openRepoName = ex.openRepoName,
|
||||
openRepoPlugins = mapCloudstreamPlugins(ex, installedPlugins),
|
||||
isLoadingRepoPlugins = ex.isLoadingRepoPlugins,
|
||||
installingPluginKeys = ex.installingPluginKeys,
|
||||
repoDialogError = ex.repoDialogError
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapCloudstreamPlugins(ex: Extras, installedPlugins: List<InstalledPlugin>): List<CloudstreamPluginUiModel> =
|
||||
ex.openRepoPlugins.map { plugin ->
|
||||
val key = pluginManager.pluginInstallKey(ex.openRepoUrl, plugin.internalName)
|
||||
CloudstreamPluginUiModel(
|
||||
internalName = plugin.internalName,
|
||||
name = plugin.name,
|
||||
description = plugin.description,
|
||||
iconUrl = plugin.iconUrl,
|
||||
typesLabel = plugin.getDisplayTypes(),
|
||||
isInstalled = installedPlugins.any { pluginManager.pluginInstallKey(it.repositoryUrl, it.internalName) == key }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun refresh() = Unit
|
||||
|
||||
override suspend fun addRepository(manifestUrl: String) {
|
||||
pluginRepositoryManager.addRepository(manifestUrl)
|
||||
}
|
||||
|
||||
override suspend fun removeRepository(manifestUrl: String) {
|
||||
pluginRepositoryManager.removeRepository(manifestUrl)
|
||||
}
|
||||
|
||||
override suspend fun refreshRepository(manifestUrl: String) {
|
||||
pluginRepositoryManager.refreshRepository(manifestUrl)
|
||||
}
|
||||
|
||||
override suspend fun toggleScraper(scraperId: String, enabled: Boolean) {
|
||||
pluginRepositoryManager.toggleScraper(scraperId, enabled)
|
||||
}
|
||||
|
||||
override suspend fun requestScraperSettings(scraperId: String) {
|
||||
val scraper = latestNuvioScrapers.find { it.id == scraperId } ?: return
|
||||
val sharedScraper = PluginScraperUiModel(
|
||||
id = scraper.id,
|
||||
name = scraper.name,
|
||||
repositoryUrl = scraper.repositoryUrl,
|
||||
enabled = scraper.enabled,
|
||||
supportedTypes = scraper.supportedTypes,
|
||||
hasSettings = scraper.hasSettings,
|
||||
settings = scraper.settings
|
||||
)
|
||||
extras.update {
|
||||
it.copy(scraperSettingsSheet = PluginScraperSettingsUiState(scraper = sharedScraper, loading = true, fields = emptyList()))
|
||||
}
|
||||
val fields = pluginRepositoryManager.getSettingsLayout(scraper).map { field ->
|
||||
PluginSettingsFieldUiModel(
|
||||
key = field.key,
|
||||
type = field.type,
|
||||
label = field.label,
|
||||
description = field.description,
|
||||
placeholder = field.placeholder,
|
||||
isPassword = field.isPassword,
|
||||
defaultValue = field.defaultValue,
|
||||
defaultBoolean = field.defaultBoolean,
|
||||
options = field.options.map { PluginSettingsOptionUiModel(label = it.label, value = it.value) }
|
||||
)
|
||||
}
|
||||
extras.update { current ->
|
||||
val sheet = current.scraperSettingsSheet
|
||||
if (sheet?.scraper?.id == scraperId) {
|
||||
current.copy(scraperSettingsSheet = sheet.copy(loading = false, fields = fields))
|
||||
} else {
|
||||
current
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun dismissScraperSettings() {
|
||||
extras.update { it.copy(scraperSettingsSheet = null) }
|
||||
}
|
||||
|
||||
override suspend fun saveScraperSettings(scraperId: String, values: Map<String, Any?>) {
|
||||
pluginRepositoryManager.updateScraperSettings(scraperId, values)
|
||||
extras.update { it.copy(scraperSettingsSheet = null) }
|
||||
}
|
||||
|
||||
override suspend fun addCloudstreamRepository(url: String) {
|
||||
extras.update { it.copy(isAddingCloudstreamRepo = true, cloudstreamRepoError = null) }
|
||||
val normalizedUrl = FluxaCoreNative.normalizeCloudstreamRepoUrl(url)
|
||||
val result = pluginManager.addRepository(normalizedUrl)
|
||||
extras.update {
|
||||
if (result.isSuccess) {
|
||||
it.copy(isAddingCloudstreamRepo = false)
|
||||
} else {
|
||||
it.copy(
|
||||
isAddingCloudstreamRepo = false,
|
||||
cloudstreamRepoError = result.exceptionOrNull()?.message
|
||||
?: AppStrings.t(language(), "addons.repository_add_failed")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun openRepo(url: String) {
|
||||
val repoName = pluginManager.repositories.value.find { it.url == url }?.name?.takeIf { it.isNotBlank() }
|
||||
extras.update {
|
||||
it.copy(openRepoUrl = url, openRepoName = repoName, isLoadingRepoPlugins = true, openRepoPlugins = emptyList(), repoDialogError = null)
|
||||
}
|
||||
val plugins = pluginManager.getPluginsFromRepository(url)
|
||||
extras.update { it.copy(openRepoPlugins = plugins, isLoadingRepoPlugins = false) }
|
||||
}
|
||||
|
||||
override suspend fun dismissRepoDialog() {
|
||||
extras.update { it.copy(openRepoUrl = null, openRepoName = null, openRepoPlugins = emptyList(), repoDialogError = null) }
|
||||
}
|
||||
|
||||
override suspend fun removeRepo(url: String) {
|
||||
pluginManager.removeRepository(url)
|
||||
}
|
||||
|
||||
override suspend fun toggleRepoPlugin(repoUrl: String, internalName: String) {
|
||||
val key = pluginManager.pluginInstallKey(repoUrl, internalName)
|
||||
val plugin = extras.value.openRepoPlugins.find { it.internalName == internalName } ?: return
|
||||
val isInstalled = pluginManager.installedPlugins.value.any {
|
||||
pluginManager.pluginInstallKey(it.repositoryUrl, it.internalName) == key
|
||||
}
|
||||
extras.update { it.copy(installingPluginKeys = it.installingPluginKeys + key, repoDialogError = null) }
|
||||
if (isInstalled) {
|
||||
pluginManager.uninstallPlugin(repoUrl, internalName)
|
||||
} else {
|
||||
val result = pluginManager.installPlugin(plugin, repoUrl)
|
||||
if (result.isFailure) {
|
||||
extras.update {
|
||||
it.copy(repoDialogError = result.exceptionOrNull()?.message ?: AppStrings.t(language(), "auto.install_failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
extras.update { it.copy(installingPluginKeys = it.installingPluginKeys - key) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
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
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
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<PluginSettingsFieldUiModel>
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class PluginsSettingsViewModel @Inject constructor(
|
||||
private val manager: PluginRepositoryManager
|
||||
) : ViewModel() {
|
||||
|
||||
val state: StateFlow<PluginsUiState> = manager.state
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), PluginsUiState())
|
||||
|
||||
var settingsSheet by mutableStateOf<PluginScraperSettingsUiState?>(null)
|
||||
private set
|
||||
|
||||
fun addRepository(url: String) {
|
||||
val trimmed = url.trim()
|
||||
if (trimmed.isEmpty()) return
|
||||
viewModelScope.launch { manager.addRepository(trimmed) }
|
||||
}
|
||||
|
||||
fun removeRepository(manifestUrl: String) {
|
||||
viewModelScope.launch { manager.removeRepository(manifestUrl) }
|
||||
}
|
||||
|
||||
fun refreshRepository(manifestUrl: String) {
|
||||
viewModelScope.launch { manager.refreshRepository(manifestUrl) }
|
||||
}
|
||||
|
||||
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<String, Any?>) {
|
||||
viewModelScope.launch { manager.updateScraperSettings(scraperId, values) }
|
||||
settingsSheet = null
|
||||
}
|
||||
}
|
||||
|
|
@ -107,6 +107,9 @@ import com.fluxa.app.shared.feature.library.LibraryAction
|
|||
import com.fluxa.app.shared.feature.library.LibraryFolderDetailScreen
|
||||
import com.fluxa.app.shared.feature.library.LibraryScreen
|
||||
import com.fluxa.app.shared.feature.library.LibraryUiState
|
||||
import com.fluxa.app.shared.feature.plugins.PluginsAction
|
||||
import com.fluxa.app.shared.feature.plugins.PluginsScreen
|
||||
import com.fluxa.app.shared.feature.plugins.PluginsUiState
|
||||
import com.fluxa.app.shared.feature.profile.ProfileAction
|
||||
import com.fluxa.app.shared.feature.profile.ProfileEditScreen
|
||||
import com.fluxa.app.shared.feature.profile.ProfileEditTarget
|
||||
|
|
@ -150,6 +153,7 @@ enum class FluxaDestination(val titleKey: String) {
|
|||
Library("nav.library"),
|
||||
Settings("nav.settings"),
|
||||
AddonStore("auto.addons"),
|
||||
Plugins("settings.plugins.title"),
|
||||
Auth("auth.log_in"),
|
||||
ProfileList("auto.profile")
|
||||
}
|
||||
|
|
@ -205,6 +209,9 @@ fun FluxaApp(
|
|||
onAddonStoreAction: (AddonStoreAction) -> Unit = {},
|
||||
onOpenUrlRequested: (String) -> Unit = {},
|
||||
onAddonStoreBackRequested: () -> Unit = {},
|
||||
pluginsState: PluginsUiState? = null,
|
||||
onPluginsAction: (PluginsAction) -> Unit = {},
|
||||
onPluginsBackRequested: () -> Unit = {},
|
||||
authState: AuthUiState? = null,
|
||||
onAuthAction: (AuthAction) -> Unit = {},
|
||||
nuvioIcon: @Composable () -> Unit = {},
|
||||
|
|
@ -426,6 +433,13 @@ fun FluxaApp(
|
|||
onBackRequested = onAddonStoreBackRequested,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
state.destination == FluxaDestination.Plugins && pluginsState != null -> PluginsScreen(
|
||||
state = pluginsState,
|
||||
language = state.language,
|
||||
onAction = onPluginsAction,
|
||||
onBackRequested = onPluginsBackRequested,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
state.destination == FluxaDestination.Auth && authState != null && deviceType == com.fluxa.app.ui.catalog.DeviceType.TV -> com.fluxa.app.shared.feature.auth.TvAuthScreen(
|
||||
state = authState,
|
||||
language = state.language,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import androidx.compose.ui.Modifier
|
|||
import com.fluxa.app.shared.feature.addonstore.AddonStoreAction
|
||||
import com.fluxa.app.shared.feature.addonstore.AddonStoreDataSource
|
||||
import com.fluxa.app.shared.feature.addonstore.AddonStoreStore
|
||||
import com.fluxa.app.shared.feature.plugins.PluginsAction
|
||||
import com.fluxa.app.shared.feature.plugins.PluginsDataSource
|
||||
import com.fluxa.app.shared.feature.plugins.PluginsStore
|
||||
import com.fluxa.app.shared.feature.auth.AuthAction
|
||||
import com.fluxa.app.shared.feature.auth.AuthDataSource
|
||||
import com.fluxa.app.shared.feature.auth.AuthStore
|
||||
|
|
@ -45,6 +48,7 @@ import com.fluxa.app.shared.feature.settings.SettingsAction
|
|||
import com.fluxa.app.shared.feature.settings.SettingsDataSource
|
||||
import com.fluxa.app.shared.feature.settings.SettingsStore
|
||||
import com.fluxa.app.shared.platform.FluxaAddonStoreServices
|
||||
import com.fluxa.app.shared.platform.FluxaPluginsServices
|
||||
import com.fluxa.app.shared.platform.FluxaAuthServices
|
||||
import com.fluxa.app.shared.platform.FluxaDetailServices
|
||||
import com.fluxa.app.shared.platform.FluxaCalendarServices
|
||||
|
|
@ -69,6 +73,7 @@ fun FluxaAppHost(
|
|||
showNavigationBar: Boolean = true,
|
||||
onOpenUrlRequested: (String) -> Unit = {},
|
||||
onAddonStoreBackRequested: () -> Unit = {},
|
||||
onPluginsBackRequested: () -> Unit = {},
|
||||
onAuthBackRequested: () -> Unit = {},
|
||||
onAuthCompleted: () -> Unit = {},
|
||||
authStartOnNuvio: Boolean = false,
|
||||
|
|
@ -104,6 +109,7 @@ fun FluxaAppHost(
|
|||
searchDataSource = (platformServices as? FluxaSearchServices)?.searchDataSource,
|
||||
profileDataSource = (platformServices as? FluxaProfileServices)?.profileDataSource,
|
||||
addonStoreDataSource = (platformServices as? FluxaAddonStoreServices)?.addonStoreDataSource,
|
||||
pluginsDataSource = (platformServices as? FluxaPluginsServices)?.pluginsDataSource,
|
||||
authDataSource = (platformServices as? FluxaAuthServices)?.authDataSource,
|
||||
settingsDataSource = (platformServices as? FluxaSettingsServices)?.settingsDataSource,
|
||||
deviceType = deviceType,
|
||||
|
|
@ -116,6 +122,7 @@ fun FluxaAppHost(
|
|||
showNavigationBar = showNavigationBar,
|
||||
onOpenUrlRequested = onOpenUrlRequested,
|
||||
onAddonStoreBackRequested = onAddonStoreBackRequested,
|
||||
onPluginsBackRequested = onPluginsBackRequested,
|
||||
onAuthBackRequested = onAuthBackRequested,
|
||||
onAuthCompleted = onAuthCompleted,
|
||||
authStartOnNuvio = authStartOnNuvio,
|
||||
|
|
@ -154,6 +161,7 @@ fun FluxaAppHost(
|
|||
searchDataSource: SearchDataSource? = null,
|
||||
profileDataSource: ProfileDataSource? = null,
|
||||
addonStoreDataSource: AddonStoreDataSource? = null,
|
||||
pluginsDataSource: PluginsDataSource? = null,
|
||||
authDataSource: AuthDataSource? = null,
|
||||
settingsDataSource: SettingsDataSource? = null,
|
||||
deviceType: com.fluxa.app.ui.catalog.DeviceType = com.fluxa.app.ui.catalog.DeviceType.Mobile,
|
||||
|
|
@ -166,6 +174,7 @@ fun FluxaAppHost(
|
|||
showNavigationBar: Boolean = true,
|
||||
onOpenUrlRequested: (String) -> Unit = {},
|
||||
onAddonStoreBackRequested: () -> Unit = {},
|
||||
onPluginsBackRequested: () -> Unit = {},
|
||||
onAuthBackRequested: () -> Unit = {},
|
||||
onAuthCompleted: () -> Unit = {},
|
||||
authStartOnNuvio: Boolean = false,
|
||||
|
|
@ -225,6 +234,10 @@ fun FluxaAppHost(
|
|||
remember(source) { AddonStoreStore(source, scope) }
|
||||
}
|
||||
val addonStoreState = addonStoreStore?.state?.collectAsState()?.value
|
||||
val pluginsStore = pluginsDataSource?.let { source ->
|
||||
remember(source) { PluginsStore(source, scope) }
|
||||
}
|
||||
val pluginsState = pluginsStore?.state?.collectAsState()?.value
|
||||
val authStore = authDataSource?.let { source ->
|
||||
remember(source) { AuthStore(source, scope) }
|
||||
}
|
||||
|
|
@ -319,6 +332,11 @@ fun FluxaAppHost(
|
|||
addonStoreStore?.dispatch(AddonStoreAction.Refresh)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(appState.uiState.destination, pluginsStore) {
|
||||
if (appState.uiState.destination == FluxaDestination.Plugins) {
|
||||
pluginsStore?.dispatch(PluginsAction.Refresh)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(authStartOnNuvio, authStore) {
|
||||
if (authStartOnNuvio) {
|
||||
authStore?.dispatch(AuthAction.ContinueWithNuvio)
|
||||
|
|
@ -472,6 +490,13 @@ fun FluxaAppHost(
|
|||
},
|
||||
onOpenUrlRequested = onOpenUrlRequested,
|
||||
onAddonStoreBackRequested = onAddonStoreBackRequested,
|
||||
pluginsState = pluginsState,
|
||||
onPluginsAction = { action ->
|
||||
scope.launch {
|
||||
pluginsStore?.dispatch(action)
|
||||
}
|
||||
},
|
||||
onPluginsBackRequested = onPluginsBackRequested,
|
||||
authState = authState,
|
||||
onAuthAction = { action ->
|
||||
when (action) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.Flow
|
|||
enum class AddonStoreInputType {
|
||||
UNKNOWN,
|
||||
STREMIO_MANIFEST,
|
||||
CLOUDSTREAM_REPO,
|
||||
SEARCH_QUERY
|
||||
}
|
||||
|
||||
|
|
@ -24,36 +23,14 @@ data class InstalledAddonUiModel(
|
|||
val isRefreshing: Boolean = false
|
||||
)
|
||||
|
||||
data class CloudstreamRepoUiModel(
|
||||
val name: String,
|
||||
val url: String,
|
||||
val iconUrl: String? = null
|
||||
)
|
||||
|
||||
data class CloudstreamPluginUiModel(
|
||||
val internalName: String,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val iconUrl: String? = null,
|
||||
val typesLabel: String = "",
|
||||
val isInstalled: Boolean = false
|
||||
)
|
||||
|
||||
data class AddonStoreUiState(
|
||||
val installedAddons: List<InstalledAddonUiModel> = emptyList(),
|
||||
val cloudstreamRepos: List<CloudstreamRepoUiModel> = emptyList(),
|
||||
val accentColorArgb: Long = 0xFF4CAF50,
|
||||
val isLoading: Boolean = false,
|
||||
val inputText: String = "",
|
||||
val inputDetectedType: AddonStoreInputType = AddonStoreInputType.UNKNOWN,
|
||||
val isSubmittingInput: Boolean = false,
|
||||
val inputError: String? = null,
|
||||
val openRepoUrl: String? = null,
|
||||
val openRepoName: String? = null,
|
||||
val openRepoPlugins: List<CloudstreamPluginUiModel> = emptyList(),
|
||||
val isLoadingRepoPlugins: Boolean = false,
|
||||
val installingPluginKeys: Set<String> = emptySet(),
|
||||
val repoDialogError: String? = null,
|
||||
val addedAddonName: String? = null
|
||||
)
|
||||
|
||||
|
|
@ -66,10 +43,6 @@ sealed interface AddonStoreAction {
|
|||
data class AddonMoved(val url: String, val direction: Int) : AddonStoreAction
|
||||
data class AddonRefreshed(val url: String) : AddonStoreAction
|
||||
data class ConfigureRequested(val url: String) : AddonStoreAction
|
||||
data class RepoOpened(val url: String) : AddonStoreAction
|
||||
data object RepoDialogDismissed : AddonStoreAction
|
||||
data class RepoRemoved(val url: String) : AddonStoreAction
|
||||
data class RepoPluginToggled(val repoUrl: String, val internalName: String) : AddonStoreAction
|
||||
data object AddedAddonDialogDismissed : AddonStoreAction
|
||||
}
|
||||
|
||||
|
|
@ -83,9 +56,5 @@ interface AddonStoreDataSource {
|
|||
suspend fun removeAddon(url: String)
|
||||
suspend fun moveAddon(url: String, direction: Int)
|
||||
suspend fun refreshAddon(url: String)
|
||||
suspend fun openRepo(url: String)
|
||||
suspend fun dismissRepoDialog()
|
||||
suspend fun removeRepo(url: String)
|
||||
suspend fun toggleRepoPlugin(repoUrl: String, internalName: String)
|
||||
suspend fun dismissAddedAddonDialog()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,10 @@ import androidx.compose.foundation.layout.Spacer
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
|
@ -25,21 +23,18 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -112,25 +107,6 @@ fun AddonStoreScreen(
|
|||
)
|
||||
}
|
||||
|
||||
if (state.cloudstreamRepos.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = AppStrings.t(language, "auto.cloudstream3_repositories"),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
}
|
||||
items(state.cloudstreamRepos, key = { "repo:${it.url}" }) { repo ->
|
||||
CloudstreamRepoItem(
|
||||
repo = repo,
|
||||
language = language,
|
||||
onRemove = { onAction(AddonStoreAction.RepoRemoved(repo.url)) },
|
||||
onClick = { onAction(AddonStoreAction.RepoOpened(repo.url)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.installedAddons.isNotEmpty()) {
|
||||
item {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
|
@ -160,17 +136,6 @@ fun AddonStoreScreen(
|
|||
}
|
||||
}
|
||||
|
||||
if (state.openRepoUrl != null) {
|
||||
AddonRepoPluginsDialog(
|
||||
state = state,
|
||||
language = language,
|
||||
onDismiss = { onAction(AddonStoreAction.RepoDialogDismissed) },
|
||||
onTogglePlugin = { internalName ->
|
||||
onAction(AddonStoreAction.RepoPluginToggled(state.openRepoUrl, internalName))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val addedAddonName = state.addedAddonName
|
||||
if (addedAddonName != null) {
|
||||
AlertDialog(
|
||||
|
|
@ -258,49 +223,6 @@ private fun AddonSmartInput(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CloudstreamRepoItem(
|
||||
repo: CloudstreamRepoUiModel,
|
||||
language: String?,
|
||||
onRemove: () -> Unit,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.onFocusChanged { focused = it.isFocused }
|
||||
.background(if (focused) Color.White.copy(alpha = 0.14f) else Color.White.copy(alpha = 0.04f))
|
||||
.border(if (focused) 2.dp else 1.dp, if (focused) Color.White else Color.White.copy(alpha = 0.08f), RoundedCornerShape(14.dp))
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
AddonLogo(url = repo.iconUrl, size = 36.dp, contentDescription = repo.name)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = repo.name.ifBlank { AppStrings.t(language, "addons.cloudstream_repo") },
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 14.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = repo.url,
|
||||
color = Color.White.copy(alpha = 0.5f),
|
||||
fontSize = 11.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
AddonIconButton(icon = Icons.Filled.Close, tint = Color.White.copy(alpha = 0.5f), onClick = onRemove)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InstalledAddonItem(
|
||||
addon: InstalledAddonUiModel,
|
||||
|
|
@ -388,126 +310,6 @@ private fun InstalledAddonItem(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddonRepoPluginsDialog(
|
||||
state: AddonStoreUiState,
|
||||
language: String?,
|
||||
onDismiss: () -> Unit,
|
||||
onTogglePlugin: (String) -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
text = state.openRepoName ?: AppStrings.t(language, "auto.repository_plugins"),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
text = {
|
||||
LazyColumn(modifier = Modifier.height(400.dp)) {
|
||||
if (state.isLoadingRepoPlugins) {
|
||||
item {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||
CircularProgressIndicator(color = Color.White, modifier = Modifier.size(32.dp))
|
||||
}
|
||||
}
|
||||
} else if (state.openRepoPlugins.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = AppStrings.t(language, "auto.no_plugins_found_in_this_repository"),
|
||||
color = Color.White.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val error = state.repoDialogError
|
||||
if (error != null) {
|
||||
item {
|
||||
Text(
|
||||
text = error,
|
||||
color = FluxaColors.errorRed,
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(state.openRepoPlugins, key = { _, p -> p.internalName }) { index, plugin ->
|
||||
val installing = plugin.internalName in state.installingPluginKeys
|
||||
CloudstreamPluginItem(
|
||||
plugin = plugin,
|
||||
isInstalling = installing,
|
||||
onClick = { if (!installing) onTogglePlugin(plugin.internalName) }
|
||||
)
|
||||
if (index < state.openRepoPlugins.lastIndex) {
|
||||
HorizontalDivider(color = Color.White.copy(alpha = 0.07f), thickness = 0.5.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = AppStrings.t(language, "auto.close"), color = Color.White.copy(alpha = 0.7f))
|
||||
}
|
||||
},
|
||||
containerColor = Color(0xFF1A1D26)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CloudstreamPluginItem(
|
||||
plugin: CloudstreamPluginUiModel,
|
||||
isInstalling: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val accentColor = if (plugin.isInstalled) FluxaColors.successGreen else Color.White.copy(alpha = 0.6f)
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.onFocusChanged { focused = it.isFocused }
|
||||
.background(if (focused) Color.White.copy(alpha = 0.14f) else Color.Transparent)
|
||||
.clickable(enabled = !isInstalling, onClick = onClick)
|
||||
.padding(horizontal = 4.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AddonLogo(url = plugin.iconUrl, size = 42.dp, contentDescription = plugin.name)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = plugin.name,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
val subtitle = buildString {
|
||||
if (plugin.description.isNotBlank()) append(plugin.description.take(60))
|
||||
if (plugin.typesLabel.isNotBlank()) {
|
||||
if (isNotEmpty()) append(" · ")
|
||||
append(plugin.typesLabel)
|
||||
}
|
||||
}
|
||||
if (subtitle.isNotBlank()) {
|
||||
Text(text = subtitle, color = Color.White.copy(alpha = 0.45f), fontSize = 11.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(10.dp))
|
||||
if (isInstalling) {
|
||||
CircularProgressIndicator(color = accentColor, strokeWidth = 2.dp, modifier = Modifier.size(20.dp))
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = if (plugin.isInstalled) Icons.Filled.Close else Icons.Filled.Download,
|
||||
contentDescription = null,
|
||||
tint = if (plugin.isInstalled) FluxaColors.errorRed else Color.White.copy(alpha = 0.5f),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddonIconButton(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
|
|
|
|||
|
|
@ -22,10 +22,6 @@ class AddonStoreStore(
|
|||
is AddonStoreAction.AddonMoved -> dataSource.moveAddon(action.url, action.direction)
|
||||
is AddonStoreAction.AddonRefreshed -> dataSource.refreshAddon(action.url)
|
||||
is AddonStoreAction.ConfigureRequested -> Unit
|
||||
is AddonStoreAction.RepoOpened -> dataSource.openRepo(action.url)
|
||||
AddonStoreAction.RepoDialogDismissed -> dataSource.dismissRepoDialog()
|
||||
is AddonStoreAction.RepoRemoved -> dataSource.removeRepo(action.url)
|
||||
is AddonStoreAction.RepoPluginToggled -> dataSource.toggleRepoPlugin(action.repoUrl, action.internalName)
|
||||
AddonStoreAction.AddedAddonDialogDismissed -> dataSource.dismissAddedAddonDialog()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.fluxa.app.ui.settings
|
||||
package com.fluxa.app.shared.feature.plugins
|
||||
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -50,7 +50,6 @@ 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
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.fluxa.app.shared.feature.plugins
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
data class PluginRepositoryUiModel(
|
||||
val manifestUrl: String,
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val scraperCount: Int
|
||||
)
|
||||
|
||||
data class PluginScraperUiModel(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val repositoryUrl: String,
|
||||
val enabled: Boolean,
|
||||
val supportedTypes: List<String>,
|
||||
val hasSettings: Boolean = false,
|
||||
val settings: Map<String, Any?> = emptyMap()
|
||||
)
|
||||
|
||||
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<PluginSettingsOptionUiModel> = emptyList()
|
||||
)
|
||||
|
||||
data class PluginScraperSettingsUiState(
|
||||
val scraper: PluginScraperUiModel,
|
||||
val loading: Boolean,
|
||||
val fields: List<PluginSettingsFieldUiModel>
|
||||
)
|
||||
|
||||
data class CloudstreamRepoUiModel(
|
||||
val name: String,
|
||||
val url: String,
|
||||
val iconUrl: String? = null
|
||||
)
|
||||
|
||||
data class CloudstreamPluginUiModel(
|
||||
val internalName: String,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val iconUrl: String? = null,
|
||||
val typesLabel: String = "",
|
||||
val isInstalled: Boolean = false
|
||||
)
|
||||
|
||||
data class PluginsUiState(
|
||||
val repositories: List<PluginRepositoryUiModel> = emptyList(),
|
||||
val scrapers: List<PluginScraperUiModel> = emptyList(),
|
||||
val addingRepositoryUrl: String? = null,
|
||||
val repositoryError: String? = null,
|
||||
val scraperSettingsSheet: PluginScraperSettingsUiState? = null,
|
||||
val cloudstreamRepos: List<CloudstreamRepoUiModel> = emptyList(),
|
||||
val isAddingCloudstreamRepo: Boolean = false,
|
||||
val cloudstreamRepoError: String? = null,
|
||||
val openRepoUrl: String? = null,
|
||||
val openRepoName: String? = null,
|
||||
val openRepoPlugins: List<CloudstreamPluginUiModel> = emptyList(),
|
||||
val isLoadingRepoPlugins: Boolean = false,
|
||||
val installingPluginKeys: Set<String> = emptySet(),
|
||||
val repoDialogError: String? = null
|
||||
)
|
||||
|
||||
sealed interface PluginsAction {
|
||||
data object Refresh : PluginsAction
|
||||
data class RepositoryAdded(val manifestUrl: String) : PluginsAction
|
||||
data class RepositoryRemoved(val manifestUrl: String) : PluginsAction
|
||||
data class RepositoryRefreshed(val manifestUrl: String) : PluginsAction
|
||||
data class ScraperToggled(val scraperId: String, val enabled: Boolean) : PluginsAction
|
||||
data class ScraperSettingsRequested(val scraperId: String) : PluginsAction
|
||||
data object ScraperSettingsDismissed : PluginsAction
|
||||
data class ScraperSettingsSaved(val scraperId: String, val values: Map<String, Any?>) : PluginsAction
|
||||
data class CloudstreamRepoAdded(val url: String) : PluginsAction
|
||||
data class RepoOpened(val url: String) : PluginsAction
|
||||
data object RepoDialogDismissed : PluginsAction
|
||||
data class RepoRemoved(val url: String) : PluginsAction
|
||||
data class RepoPluginToggled(val repoUrl: String, val internalName: String) : PluginsAction
|
||||
}
|
||||
|
||||
interface PluginsDataSource {
|
||||
fun observePlugins(): Flow<PluginsUiState>
|
||||
suspend fun refresh()
|
||||
suspend fun addRepository(manifestUrl: String)
|
||||
suspend fun removeRepository(manifestUrl: String)
|
||||
suspend fun refreshRepository(manifestUrl: String)
|
||||
suspend fun toggleScraper(scraperId: String, enabled: Boolean)
|
||||
suspend fun requestScraperSettings(scraperId: String)
|
||||
suspend fun dismissScraperSettings()
|
||||
suspend fun saveScraperSettings(scraperId: String, values: Map<String, Any?>)
|
||||
suspend fun addCloudstreamRepository(url: String)
|
||||
suspend fun openRepo(url: String)
|
||||
suspend fun dismissRepoDialog()
|
||||
suspend fun removeRepo(url: String)
|
||||
suspend fun toggleRepoPlugin(repoUrl: String, internalName: String)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.fluxa.app.ui.settings
|
||||
package com.fluxa.app.shared.feature.plugins
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
|
|
@ -17,14 +17,17 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -33,8 +36,8 @@ import androidx.compose.material3.OutlinedTextFieldDefaults
|
|||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -47,57 +50,19 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.fluxa.app.common.AppStrings
|
||||
import com.fluxa.app.plugins.PluginRepositoryUiModel
|
||||
import com.fluxa.app.plugins.PluginScraperUiModel
|
||||
import com.fluxa.app.plugins.PluginsUiState
|
||||
import com.fluxa.app.shared.image.FluxaRemoteImage
|
||||
import com.fluxa.app.ui.catalog.FluxaColors
|
||||
|
||||
@Composable
|
||||
fun PluginsSettingsRoute(
|
||||
language: String?,
|
||||
onBackRequested: () -> Unit,
|
||||
viewModel: PluginsSettingsViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
PluginsSettingsScreen(
|
||||
state = state,
|
||||
language = language,
|
||||
onAddRepository = viewModel::addRepository,
|
||||
onRemoveRepository = viewModel::removeRepository,
|
||||
onRefreshRepository = viewModel::refreshRepository,
|
||||
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
|
||||
fun PluginsSettingsScreen(
|
||||
fun PluginsScreen(
|
||||
state: PluginsUiState,
|
||||
language: String?,
|
||||
onAddRepository: (String) -> Unit,
|
||||
onRemoveRepository: (String) -> Unit,
|
||||
onRefreshRepository: (String) -> Unit,
|
||||
onToggleScraper: (String, Boolean) -> Unit,
|
||||
onOpenScraperSettings: (PluginScraperUiModel) -> Unit,
|
||||
onBackRequested: () -> Unit,
|
||||
onAction: (PluginsAction) -> Unit,
|
||||
onBackRequested: () -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scrapersByRepository = state.scrapers.groupBy { it.repositoryUrl }
|
||||
|
|
@ -137,13 +102,26 @@ fun PluginsSettingsScreen(
|
|||
}
|
||||
|
||||
item {
|
||||
PluginRepositoryInput(language = language, onSubmit = onAddRepository)
|
||||
Text(
|
||||
text = AppStrings.t(language, "settings.plugins.installed_repositories"),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
}
|
||||
|
||||
if (state.error != null) {
|
||||
item {
|
||||
PluginRepositoryInput(
|
||||
language = language,
|
||||
placeholderKey = "settings.plugins.repository_url_placeholder",
|
||||
onSubmit = { url -> onAction(PluginsAction.RepositoryAdded(url)) }
|
||||
)
|
||||
}
|
||||
|
||||
if (state.repositoryError != null) {
|
||||
item {
|
||||
Text(
|
||||
text = AppStrings.format(language, "settings.plugins.error", state.error),
|
||||
text = AppStrings.format(language, "settings.plugins.error", state.repositoryError),
|
||||
color = FluxaColors.errorRed,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
|
|
@ -165,15 +143,6 @@ fun PluginsSettingsScreen(
|
|||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = AppStrings.t(language, "settings.plugins.installed_repositories"),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
}
|
||||
|
||||
if (state.repositories.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
|
|
@ -184,28 +153,105 @@ fun PluginsSettingsScreen(
|
|||
)
|
||||
}
|
||||
} else {
|
||||
items(state.repositories, key = { "repo:${it.manifestUrl}" }) { repository ->
|
||||
items(state.repositories, key = { "plugin-repo:${it.manifestUrl}" }) { repository ->
|
||||
PluginRepositoryGroup(
|
||||
repository = repository,
|
||||
scrapers = scrapersByRepository[repository.manifestUrl].orEmpty(),
|
||||
language = language,
|
||||
isRefreshing = state.addingRepositoryUrl == repository.manifestUrl,
|
||||
onRemove = { onRemoveRepository(repository.manifestUrl) },
|
||||
onRefresh = { onRefreshRepository(repository.manifestUrl) },
|
||||
onToggleScraper = onToggleScraper,
|
||||
onOpenScraperSettings = onOpenScraperSettings
|
||||
onRemove = { onAction(PluginsAction.RepositoryRemoved(repository.manifestUrl)) },
|
||||
onRefresh = { onAction(PluginsAction.RepositoryRefreshed(repository.manifestUrl)) },
|
||||
onToggleScraper = { scraperId, enabled -> onAction(PluginsAction.ScraperToggled(scraperId, enabled)) },
|
||||
onOpenScraperSettings = { scraperId -> onAction(PluginsAction.ScraperSettingsRequested(scraperId)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = AppStrings.t(language, "auto.cloudstream3_repositories"),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
PluginRepositoryInput(
|
||||
language = language,
|
||||
placeholderKey = "addons.cloudstream_repo",
|
||||
onSubmit = { url -> onAction(PluginsAction.CloudstreamRepoAdded(url)) }
|
||||
)
|
||||
}
|
||||
|
||||
if (state.cloudstreamRepoError != null) {
|
||||
item {
|
||||
Text(
|
||||
text = AppStrings.format(language, "settings.plugins.error", state.cloudstreamRepoError),
|
||||
color = FluxaColors.errorRed,
|
||||
fontSize = 13.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isAddingCloudstreamRepo) {
|
||||
item {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
CircularProgressIndicator(color = Color.White.copy(alpha = 0.68f), strokeWidth = 2.dp, modifier = Modifier.size(16.dp))
|
||||
Text(
|
||||
text = AppStrings.t(language, "settings.plugins.adding"),
|
||||
color = Color.White.copy(alpha = 0.6f),
|
||||
fontSize = 13.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.cloudstreamRepos.isNotEmpty()) {
|
||||
items(state.cloudstreamRepos, key = { "cs-repo:${it.url}" }) { repo ->
|
||||
CloudstreamRepoItem(
|
||||
repo = repo,
|
||||
language = language,
|
||||
onRemove = { onAction(PluginsAction.RepoRemoved(repo.url)) },
|
||||
onClick = { onAction(PluginsAction.RepoOpened(repo.url)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(Modifier.height(80.dp)) }
|
||||
}
|
||||
|
||||
if (state.openRepoUrl != null) {
|
||||
CloudstreamRepoPluginsDialog(
|
||||
state = state,
|
||||
language = language,
|
||||
onDismiss = { onAction(PluginsAction.RepoDialogDismissed) },
|
||||
onTogglePlugin = { internalName ->
|
||||
onAction(PluginsAction.RepoPluginToggled(state.openRepoUrl, internalName))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val settingsSheet = state.scraperSettingsSheet
|
||||
if (settingsSheet != null) {
|
||||
PluginScraperSettingsSheet(
|
||||
scraperName = settingsSheet.scraper.name,
|
||||
language = language,
|
||||
loading = settingsSheet.loading,
|
||||
fields = settingsSheet.fields,
|
||||
initialValues = settingsSheet.scraper.settings,
|
||||
onDismiss = { onAction(PluginsAction.ScraperSettingsDismissed) },
|
||||
onSave = { values -> onAction(PluginsAction.ScraperSettingsSaved(settingsSheet.scraper.id, values)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PluginRepositoryInput(
|
||||
language: String?,
|
||||
placeholderKey: String,
|
||||
onSubmit: (String) -> Unit
|
||||
) {
|
||||
var url by remember { mutableStateOf("") }
|
||||
|
|
@ -224,7 +270,7 @@ private fun PluginRepositoryInput(
|
|||
singleLine = true,
|
||||
placeholder = {
|
||||
Text(
|
||||
AppStrings.t(language, "settings.plugins.repository_url_placeholder"),
|
||||
AppStrings.t(language, placeholderKey),
|
||||
color = Color.White.copy(alpha = 0.3f),
|
||||
fontSize = 13.sp
|
||||
)
|
||||
|
|
@ -268,7 +314,7 @@ private fun PluginRepositoryGroup(
|
|||
onRemove: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
onToggleScraper: (String, Boolean) -> Unit,
|
||||
onOpenScraperSettings: (PluginScraperUiModel) -> Unit
|
||||
onOpenScraperSettings: (String) -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -302,9 +348,9 @@ private fun PluginRepositoryGroup(
|
|||
CircularProgressIndicator(color = Color.White.copy(alpha = 0.68f), strokeWidth = 2.dp, modifier = Modifier.size(18.dp))
|
||||
}
|
||||
} else {
|
||||
PluginIconButton(icon = Icons.Filled.Refresh, onClick = onRefresh)
|
||||
PluginsIconButton(icon = Icons.Filled.Refresh, onClick = onRefresh)
|
||||
}
|
||||
PluginIconButton(icon = Icons.Filled.Close, tint = Color.White.copy(alpha = 0.5f), onClick = onRemove)
|
||||
PluginsIconButton(icon = Icons.Filled.Close, tint = Color.White.copy(alpha = 0.5f), onClick = onRemove)
|
||||
}
|
||||
|
||||
if (scrapers.isNotEmpty()) {
|
||||
|
|
@ -314,7 +360,7 @@ private fun PluginRepositoryGroup(
|
|||
scraper = scraper,
|
||||
language = language,
|
||||
onToggle = { enabled -> onToggleScraper(scraper.id, enabled) },
|
||||
onOpenSettings = { onOpenScraperSettings(scraper) }
|
||||
onOpenSettings = { onOpenScraperSettings(scraper.id) }
|
||||
)
|
||||
if (index < scrapers.lastIndex) {
|
||||
HorizontalDivider(color = Color.White.copy(alpha = 0.06f), thickness = 0.5.dp, modifier = Modifier.padding(horizontal = 16.dp))
|
||||
|
|
@ -351,7 +397,7 @@ private fun PluginScraperRow(
|
|||
)
|
||||
}
|
||||
if (scraper.hasSettings) {
|
||||
PluginIconButton(
|
||||
PluginsIconButton(
|
||||
icon = Icons.Filled.Settings,
|
||||
contentDescription = AppStrings.t(language, "settings.plugins.settings"),
|
||||
onClick = onOpenSettings
|
||||
|
|
@ -372,9 +418,173 @@ private fun PluginScraperRow(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun PluginIconButton(
|
||||
private fun CloudstreamRepoItem(
|
||||
repo: CloudstreamRepoUiModel,
|
||||
language: String?,
|
||||
onRemove: () -> Unit,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.onFocusChanged { focused = it.isFocused }
|
||||
.background(if (focused) Color.White.copy(alpha = 0.14f) else Color.White.copy(alpha = 0.04f))
|
||||
.border(if (focused) 2.dp else 1.dp, if (focused) Color.White else Color.White.copy(alpha = 0.08f), RoundedCornerShape(14.dp))
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp)
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
PluginsLogo(url = repo.iconUrl, size = 36.dp, contentDescription = repo.name)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = repo.name.ifBlank { AppStrings.t(language, "addons.cloudstream_repo") },
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 14.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = repo.url,
|
||||
color = Color.White.copy(alpha = 0.5f),
|
||||
fontSize = 11.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
PluginsIconButton(icon = Icons.Filled.Close, tint = Color.White.copy(alpha = 0.5f), onClick = onRemove)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CloudstreamRepoPluginsDialog(
|
||||
state: PluginsUiState,
|
||||
language: String?,
|
||||
onDismiss: () -> Unit,
|
||||
onTogglePlugin: (String) -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
text = state.openRepoName ?: AppStrings.t(language, "auto.repository_plugins"),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
text = {
|
||||
LazyColumn(modifier = Modifier.height(400.dp)) {
|
||||
if (state.isLoadingRepoPlugins) {
|
||||
item {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||
CircularProgressIndicator(color = Color.White, modifier = Modifier.size(32.dp))
|
||||
}
|
||||
}
|
||||
} else if (state.openRepoPlugins.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = AppStrings.t(language, "auto.no_plugins_found_in_this_repository"),
|
||||
color = Color.White.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val error = state.repoDialogError
|
||||
if (error != null) {
|
||||
item {
|
||||
Text(
|
||||
text = error,
|
||||
color = FluxaColors.errorRed,
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(state.openRepoPlugins, key = { _, p -> p.internalName }) { index, plugin ->
|
||||
val installing = plugin.internalName in state.installingPluginKeys
|
||||
CloudstreamPluginItem(
|
||||
plugin = plugin,
|
||||
isInstalling = installing,
|
||||
onClick = { if (!installing) onTogglePlugin(plugin.internalName) }
|
||||
)
|
||||
if (index < state.openRepoPlugins.lastIndex) {
|
||||
HorizontalDivider(color = Color.White.copy(alpha = 0.07f), thickness = 0.5.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = AppStrings.t(language, "auto.close"), color = Color.White.copy(alpha = 0.7f))
|
||||
}
|
||||
},
|
||||
containerColor = Color(0xFF1A1D26)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CloudstreamPluginItem(
|
||||
plugin: CloudstreamPluginUiModel,
|
||||
isInstalling: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val accentColor = if (plugin.isInstalled) FluxaColors.successGreen else Color.White.copy(alpha = 0.6f)
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.onFocusChanged { focused = it.isFocused }
|
||||
.background(if (focused) Color.White.copy(alpha = 0.14f) else Color.Transparent)
|
||||
.clickable(enabled = !isInstalling, onClick = onClick)
|
||||
.padding(horizontal = 4.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
PluginsLogo(url = plugin.iconUrl, size = 42.dp, contentDescription = plugin.name)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = plugin.name,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
val subtitle = buildString {
|
||||
if (plugin.description.isNotBlank()) append(plugin.description.take(60))
|
||||
if (plugin.typesLabel.isNotBlank()) {
|
||||
if (isNotEmpty()) append(" · ")
|
||||
append(plugin.typesLabel)
|
||||
}
|
||||
}
|
||||
if (subtitle.isNotBlank()) {
|
||||
Text(text = subtitle, color = Color.White.copy(alpha = 0.45f), fontSize = 11.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(10.dp))
|
||||
if (isInstalling) {
|
||||
CircularProgressIndicator(color = accentColor, strokeWidth = 2.dp, modifier = Modifier.size(20.dp))
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = if (plugin.isInstalled) Icons.Filled.Close else Icons.Filled.Download,
|
||||
contentDescription = null,
|
||||
tint = if (plugin.isInstalled) FluxaColors.errorRed else Color.White.copy(alpha = 0.5f),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PluginsIconButton(
|
||||
icon: ImageVector,
|
||||
contentDescription: String? = null,
|
||||
enabled: Boolean = true,
|
||||
tint: Color = Color.White.copy(alpha = 0.68f),
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
|
|
@ -385,14 +595,27 @@ private fun PluginIconButton(
|
|||
.clip(CircleShape)
|
||||
.onFocusChanged { focused = it.isFocused }
|
||||
.background(if (focused) Color.White else Color.Transparent)
|
||||
.clickable(onClick = onClick),
|
||||
.clickable(enabled = enabled, onClick = onClick),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = if (focused) Color.Black else tint,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Icon(icon, contentDescription = contentDescription, tint = if (focused) Color.Black else tint, modifier = Modifier.size(18.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PluginsLogo(url: String?, size: Dp, contentDescription: String?) {
|
||||
Box(
|
||||
modifier = Modifier.size(size).clip(RoundedCornerShape(12.dp)).background(Color.White.copy(alpha = 0.08f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (!url.isNullOrBlank()) {
|
||||
FluxaRemoteImage(
|
||||
imageUrl = url,
|
||||
cacheKey = "plugin-logo:$url",
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(size * 0.82f).clip(RoundedCornerShape(8.dp)),
|
||||
contentScale = androidx.compose.ui.layout.ContentScale.Fit
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.fluxa.app.shared.feature.plugins
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class PluginsStore(
|
||||
private val dataSource: PluginsDataSource,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
val state: StateFlow<PluginsUiState> = dataSource.observePlugins()
|
||||
.stateIn(scope, SharingStarted.WhileSubscribed(5_000), PluginsUiState())
|
||||
|
||||
suspend fun dispatch(action: PluginsAction) {
|
||||
when (action) {
|
||||
PluginsAction.Refresh -> dataSource.refresh()
|
||||
is PluginsAction.RepositoryAdded -> dataSource.addRepository(action.manifestUrl)
|
||||
is PluginsAction.RepositoryRemoved -> dataSource.removeRepository(action.manifestUrl)
|
||||
is PluginsAction.RepositoryRefreshed -> dataSource.refreshRepository(action.manifestUrl)
|
||||
is PluginsAction.ScraperToggled -> dataSource.toggleScraper(action.scraperId, action.enabled)
|
||||
is PluginsAction.ScraperSettingsRequested -> dataSource.requestScraperSettings(action.scraperId)
|
||||
PluginsAction.ScraperSettingsDismissed -> dataSource.dismissScraperSettings()
|
||||
is PluginsAction.ScraperSettingsSaved -> dataSource.saveScraperSettings(action.scraperId, action.values)
|
||||
is PluginsAction.CloudstreamRepoAdded -> dataSource.addCloudstreamRepository(action.url)
|
||||
is PluginsAction.RepoOpened -> dataSource.openRepo(action.url)
|
||||
PluginsAction.RepoDialogDismissed -> dataSource.dismissRepoDialog()
|
||||
is PluginsAction.RepoRemoved -> dataSource.removeRepo(action.url)
|
||||
is PluginsAction.RepoPluginToggled -> dataSource.toggleRepoPlugin(action.repoUrl, action.internalName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import com.fluxa.app.shared.feature.calendar.CalendarDataSource
|
|||
import com.fluxa.app.shared.feature.detail.DetailDataSource
|
||||
import com.fluxa.app.shared.feature.discover.DiscoverDataSource
|
||||
import com.fluxa.app.shared.feature.library.LibraryDataSource
|
||||
import com.fluxa.app.shared.feature.plugins.PluginsDataSource
|
||||
import com.fluxa.app.shared.feature.profile.ProfileDataSource
|
||||
import com.fluxa.app.shared.feature.search.SearchDataSource
|
||||
import com.fluxa.app.shared.feature.settings.SettingsDataSource
|
||||
|
|
@ -43,6 +44,10 @@ interface FluxaAddonStoreServices {
|
|||
val addonStoreDataSource: AddonStoreDataSource
|
||||
}
|
||||
|
||||
interface FluxaPluginsServices {
|
||||
val pluginsDataSource: PluginsDataSource
|
||||
}
|
||||
|
||||
interface FluxaAuthServices {
|
||||
val authDataSource: AuthDataSource
|
||||
}
|
||||
|
|
@ -60,5 +65,6 @@ interface FluxaMobilePlatformServices :
|
|||
FluxaDetailServices,
|
||||
FluxaProfileServices,
|
||||
FluxaAddonStoreServices,
|
||||
FluxaPluginsServices,
|
||||
FluxaAuthServices,
|
||||
FluxaSettingsServices
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ 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.ApplePluginsDataSource
|
||||
import com.fluxa.app.shared.platform.AppleAuthDataSource
|
||||
import com.fluxa.app.shared.platform.AppleCatalogHomeDataSource
|
||||
import com.fluxa.app.shared.platform.AppleCalendarDataSource
|
||||
|
|
@ -45,6 +46,7 @@ object FluxaApple {
|
|||
internal val profileDataSource = AppleProfileDataSource(watchlistStore::setActiveProfile)
|
||||
internal val settingsDataSource = AppleSettingsDataSource()
|
||||
internal val addonStoreDataSource = AppleAddonStoreDataSource()
|
||||
internal val pluginsDataSource = ApplePluginsDataSource()
|
||||
internal val authDataSource = AppleAuthDataSource()
|
||||
internal val platformServices = AppleFluxaPlatformServices(
|
||||
catalogHomeDataSource,
|
||||
|
|
@ -56,6 +58,7 @@ object FluxaApple {
|
|||
profileDataSource,
|
||||
settingsDataSource,
|
||||
addonStoreDataSource,
|
||||
pluginsDataSource,
|
||||
authDataSource
|
||||
)
|
||||
private var onPlaybackRequested: (ApplePlaybackRequestSnapshot) -> Unit = {}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@ package com.fluxa.app.shared.platform
|
|||
import com.fluxa.app.shared.feature.addonstore.AddonStoreDataSource
|
||||
import com.fluxa.app.shared.feature.addonstore.AddonStoreInputType
|
||||
import com.fluxa.app.shared.feature.addonstore.AddonStoreUiState
|
||||
import com.fluxa.app.shared.feature.addonstore.CloudstreamPluginUiModel
|
||||
import com.fluxa.app.shared.feature.addonstore.CloudstreamRepoUiModel
|
||||
import com.fluxa.app.shared.feature.addonstore.InstalledAddonUiModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -15,9 +12,7 @@ data class AppleAddonStoreActionSnapshot(
|
|||
val url: String? = null,
|
||||
val text: String? = null,
|
||||
val enabled: Boolean? = null,
|
||||
val direction: Int? = null,
|
||||
val repoUrl: String? = null,
|
||||
val internalName: String? = null
|
||||
val direction: Int? = null
|
||||
)
|
||||
|
||||
class AppleAddonStoreDataSource : AddonStoreDataSource {
|
||||
|
|
@ -31,7 +26,6 @@ class AppleAddonStoreDataSource : AddonStoreDataSource {
|
|||
return when {
|
||||
trimmed.isEmpty() -> AddonStoreInputType.UNKNOWN
|
||||
trimmed.endsWith("manifest.json") || trimmed.startsWith("stremio://") -> AddonStoreInputType.STREMIO_MANIFEST
|
||||
trimmed.endsWith(".json") && trimmed.startsWith("http") -> AddonStoreInputType.CLOUDSTREAM_REPO
|
||||
trimmed.startsWith("http://") || trimmed.startsWith("https://") -> AddonStoreInputType.STREMIO_MANIFEST
|
||||
else -> AddonStoreInputType.SEARCH_QUERY
|
||||
}
|
||||
|
|
@ -67,23 +61,6 @@ class AppleAddonStoreDataSource : AddonStoreDataSource {
|
|||
postAction(AppleAddonStoreActionSnapshot(type = "refreshAddon", url = url))
|
||||
}
|
||||
|
||||
override suspend fun openRepo(url: String) {
|
||||
state.value = state.value.copy(openRepoUrl = url, isLoadingRepoPlugins = true)
|
||||
postAction(AppleAddonStoreActionSnapshot(type = "openRepo", url = url))
|
||||
}
|
||||
|
||||
override suspend fun dismissRepoDialog() {
|
||||
state.value = state.value.copy(openRepoUrl = null, openRepoName = null, openRepoPlugins = emptyList(), repoDialogError = null)
|
||||
}
|
||||
|
||||
override suspend fun removeRepo(url: String) {
|
||||
postAction(AppleAddonStoreActionSnapshot(type = "removeRepo", url = url))
|
||||
}
|
||||
|
||||
override suspend fun toggleRepoPlugin(repoUrl: String, internalName: String) {
|
||||
postAction(AppleAddonStoreActionSnapshot(type = "toggleRepoPlugin", repoUrl = repoUrl, internalName = internalName))
|
||||
}
|
||||
|
||||
override suspend fun dismissAddedAddonDialog() {
|
||||
state.value = state.value.copy(addedAddonName = null)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ class AppleFluxaPlatformServices(
|
|||
override val profileDataSource: AppleProfileDataSource,
|
||||
override val settingsDataSource: AppleSettingsDataSource,
|
||||
override val addonStoreDataSource: AppleAddonStoreDataSource,
|
||||
override val pluginsDataSource: ApplePluginsDataSource,
|
||||
override val authDataSource: AppleAuthDataSource
|
||||
) : FluxaPlatformServices, FluxaDetailServices, FluxaSearchServices, FluxaDiscoverServices, FluxaCalendarServices,
|
||||
FluxaLibraryServices, FluxaProfileServices, FluxaSettingsServices, FluxaAddonStoreServices, FluxaAuthServices
|
||||
FluxaLibraryServices, FluxaProfileServices, FluxaSettingsServices, FluxaAddonStoreServices, FluxaPluginsServices,
|
||||
FluxaAuthServices
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
package com.fluxa.app.shared.platform
|
||||
|
||||
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
|
||||
|
||||
data class ApplePluginsActionSnapshot(
|
||||
val type: String,
|
||||
val manifestUrl: String? = null,
|
||||
val scraperId: String? = null,
|
||||
val enabled: Boolean? = null,
|
||||
val settingsJson: String? = null,
|
||||
val url: String? = null,
|
||||
val repoUrl: String? = null,
|
||||
val internalName: String? = null
|
||||
)
|
||||
|
||||
class ApplePluginsDataSource : PluginsDataSource {
|
||||
private val state = MutableStateFlow(PluginsUiState())
|
||||
private var onActionRequested: (ApplePluginsActionSnapshot) -> Unit = {}
|
||||
|
||||
override fun observePlugins(): Flow<PluginsUiState> = state.asStateFlow()
|
||||
|
||||
override suspend fun refresh() {
|
||||
postAction(ApplePluginsActionSnapshot(type = "refresh"))
|
||||
}
|
||||
|
||||
override suspend fun addRepository(manifestUrl: String) {
|
||||
postAction(ApplePluginsActionSnapshot(type = "addRepository", manifestUrl = manifestUrl))
|
||||
}
|
||||
|
||||
override suspend fun removeRepository(manifestUrl: String) {
|
||||
postAction(ApplePluginsActionSnapshot(type = "removeRepository", manifestUrl = manifestUrl))
|
||||
}
|
||||
|
||||
override suspend fun refreshRepository(manifestUrl: String) {
|
||||
postAction(ApplePluginsActionSnapshot(type = "refreshRepository", manifestUrl = manifestUrl))
|
||||
}
|
||||
|
||||
override suspend fun toggleScraper(scraperId: String, enabled: Boolean) {
|
||||
postAction(ApplePluginsActionSnapshot(type = "toggleScraper", scraperId = scraperId, enabled = enabled))
|
||||
}
|
||||
|
||||
override suspend fun requestScraperSettings(scraperId: String) {
|
||||
postAction(ApplePluginsActionSnapshot(type = "requestScraperSettings", scraperId = scraperId))
|
||||
}
|
||||
|
||||
override suspend fun dismissScraperSettings() {
|
||||
state.value = state.value.copy(scraperSettingsSheet = null)
|
||||
}
|
||||
|
||||
override suspend fun saveScraperSettings(scraperId: String, values: Map<String, Any?>) {
|
||||
postAction(ApplePluginsActionSnapshot(type = "saveScraperSettings", scraperId = scraperId))
|
||||
}
|
||||
|
||||
override suspend fun addCloudstreamRepository(url: String) {
|
||||
postAction(ApplePluginsActionSnapshot(type = "addCloudstreamRepository", url = url))
|
||||
}
|
||||
|
||||
override suspend fun openRepo(url: String) {
|
||||
state.value = state.value.copy(openRepoUrl = url, isLoadingRepoPlugins = true)
|
||||
postAction(ApplePluginsActionSnapshot(type = "openRepo", url = url))
|
||||
}
|
||||
|
||||
override suspend fun dismissRepoDialog() {
|
||||
state.value = state.value.copy(openRepoUrl = null, openRepoName = null, openRepoPlugins = emptyList(), repoDialogError = null)
|
||||
}
|
||||
|
||||
override suspend fun removeRepo(url: String) {
|
||||
postAction(ApplePluginsActionSnapshot(type = "removeRepo", url = url))
|
||||
}
|
||||
|
||||
override suspend fun toggleRepoPlugin(repoUrl: String, internalName: String) {
|
||||
postAction(ApplePluginsActionSnapshot(type = "toggleRepoPlugin", repoUrl = repoUrl, internalName = internalName))
|
||||
}
|
||||
|
||||
fun setOnActionRequested(handler: (ApplePluginsActionSnapshot) -> Unit) {
|
||||
onActionRequested = handler
|
||||
}
|
||||
|
||||
private fun postAction(action: ApplePluginsActionSnapshot) {
|
||||
onActionRequested(action)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue