mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-03 08:49:07 +00:00
flavouring yt extraction
This commit is contained in:
parent
49a178c7f9
commit
f65f934acd
20 changed files with 225 additions and 84 deletions
|
|
@ -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}"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.nuvio.app.features.trailer
|
||||||
|
|
||||||
|
actual object TrailerPlaybackResolver {
|
||||||
|
actual suspend fun resolveFromYouTubeUrl(youtubeUrl: String): TrailerPlaybackSource? = null
|
||||||
|
}
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue