flavouring yt extraction

This commit is contained in:
tapframe 2026-04-04 21:30:19 +05:30
parent 49a178c7f9
commit f65f934acd
20 changed files with 225 additions and 84 deletions

View file

@ -210,6 +210,15 @@ android {
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
} }
flavorDimensions += "distribution"
productFlavors {
create("full") {
dimension = "distribution"
}
create("playstore") {
dimension = "distribution"
}
}
packaging { packaging {
resources { resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"

View file

@ -0,0 +1,9 @@
package com.nuvio.app.features.player
import androidx.media3.datasource.DataSource
import com.nuvio.app.features.trailer.YoutubeChunkedDataSourceFactory
internal object PlatformPlaybackDataSourceFactory {
fun create(defaultRequestHeaders: Map<String, String>): DataSource.Factory =
YoutubeChunkedDataSourceFactory(defaultRequestHeaders = defaultRequestHeaders)
}

View file

@ -41,7 +41,6 @@ import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import androidx.media3.ui.SubtitleView import androidx.media3.ui.SubtitleView
import androidx.media3.ui.CaptionStyleCompat import androidx.media3.ui.CaptionStyleCompat
import com.nuvio.app.features.trailer.YoutubeChunkedDataSourceFactory
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -112,7 +111,7 @@ actual fun PlatformPlayerSurface(
.setTsExtractorTimestampSearchBytes(1500 * TsExtractor.TS_PACKET_SIZE) .setTsExtractorTimestampSearchBytes(1500 * TsExtractor.TS_PACKET_SIZE)
val mediaSourceFactory = DefaultMediaSourceFactory( val mediaSourceFactory = DefaultMediaSourceFactory(
YoutubeChunkedDataSourceFactory(defaultRequestHeaders = sanitizedSourceHeaders), PlatformPlaybackDataSourceFactory.create(defaultRequestHeaders = sanitizedSourceHeaders),
extractorsFactory, extractorsFactory,
) )

View file

@ -0,0 +1,9 @@
package com.nuvio.app.features.player
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultHttpDataSource
internal object PlatformPlaybackDataSourceFactory {
fun create(defaultRequestHeaders: Map<String, String>): DataSource.Factory =
DefaultHttpDataSource.Factory().setDefaultRequestProperties(defaultRequestHeaders)
}

View file

@ -0,0 +1,5 @@
package com.nuvio.app.features.trailer
actual object TrailerPlaybackResolver {
actual suspend fun resolveFromYouTubeUrl(youtubeUrl: String): TrailerPlaybackSource? = null
}

View file

@ -66,6 +66,7 @@ import coil3.ImageLoader
import coil3.compose.setSingletonImageLoaderFactory import coil3.compose.setSingletonImageLoaderFactory
import coil3.request.CachePolicy import coil3.request.CachePolicy
import coil3.request.crossfade import coil3.request.crossfade
import com.nuvio.app.core.build.AppFeaturePolicy
import com.nuvio.app.core.auth.AuthRepository import com.nuvio.app.core.auth.AuthRepository
import com.nuvio.app.core.auth.AuthState import com.nuvio.app.core.auth.AuthState
import com.nuvio.app.core.deeplink.AppDeepLink import com.nuvio.app.core.deeplink.AppDeepLink
@ -609,7 +610,11 @@ private fun MainAppContent(
onMetaScreenSettingsClick = { navController.navigate(MetaScreenSettingsRoute) }, onMetaScreenSettingsClick = { navController.navigate(MetaScreenSettingsRoute) },
onContinueWatchingSettingsClick = { navController.navigate(ContinueWatchingSettingsRoute) }, onContinueWatchingSettingsClick = { navController.navigate(ContinueWatchingSettingsRoute) },
onAddonsSettingsClick = { navController.navigate(AddonsSettingsRoute) }, onAddonsSettingsClick = { navController.navigate(AddonsSettingsRoute) },
onPluginsSettingsClick = { navController.navigate(PluginsSettingsRoute) }, onPluginsSettingsClick = {
if (AppFeaturePolicy.pluginsEnabled) {
navController.navigate(PluginsSettingsRoute)
}
},
onAccountSettingsClick = { navController.navigate(AccountSettingsRoute) }, onAccountSettingsClick = { navController.navigate(AccountSettingsRoute) },
onInitialHomeContentRendered = { initialHomeReady = true }, onInitialHomeContentRendered = { initialHomeReady = true },
) )
@ -1084,10 +1089,12 @@ private fun MainAppContent(
onBack = { navController.popBackStack() }, onBack = { navController.popBackStack() },
) )
} }
composable<PluginsSettingsRoute> { if (AppFeaturePolicy.pluginsEnabled) {
PluginsSettingsScreen( composable<PluginsSettingsRoute> {
onBack = { navController.popBackStack() }, PluginsSettingsScreen(
) onBack = { navController.popBackStack() },
)
}
} }
composable<AccountSettingsRoute> { composable<AccountSettingsRoute> {
AccountSettingsScreen( AccountSettingsScreen(

View file

@ -1,5 +1,6 @@
package com.nuvio.app.core.storage package com.nuvio.app.core.storage
import com.nuvio.app.core.build.AppFeaturePolicy
import com.nuvio.app.features.addons.AddonRepository import com.nuvio.app.features.addons.AddonRepository
import com.nuvio.app.features.catalog.CatalogRepository import com.nuvio.app.features.catalog.CatalogRepository
import com.nuvio.app.features.details.MetaDetailsRepository import com.nuvio.app.features.details.MetaDetailsRepository
@ -28,7 +29,9 @@ internal object LocalAccountDataCleaner {
ProfileRepository.clearInMemory() ProfileRepository.clearInMemory()
AddonRepository.clearLocalState() AddonRepository.clearLocalState()
PluginRepository.clearLocalState() if (AppFeaturePolicy.pluginsEnabled) {
PluginRepository.clearLocalState()
}
HomeRepository.clear() HomeRepository.clear()
HomeCatalogSettingsRepository.clearLocalState() HomeCatalogSettingsRepository.clearLocalState()
MetaScreenSettingsRepository.clearLocalState() MetaScreenSettingsRepository.clearLocalState()

View file

@ -1,6 +1,7 @@
package com.nuvio.app.core.sync package com.nuvio.app.core.sync
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.nuvio.app.core.build.AppFeaturePolicy
import com.nuvio.app.core.auth.AuthRepository import com.nuvio.app.core.auth.AuthRepository
import com.nuvio.app.core.auth.AuthState import com.nuvio.app.core.auth.AuthState
import com.nuvio.app.features.addons.AddonRepository import com.nuvio.app.features.addons.AddonRepository
@ -31,10 +32,12 @@ object SyncManager {
.onSuccess { log.i { "pullAllForProfile — addons pull completed" } } .onSuccess { log.i { "pullAllForProfile — addons pull completed" } }
.onFailure { log.e(it) { "Addon pull failed" } } .onFailure { log.e(it) { "Addon pull failed" } }
log.i { "pullAllForProfile — pulling plugins (await)..." } if (AppFeaturePolicy.pluginsEnabled) {
runCatching { PluginRepository.pullFromServer(profileId) } log.i { "pullAllForProfile — pulling plugins (await)..." }
.onSuccess { log.i { "pullAllForProfile — plugins pull completed" } } runCatching { PluginRepository.pullFromServer(profileId) }
.onFailure { log.e(it) { "Plugin pull failed" } } .onSuccess { log.i { "pullAllForProfile — plugins pull completed" } }
.onFailure { log.e(it) { "Plugin pull failed" } }
}
log.i { "pullAllForProfile — launching remaining pulls in parallel" } log.i { "pullAllForProfile — launching remaining pulls in parallel" }
launch { launch {

View file

@ -44,12 +44,15 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.nuvio.app.core.build.AppFeaturePolicy
import com.nuvio.app.core.build.TrailerPlaybackMode
import com.nuvio.app.core.ui.NuvioBackButton import com.nuvio.app.core.ui.NuvioBackButton
import com.nuvio.app.core.ui.TraktListPickerDialog import com.nuvio.app.core.ui.TraktListPickerDialog
import com.nuvio.app.core.ui.nuvioPlatformExtraBottomPadding import com.nuvio.app.core.ui.nuvioPlatformExtraBottomPadding
@ -295,37 +298,43 @@ fun MetaDetailsScreen(
val hasTrailersSection = remember(meta) { val hasTrailersSection = remember(meta) {
meta.trailers.isNotEmpty() meta.trailers.isNotEmpty()
} }
val uriHandler = LocalUriHandler.current
val inAppTrailerPlaybackEnabled = AppFeaturePolicy.trailerPlaybackMode == TrailerPlaybackMode.IN_APP
val trailerScope = rememberCoroutineScope() val trailerScope = rememberCoroutineScope()
var selectedTrailer by remember(meta.id) { mutableStateOf<MetaTrailer?>(null) } var selectedTrailer by remember(meta.id) { mutableStateOf<MetaTrailer?>(null) }
var trailerPlaybackSource by remember(meta.id) { mutableStateOf<TrailerPlaybackSource?>(null) } var trailerPlaybackSource by remember(meta.id) { mutableStateOf<TrailerPlaybackSource?>(null) }
var trailerLoading by remember(meta.id) { mutableStateOf(false) } var trailerLoading by remember(meta.id) { mutableStateOf(false) }
var trailerErrorMessage by remember(meta.id) { mutableStateOf<String?>(null) } var trailerErrorMessage by remember(meta.id) { mutableStateOf<String?>(null) }
var trailerRequestToken by remember(meta.id) { mutableIntStateOf(0) } var trailerRequestToken by remember(meta.id) { mutableIntStateOf(0) }
val resolveTrailer: (MetaTrailer) -> Unit = remember(meta.id) { val resolveTrailer: (MetaTrailer) -> Unit = remember(meta.id, inAppTrailerPlaybackEnabled, uriHandler) {
{ trailer -> { trailer ->
selectedTrailer = trailer val youtubeUrl = trailer.key.takeIf {
trailerPlaybackSource = null it.startsWith("http://") || it.startsWith("https://")
trailerErrorMessage = null } ?: "https://www.youtube.com/watch?v=${trailer.key}"
trailerLoading = true if (!inAppTrailerPlaybackEnabled) {
trailerRequestToken += 1 runCatching { uriHandler.openUri(youtubeUrl) }
val currentRequestToken = trailerRequestToken } else {
trailerScope.launch { selectedTrailer = trailer
val youtubeUrl = trailer.key.takeIf { trailerPlaybackSource = null
it.startsWith("http://") || it.startsWith("https://") trailerErrorMessage = null
} ?: "https://www.youtube.com/watch?v=${trailer.key}" trailerLoading = true
val resolvedSource = runCatching { trailerRequestToken += 1
TrailerPlaybackResolver.resolveFromYouTubeUrl(youtubeUrl) val currentRequestToken = trailerRequestToken
}.getOrNull() trailerScope.launch {
if (currentRequestToken != trailerRequestToken) { val resolvedSource = runCatching {
return@launch TrailerPlaybackResolver.resolveFromYouTubeUrl(youtubeUrl)
}.getOrNull()
if (currentRequestToken != trailerRequestToken) {
return@launch
}
trailerPlaybackSource = resolvedSource
trailerErrorMessage = if (resolvedSource == null) {
"No playable trailer stream found."
} else {
null
}
trailerLoading = false
} }
trailerPlaybackSource = resolvedSource
trailerErrorMessage = if (resolvedSource == null) {
"No playable trailer stream found."
} else {
null
}
trailerLoading = false
} }
} }
} }
@ -654,25 +663,27 @@ fun MetaDetailsScreen(
) )
} }
TrailerPlayerPopup( if (inAppTrailerPlaybackEnabled) {
visible = selectedTrailer != null, TrailerPlayerPopup(
trailerTitle = selectedTrailer?.displayName ?: selectedTrailer?.name.orEmpty(), visible = selectedTrailer != null,
trailerType = selectedTrailer?.type.orEmpty(), trailerTitle = selectedTrailer?.displayName ?: selectedTrailer?.name.orEmpty(),
contentTitle = meta.name, trailerType = selectedTrailer?.type.orEmpty(),
playbackSource = trailerPlaybackSource, contentTitle = meta.name,
isLoading = trailerLoading, playbackSource = trailerPlaybackSource,
errorMessage = trailerErrorMessage, isLoading = trailerLoading,
onDismiss = { errorMessage = trailerErrorMessage,
trailerRequestToken += 1 onDismiss = {
trailerLoading = false trailerRequestToken += 1
trailerPlaybackSource = null trailerLoading = false
trailerErrorMessage = null trailerPlaybackSource = null
selectedTrailer = null trailerErrorMessage = null
}, selectedTrailer = null
onRetry = selectedTrailer?.let { trailer -> },
{ resolveTrailer(trailer) } onRetry = selectedTrailer?.let { trailer ->
}, { resolveTrailer(trailer) }
) },
)
}
TraktListPickerDialog( TraktListPickerDialog(
visible = showLibraryListPicker, visible = showLibraryListPicker,

View file

@ -1,5 +1,6 @@
package com.nuvio.app.features.player package com.nuvio.app.features.player
import com.nuvio.app.core.build.AppFeaturePolicy
import com.nuvio.app.features.player.skip.NextEpisodeThresholdMode import com.nuvio.app.features.player.skip.NextEpisodeThresholdMode
import com.nuvio.app.features.streams.StreamAutoPlayMode import com.nuvio.app.features.streams.StreamAutoPlayMode
import com.nuvio.app.features.streams.StreamAutoPlaySource import com.nuvio.app.features.streams.StreamAutoPlaySource
@ -141,6 +142,17 @@ object PlayerSettingsRepository {
?: StreamAutoPlaySource.ALL_SOURCES ?: StreamAutoPlaySource.ALL_SOURCES
streamAutoPlaySelectedAddons = PlayerSettingsStorage.loadStreamAutoPlaySelectedAddons() ?: emptySet() streamAutoPlaySelectedAddons = PlayerSettingsStorage.loadStreamAutoPlaySelectedAddons() ?: emptySet()
streamAutoPlaySelectedPlugins = PlayerSettingsStorage.loadStreamAutoPlaySelectedPlugins() ?: emptySet() streamAutoPlaySelectedPlugins = PlayerSettingsStorage.loadStreamAutoPlaySelectedPlugins() ?: emptySet()
if (!AppFeaturePolicy.pluginsEnabled) {
val normalizedSource = normalizeStreamAutoPlaySource(streamAutoPlaySource)
if (normalizedSource != streamAutoPlaySource) {
streamAutoPlaySource = normalizedSource
PlayerSettingsStorage.saveStreamAutoPlaySource(normalizedSource.name)
}
if (streamAutoPlaySelectedPlugins.isNotEmpty()) {
streamAutoPlaySelectedPlugins = emptySet()
PlayerSettingsStorage.saveStreamAutoPlaySelectedPlugins(emptySet())
}
}
streamAutoPlayRegex = PlayerSettingsStorage.loadStreamAutoPlayRegex() ?: "" streamAutoPlayRegex = PlayerSettingsStorage.loadStreamAutoPlayRegex() ?: ""
streamAutoPlayTimeoutSeconds = PlayerSettingsStorage.loadStreamAutoPlayTimeoutSeconds() ?: 3 streamAutoPlayTimeoutSeconds = PlayerSettingsStorage.loadStreamAutoPlayTimeoutSeconds() ?: 3
skipIntroEnabled = PlayerSettingsStorage.loadSkipIntroEnabled() ?: true skipIntroEnabled = PlayerSettingsStorage.loadSkipIntroEnabled() ?: true
@ -261,10 +273,11 @@ object PlayerSettingsRepository {
fun setStreamAutoPlaySource(source: StreamAutoPlaySource) { fun setStreamAutoPlaySource(source: StreamAutoPlaySource) {
ensureLoaded() ensureLoaded()
if (streamAutoPlaySource == source) return val normalizedSource = normalizeStreamAutoPlaySource(source)
streamAutoPlaySource = source if (streamAutoPlaySource == normalizedSource) return
streamAutoPlaySource = normalizedSource
publish() publish()
PlayerSettingsStorage.saveStreamAutoPlaySource(source.name) PlayerSettingsStorage.saveStreamAutoPlaySource(normalizedSource.name)
} }
fun setStreamAutoPlaySelectedAddons(addons: Set<String>) { fun setStreamAutoPlaySelectedAddons(addons: Set<String>) {
@ -277,10 +290,11 @@ object PlayerSettingsRepository {
fun setStreamAutoPlaySelectedPlugins(plugins: Set<String>) { fun setStreamAutoPlaySelectedPlugins(plugins: Set<String>) {
ensureLoaded() ensureLoaded()
if (streamAutoPlaySelectedPlugins == plugins) return val normalizedPlugins = if (AppFeaturePolicy.pluginsEnabled) plugins else emptySet()
streamAutoPlaySelectedPlugins = plugins if (streamAutoPlaySelectedPlugins == normalizedPlugins) return
streamAutoPlaySelectedPlugins = normalizedPlugins
publish() publish()
PlayerSettingsStorage.saveStreamAutoPlaySelectedPlugins(plugins) PlayerSettingsStorage.saveStreamAutoPlaySelectedPlugins(normalizedPlugins)
} }
fun setStreamAutoPlayRegex(regex: String) { fun setStreamAutoPlayRegex(regex: String) {
@ -392,4 +406,12 @@ object PlayerSettingsRepository {
nextEpisodeThresholdMinutesBeforeEnd = nextEpisodeThresholdMinutesBeforeEnd, nextEpisodeThresholdMinutesBeforeEnd = nextEpisodeThresholdMinutesBeforeEnd,
) )
} }
private fun normalizeStreamAutoPlaySource(source: StreamAutoPlaySource): StreamAutoPlaySource {
return if (!AppFeaturePolicy.pluginsEnabled && source == StreamAutoPlaySource.ENABLED_PLUGINS_ONLY) {
StreamAutoPlaySource.ALL_SOURCES
} else {
source
}
}
} }

View file

@ -1,6 +1,7 @@
package com.nuvio.app.features.player package com.nuvio.app.features.player
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.nuvio.app.core.build.AppFeaturePolicy
import com.nuvio.app.features.addons.AddonRepository import com.nuvio.app.features.addons.AddonRepository
import com.nuvio.app.features.addons.httpGetText import com.nuvio.app.features.addons.httpGetText
import com.nuvio.app.features.details.MetaDetailsRepository import com.nuvio.app.features.details.MetaDetailsRepository
@ -149,8 +150,12 @@ object PlayerStreamsRepository {
} }
val installedAddons = AddonRepository.uiState.value.addons val installedAddons = AddonRepository.uiState.value.addons
PluginRepository.initialize() val pluginScrapers = if (AppFeaturePolicy.pluginsEnabled) {
val pluginScrapers = PluginRepository.getEnabledScrapersForType(type) PluginRepository.initialize()
PluginRepository.getEnabledScrapersForType(type)
} else {
emptyList()
}
if (installedAddons.isEmpty() && pluginScrapers.isEmpty()) { if (installedAddons.isEmpty() && pluginScrapers.isEmpty()) {
stateFlow.value = StreamsUiState( stateFlow.value = StreamsUiState(

View file

@ -132,7 +132,9 @@ object ProfileRepository {
LibraryRepository.onProfileChanged(profileIndex) LibraryRepository.onProfileChanged(profileIndex)
WatchProgressRepository.onProfileChanged(profileIndex) WatchProgressRepository.onProfileChanged(profileIndex)
AddonRepository.onProfileChanged(profileIndex) AddonRepository.onProfileChanged(profileIndex)
PluginRepository.onProfileChanged(profileIndex) if (com.nuvio.app.core.build.AppFeaturePolicy.pluginsEnabled) {
PluginRepository.onProfileChanged(profileIndex)
}
ThemeSettingsRepository.onProfileChanged() ThemeSettingsRepository.onProfileChanged()
PlayerSettingsRepository.onProfileChanged() PlayerSettingsRepository.onProfileChanged()
HomeCatalogSettingsRepository.onProfileChanged() HomeCatalogSettingsRepository.onProfileChanged()

View file

@ -8,6 +8,7 @@ import androidx.compose.material.icons.rounded.Tune
internal fun LazyListScope.contentDiscoveryContent( internal fun LazyListScope.contentDiscoveryContent(
isTablet: Boolean, isTablet: Boolean,
showPluginsEntry: Boolean,
onAddonsClick: () -> Unit, onAddonsClick: () -> Unit,
onPluginsClick: () -> Unit, onPluginsClick: () -> Unit,
onHomescreenClick: () -> Unit, onHomescreenClick: () -> Unit,
@ -26,13 +27,15 @@ internal fun LazyListScope.contentDiscoveryContent(
isTablet = isTablet, isTablet = isTablet,
onClick = onAddonsClick, onClick = onAddonsClick,
) )
SettingsNavigationRow( if (showPluginsEntry) {
title = "Plugins", SettingsNavigationRow(
description = "Install JavaScript scraper repositories and test providers internally.", title = "Plugins",
icon = Icons.Rounded.Hub, description = "Install JavaScript scraper repositories and test providers internally.",
isTablet = isTablet, icon = Icons.Rounded.Hub,
onClick = onPluginsClick, isTablet = isTablet,
) onClick = onPluginsClick,
)
}
} }
} }
} }

View file

@ -1,5 +1,6 @@
package com.nuvio.app.features.settings package com.nuvio.app.features.settings
import com.nuvio.app.core.build.AppFeaturePolicy
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -51,6 +52,7 @@ import com.nuvio.app.features.player.AvailableLanguageOptions
import com.nuvio.app.features.player.PlayerSettingsRepository import com.nuvio.app.features.player.PlayerSettingsRepository
import com.nuvio.app.features.player.SubtitleLanguageOption import com.nuvio.app.features.player.SubtitleLanguageOption
import com.nuvio.app.features.player.languageLabelForCode import com.nuvio.app.features.player.languageLabelForCode
import com.nuvio.app.features.plugins.PluginsUiState
import com.nuvio.app.features.plugins.PluginRepository import com.nuvio.app.features.plugins.PluginRepository
import com.nuvio.app.features.streams.StreamAutoPlayMode import com.nuvio.app.features.streams.StreamAutoPlayMode
import com.nuvio.app.features.streams.StreamAutoPlaySource import com.nuvio.app.features.streams.StreamAutoPlaySource
@ -111,9 +113,15 @@ private fun PlaybackSettingsSection(
var showAutoPlayAddonSelectionDialog by remember { mutableStateOf(false) } var showAutoPlayAddonSelectionDialog by remember { mutableStateOf(false) }
var showAutoPlayPluginSelectionDialog by remember { mutableStateOf(false) } var showAutoPlayPluginSelectionDialog by remember { mutableStateOf(false) }
var showAutoPlayRegexDialog by remember { mutableStateOf(false) } var showAutoPlayRegexDialog by remember { mutableStateOf(false) }
val pluginsEnabled = AppFeaturePolicy.pluginsEnabled
val autoPlayPlayerSettings by PlayerSettingsRepository.uiState.collectAsStateWithLifecycle() val autoPlayPlayerSettings by PlayerSettingsRepository.uiState.collectAsStateWithLifecycle()
val addonUiState by AddonRepository.uiState.collectAsStateWithLifecycle() val addonUiState by AddonRepository.uiState.collectAsStateWithLifecycle()
val pluginUiState by PluginRepository.uiState.collectAsStateWithLifecycle() val pluginUiState = if (pluginsEnabled) {
val state by PluginRepository.uiState.collectAsStateWithLifecycle()
state
} else {
PluginsUiState(pluginsEnabled = false)
}
val hapticFeedback = LocalHapticFeedback.current val hapticFeedback = LocalHapticFeedback.current
val sectionSpacing = if (isTablet) 18.dp else 12.dp val sectionSpacing = if (isTablet) 18.dp else 12.dp
@ -292,7 +300,7 @@ private fun PlaybackSettingsSection(
SettingsNavigationRow( SettingsNavigationRow(
title = "Source Scope", title = "Source Scope",
description = when (autoPlayPlayerSettings.streamAutoPlaySource) { description = when (autoPlayPlayerSettings.streamAutoPlaySource) {
StreamAutoPlaySource.ALL_SOURCES -> "All Sources" StreamAutoPlaySource.ALL_SOURCES -> if (pluginsEnabled) "All Sources" else "All Addons"
StreamAutoPlaySource.INSTALLED_ADDONS_ONLY -> "Installed Addons Only" StreamAutoPlaySource.INSTALLED_ADDONS_ONLY -> "Installed Addons Only"
StreamAutoPlaySource.ENABLED_PLUGINS_ONLY -> "Enabled Plugins Only" StreamAutoPlaySource.ENABLED_PLUGINS_ONLY -> "Enabled Plugins Only"
}, },
@ -313,7 +321,7 @@ private fun PlaybackSettingsSection(
onClick = { showAutoPlayAddonSelectionDialog = true }, onClick = { showAutoPlayAddonSelectionDialog = true },
) )
} }
if (autoPlayPlayerSettings.streamAutoPlaySource != StreamAutoPlaySource.INSTALLED_ADDONS_ONLY) { if (pluginsEnabled && autoPlayPlayerSettings.streamAutoPlaySource != StreamAutoPlaySource.INSTALLED_ADDONS_ONLY) {
SettingsGroupDivider(isTablet = isTablet) SettingsGroupDivider(isTablet = isTablet)
val pluginSubtitle = if (autoPlayPlayerSettings.streamAutoPlaySelectedPlugins.isEmpty()) { val pluginSubtitle = if (autoPlayPlayerSettings.streamAutoPlaySelectedPlugins.isEmpty()) {
"All Plugins" "All Plugins"
@ -678,6 +686,7 @@ private fun PlaybackSettingsSection(
if (showAutoPlaySourceDialog) { if (showAutoPlaySourceDialog) {
StreamAutoPlaySourceDialog( StreamAutoPlaySourceDialog(
pluginsEnabled = pluginsEnabled,
selectedSource = autoPlayPlayerSettings.streamAutoPlaySource, selectedSource = autoPlayPlayerSettings.streamAutoPlaySource,
onSourceSelected = { onSourceSelected = {
PlayerSettingsRepository.setStreamAutoPlaySource(it) PlayerSettingsRepository.setStreamAutoPlaySource(it)
@ -707,7 +716,7 @@ private fun PlaybackSettingsSection(
) )
} }
if (showAutoPlayPluginSelectionDialog) { if (pluginsEnabled && showAutoPlayPluginSelectionDialog) {
val pluginNames = pluginUiState.scrapers val pluginNames = pluginUiState.scrapers
.filter { it.enabled } .filter { it.enabled }
.map { it.name } .map { it.name }
@ -1121,15 +1130,40 @@ private fun StreamAutoPlayModeDialog(
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
private fun StreamAutoPlaySourceDialog( private fun StreamAutoPlaySourceDialog(
pluginsEnabled: Boolean,
selectedSource: StreamAutoPlaySource, selectedSource: StreamAutoPlaySource,
onSourceSelected: (StreamAutoPlaySource) -> Unit, onSourceSelected: (StreamAutoPlaySource) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
val options = listOf( val options = buildList {
Triple(StreamAutoPlaySource.ALL_SOURCES, "All Sources", "Consider streams from both addons and plugins."), add(
Triple(StreamAutoPlaySource.INSTALLED_ADDONS_ONLY, "Installed Addons Only", "Only consider streams from installed addons."), Triple(
Triple(StreamAutoPlaySource.ENABLED_PLUGINS_ONLY, "Enabled Plugins Only", "Only consider streams from enabled plugins."), StreamAutoPlaySource.ALL_SOURCES,
) if (pluginsEnabled) "All Sources" else "All Addons",
if (pluginsEnabled) {
"Consider streams from both addons and plugins."
} else {
"Consider streams from all installed addons."
},
),
)
add(
Triple(
StreamAutoPlaySource.INSTALLED_ADDONS_ONLY,
"Installed Addons Only",
"Only consider streams from installed addons.",
),
)
if (pluginsEnabled) {
add(
Triple(
StreamAutoPlaySource.ENABLED_PLUGINS_ONLY,
"Enabled Plugins Only",
"Only consider streams from enabled plugins.",
),
)
}
}
BasicAlertDialog( BasicAlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,

View file

@ -1,5 +1,6 @@
package com.nuvio.app.features.settings package com.nuvio.app.features.settings
import com.nuvio.app.core.build.AppFeaturePolicy
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -135,6 +136,11 @@ fun AddonsSettingsScreen(
fun PluginsSettingsScreen( fun PluginsSettingsScreen(
onBack: () -> Unit, onBack: () -> Unit,
) { ) {
if (!AppFeaturePolicy.pluginsEnabled) {
AddonsSettingsScreen(onBack = onBack)
return
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
PluginRepository.initialize() PluginRepository.initialize()
} }

View file

@ -1,5 +1,7 @@
package com.nuvio.app.features.settings package com.nuvio.app.features.settings
import com.nuvio.app.core.build.AppFeaturePolicy
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -290,13 +292,14 @@ private fun MobileSettingsScreen(
) )
SettingsPage.ContentDiscovery -> contentDiscoveryContent( SettingsPage.ContentDiscovery -> contentDiscoveryContent(
isTablet = false, isTablet = false,
showPluginsEntry = AppFeaturePolicy.pluginsEnabled,
onAddonsClick = onAddonsClick, onAddonsClick = onAddonsClick,
onPluginsClick = onPluginsClick, onPluginsClick = onPluginsClick,
onHomescreenClick = onHomescreenClick, onHomescreenClick = onHomescreenClick,
onMetaScreenClick = onMetaScreenClick, onMetaScreenClick = onMetaScreenClick,
) )
SettingsPage.Addons -> addonsSettingsContent() SettingsPage.Addons -> addonsSettingsContent()
SettingsPage.Plugins -> pluginsSettingsContent() SettingsPage.Plugins -> if (AppFeaturePolicy.pluginsEnabled) pluginsSettingsContent() else addonsSettingsContent()
SettingsPage.Homescreen -> homescreenSettingsContent( SettingsPage.Homescreen -> homescreenSettingsContent(
isTablet = false, isTablet = false,
heroEnabled = homescreenHeroEnabled, heroEnabled = homescreenHeroEnabled,
@ -484,13 +487,14 @@ private fun TabletSettingsScreen(
) )
SettingsPage.ContentDiscovery -> contentDiscoveryContent( SettingsPage.ContentDiscovery -> contentDiscoveryContent(
isTablet = true, isTablet = true,
showPluginsEntry = AppFeaturePolicy.pluginsEnabled,
onAddonsClick = { openInlinePage(SettingsPage.Addons) }, onAddonsClick = { openInlinePage(SettingsPage.Addons) },
onPluginsClick = { openInlinePage(SettingsPage.Plugins) }, onPluginsClick = { openInlinePage(SettingsPage.Plugins) },
onHomescreenClick = { openInlinePage(SettingsPage.Homescreen) }, onHomescreenClick = { openInlinePage(SettingsPage.Homescreen) },
onMetaScreenClick = { openInlinePage(SettingsPage.MetaScreen) }, onMetaScreenClick = { openInlinePage(SettingsPage.MetaScreen) },
) )
SettingsPage.Addons -> addonsSettingsContent() SettingsPage.Addons -> addonsSettingsContent()
SettingsPage.Plugins -> pluginsSettingsContent() SettingsPage.Plugins -> if (AppFeaturePolicy.pluginsEnabled) pluginsSettingsContent() else addonsSettingsContent()
SettingsPage.Homescreen -> homescreenSettingsContent( SettingsPage.Homescreen -> homescreenSettingsContent(
isTablet = true, isTablet = true,
heroEnabled = homescreenHeroEnabled, heroEnabled = homescreenHeroEnabled,

View file

@ -1,11 +1,13 @@
package com.nuvio.app.features.streams package com.nuvio.app.features.streams
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.nuvio.app.core.build.AppFeaturePolicy
import com.nuvio.app.features.addons.AddonRepository import com.nuvio.app.features.addons.AddonRepository
import com.nuvio.app.features.addons.httpGetText import com.nuvio.app.features.addons.httpGetText
import com.nuvio.app.features.details.MetaDetailsRepository import com.nuvio.app.features.details.MetaDetailsRepository
import com.nuvio.app.features.player.PlayerSettingsRepository import com.nuvio.app.features.player.PlayerSettingsRepository
import com.nuvio.app.features.plugins.PluginRepository import com.nuvio.app.features.plugins.PluginRepository
import com.nuvio.app.features.plugins.PluginsUiState
import com.nuvio.app.features.plugins.PluginRepositoryItem import com.nuvio.app.features.plugins.PluginRepositoryItem
import com.nuvio.app.features.plugins.PluginRuntimeResult import com.nuvio.app.features.plugins.PluginRuntimeResult
import com.nuvio.app.features.plugins.PluginScraper import com.nuvio.app.features.plugins.PluginScraper
@ -39,8 +41,12 @@ object StreamsRepository {
} }
private fun load(type: String, videoId: String, season: Int?, episode: Int?, forceRefresh: Boolean) { private fun load(type: String, videoId: String, season: Int?, episode: Int?, forceRefresh: Boolean) {
PluginRepository.initialize() val pluginUiState = if (AppFeaturePolicy.pluginsEnabled) {
val pluginUiState = PluginRepository.uiState.value PluginRepository.initialize()
PluginRepository.uiState.value
} else {
PluginsUiState(pluginsEnabled = false)
}
val requestKey = "$type::$videoId::$season::$episode::pluginsGrouped=${pluginUiState.groupStreamsByRepository}" val requestKey = "$type::$videoId::$season::$episode::pluginsGrouped=${pluginUiState.groupStreamsByRepository}"
val currentState = _uiState.value val currentState = _uiState.value
if ( if (
@ -89,7 +95,11 @@ object StreamsRepository {
} }
val installedAddons = AddonRepository.uiState.value.addons val installedAddons = AddonRepository.uiState.value.addons
val pluginScrapers = PluginRepository.getEnabledScrapersForType(type) val pluginScrapers = if (AppFeaturePolicy.pluginsEnabled) {
PluginRepository.getEnabledScrapersForType(type)
} else {
emptyList()
}
val pluginProviderGroups = pluginScrapers.toPluginProviderGroups( val pluginProviderGroups = pluginScrapers.toPluginProviderGroups(
repositories = pluginUiState.repositories, repositories = pluginUiState.repositories,
groupByRepository = pluginUiState.groupStreamsByRepository, groupByRepository = pluginUiState.groupStreamsByRepository,