This commit is contained in:
siriusvoid 2026-07-27 18:00:14 +05:00 committed by GitHub
commit 8b551978c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 229 additions and 2 deletions

View file

@ -60,6 +60,9 @@ actual object PlayerSettingsStorage {
private const val streamAutoPlayRegexKey = "stream_auto_play_regex"
private const val streamAutoPlayTimeoutSecondsKey = "stream_auto_play_timeout_seconds"
private const val skipIntroEnabledKey = "skip_intro_enabled"
private const val autoSkipIntroEnabledKey = "auto_skip_intro_enabled"
private const val autoSkipRecapEnabledKey = "auto_skip_recap_enabled"
private const val autoSkipOutroEnabledKey = "auto_skip_outro_enabled"
private const val animeSkipEnabledKey = "animeskip_enabled"
private const val animeSkipClientIdKey = "animeskip_client_id"
private const val introDbApiKeyKey = "introdb_api_key"
@ -129,6 +132,9 @@ actual object PlayerSettingsStorage {
streamAutoPlayRegexKey,
streamAutoPlayTimeoutSecondsKey,
skipIntroEnabledKey,
autoSkipIntroEnabledKey,
autoSkipRecapEnabledKey,
autoSkipOutroEnabledKey,
animeSkipEnabledKey,
animeSkipClientIdKey,
streamAutoPlayNextEpisodeEnabledKey,
@ -775,6 +781,57 @@ actual object PlayerSettingsStorage {
?.apply()
}
actual fun loadAutoSkipIntroEnabled(): Boolean? =
preferences?.let { sharedPreferences ->
val key = ProfileScopedKey.of(autoSkipIntroEnabledKey)
if (sharedPreferences.contains(key)) {
sharedPreferences.getBoolean(key, false)
} else {
null
}
}
actual fun saveAutoSkipIntroEnabled(enabled: Boolean) {
preferences
?.edit()
?.putBoolean(ProfileScopedKey.of(autoSkipIntroEnabledKey), enabled)
?.apply()
}
actual fun loadAutoSkipRecapEnabled(): Boolean? =
preferences?.let { sharedPreferences ->
val key = ProfileScopedKey.of(autoSkipRecapEnabledKey)
if (sharedPreferences.contains(key)) {
sharedPreferences.getBoolean(key, false)
} else {
null
}
}
actual fun saveAutoSkipRecapEnabled(enabled: Boolean) {
preferences
?.edit()
?.putBoolean(ProfileScopedKey.of(autoSkipRecapEnabledKey), enabled)
?.apply()
}
actual fun loadAutoSkipOutroEnabled(): Boolean? =
preferences?.let { sharedPreferences ->
val key = ProfileScopedKey.of(autoSkipOutroEnabledKey)
if (sharedPreferences.contains(key)) {
sharedPreferences.getBoolean(key, false)
} else {
null
}
}
actual fun saveAutoSkipOutroEnabled(enabled: Boolean) {
preferences
?.edit()
?.putBoolean(ProfileScopedKey.of(autoSkipOutroEnabledKey), enabled)
?.apply()
}
actual fun loadAnimeSkipEnabled(): Boolean? =
preferences?.let { sharedPreferences ->
val key = ProfileScopedKey.of(animeSkipEnabledKey)
@ -1116,6 +1173,9 @@ actual object PlayerSettingsStorage {
loadStreamAutoPlayRegex()?.let { put(streamAutoPlayRegexKey, encodeSyncString(it)) }
loadStreamAutoPlayTimeoutSeconds()?.let { put(streamAutoPlayTimeoutSecondsKey, encodeSyncInt(it)) }
loadSkipIntroEnabled()?.let { put(skipIntroEnabledKey, encodeSyncBoolean(it)) }
loadAutoSkipIntroEnabled()?.let { put(autoSkipIntroEnabledKey, encodeSyncBoolean(it)) }
loadAutoSkipRecapEnabled()?.let { put(autoSkipRecapEnabledKey, encodeSyncBoolean(it)) }
loadAutoSkipOutroEnabled()?.let { put(autoSkipOutroEnabledKey, encodeSyncBoolean(it)) }
loadAnimeSkipEnabled()?.let { put(animeSkipEnabledKey, encodeSyncBoolean(it)) }
loadAnimeSkipClientId()?.let { put(animeSkipClientIdKey, encodeSyncString(it)) }
loadStreamAutoPlayNextEpisodeEnabled()?.let { put(streamAutoPlayNextEpisodeEnabledKey, encodeSyncBoolean(it)) }
@ -1190,6 +1250,9 @@ actual object PlayerSettingsStorage {
payload.decodeSyncString(streamAutoPlayRegexKey)?.let(::saveStreamAutoPlayRegex)
payload.decodeSyncInt(streamAutoPlayTimeoutSecondsKey)?.let(::saveStreamAutoPlayTimeoutSeconds)
payload.decodeSyncBoolean(skipIntroEnabledKey)?.let(::saveSkipIntroEnabled)
payload.decodeSyncBoolean(autoSkipIntroEnabledKey)?.let(::saveAutoSkipIntroEnabled)
payload.decodeSyncBoolean(autoSkipRecapEnabledKey)?.let(::saveAutoSkipRecapEnabled)
payload.decodeSyncBoolean(autoSkipOutroEnabledKey)?.let(::saveAutoSkipOutroEnabled)
payload.decodeSyncBoolean(animeSkipEnabledKey)?.let(::saveAnimeSkipEnabled)
payload.decodeSyncString(animeSkipClientIdKey)?.let(::saveAnimeSkipClientId)
payload.decodeSyncString(introDbApiKeyKey)?.let(::saveIntroDbApiKey)

View file

@ -1033,6 +1033,12 @@
<string name="settings_playback_subtitle_vertical_offset">Vertical Offset</string>
<string name="settings_playback_skip_intro_outro_recap">Skip Intro</string>
<string name="settings_playback_skip_intro_outro_recap_description">Use introdb.app to detect intros and recaps.</string>
<string name="settings_playback_auto_skip_intro">Auto-skip Intro / Opening</string>
<string name="settings_playback_auto_skip_intro_description">Skip intros and anime openings automatically.</string>
<string name="settings_playback_auto_skip_recap">Auto-skip Recap</string>
<string name="settings_playback_auto_skip_recap_description">Skip recap segments automatically.</string>
<string name="settings_playback_auto_skip_outro">Auto-skip Outro / Ending</string>
<string name="settings_playback_auto_skip_outro_description">Skip outros and anime endings automatically.</string>
<string name="settings_playback_source_scope">Auto-play Source Scope</string>
<string name="settings_playback_source_scope_all_addons">All installed addons</string>
<string name="settings_playback_source_scope_all_addons_description">Auto-play only considers streams coming from your installed addons.</string>

View file

@ -10,6 +10,7 @@ import com.nuvio.app.features.p2p.P2pStreamingEngine
import com.nuvio.app.features.p2p.P2pStreamingState
import com.nuvio.app.features.player.skip.NextEpisodeInfo
import com.nuvio.app.features.player.skip.PlayerNextEpisodeRules
import com.nuvio.app.features.player.skip.SkipInterval
import com.nuvio.app.features.player.skip.SkipIntroRepository
import com.nuvio.app.features.streams.BingeGroupCacheRepository
import com.nuvio.app.features.streams.StreamLinkCacheRepository
@ -22,6 +23,15 @@ import kotlinx.coroutines.launch
import nuvio.composeapp.generated.resources.*
import org.jetbrains.compose.resources.getString
/** Whether auto-skip is enabled for this segment's type (categorized by SkipIntroRepository). */
internal fun PlayerSettingsUiState.autoSkips(interval: SkipInterval): Boolean =
when (SkipIntroRepository.segmentCategory(interval.type)) {
"opening" -> autoSkipIntroEnabled
"recap" -> autoSkipRecapEnabled
"ending" -> autoSkipOutroEnabled
else -> false
}
@Composable
internal fun PlayerScreenRuntime.BindPlayerRuntimeEffects() {
val currentFeedback = liveGestureFeedback ?: gestureFeedback
@ -387,6 +397,7 @@ private fun PlayerScreenRuntime.BindPlayerMetadataAndSkipEffects() {
skipIntervals = emptyList()
activeSkipInterval = null
skipIntervalDismissed = false
autoSkippedIntervals = emptySet()
showNextEpisodeCard = false
nextEpisodeAutoPlayJob?.cancel()
nextEpisodeAutoPlaySearching = false
@ -422,6 +433,21 @@ private fun PlayerScreenRuntime.BindPlayerMetadataAndSkipEffects() {
}
}
// Automatically skip the segment currently playing, once per segment.
LaunchedEffect(playbackSnapshot.positionMs, skipIntervals) {
if (!initialLoadCompleted || !initialSeekApplied || pausedOverlayVisible) return@LaunchedEffect
val positionSec = playbackSnapshot.positionMs / 1000.0
val current = skipIntervals.firstOrNull { interval ->
positionSec >= interval.startTime && positionSec < interval.endTime - 0.5
} ?: return@LaunchedEffect
if (current in autoSkippedIntervals) return@LaunchedEffect
if (!playerSettingsUiState.autoSkips(current)) return@LaunchedEffect
autoSkippedIntervals = autoSkippedIntervals + current
playerController?.seekTo((current.endTime * 1000).toLong())
scheduleProgressSyncAfterSeek()
skipIntervalDismissed = true
}
LaunchedEffect(playerMetaVideos, activeSeasonNumber, activeEpisodeNumber) {
if (!isSeries || playerMetaVideos.isEmpty()) {
nextEpisodeInfo = null

View file

@ -164,6 +164,7 @@ internal class PlayerScreenRuntime(
var skipIntervals by mutableStateOf<List<SkipInterval>>(emptyList())
var activeSkipInterval by mutableStateOf<SkipInterval?>(null)
var skipIntervalDismissed by mutableStateOf(false)
var autoSkippedIntervals by mutableStateOf(setOf<SkipInterval>())
var parentalWarnings by mutableStateOf<List<ParentalWarning>>(emptyList())
var showParentalGuide by mutableStateOf(false)
var parentalGuideHasShown by mutableStateOf(false)

View file

@ -380,7 +380,9 @@ private fun BoxScope.RenderPlaybackOverlays(
renderedGestureFeedback = renderedGestureFeedback,
initialLoadCompleted = initialLoadCompleted,
pausedOverlayVisible = pausedOverlayVisible,
activeSkipInterval = activeSkipInterval,
activeSkipInterval = activeSkipInterval?.takeUnless {
playerSettingsUiState.autoSkips(it) && (it !in autoSkippedIntervals || skipIntervalDismissed)
},
skipIntervalDismissed = skipIntervalDismissed,
controlsVisible = controlsVisible,
onSkipInterval = { interval ->

View file

@ -64,6 +64,9 @@ data class PlayerSettingsUiState(
val streamAutoPlayRegex: String = "",
val streamAutoPlayTimeoutSeconds: Int = 3,
val skipIntroEnabled: Boolean = true,
val autoSkipIntroEnabled: Boolean = false,
val autoSkipRecapEnabled: Boolean = false,
val autoSkipOutroEnabled: Boolean = false,
val animeSkipEnabled: Boolean = false,
val animeSkipClientId: String = "",
val introDbApiKey: String = "",
@ -130,6 +133,9 @@ object PlayerSettingsRepository {
private var streamAutoPlayRegex = ""
private var streamAutoPlayTimeoutSeconds = 3
private var skipIntroEnabled = true
private var autoSkipIntroEnabled = false
private var autoSkipRecapEnabled = false
private var autoSkipOutroEnabled = false
private var animeSkipEnabled = false
private var animeSkipClientId = ""
private var introDbApiKey = ""
@ -201,6 +207,9 @@ object PlayerSettingsRepository {
streamAutoPlayRegex = ""
streamAutoPlayTimeoutSeconds = 3
skipIntroEnabled = true
autoSkipIntroEnabled = false
autoSkipRecapEnabled = false
autoSkipOutroEnabled = false
animeSkipEnabled = false
animeSkipClientId = ""
introDbApiKey = ""
@ -324,6 +333,9 @@ object PlayerSettingsRepository {
PlayerSettingsStorage.saveStreamAutoPlayTimeoutSeconds(streamAutoPlayTimeoutSeconds)
}
skipIntroEnabled = PlayerSettingsStorage.loadSkipIntroEnabled() ?: true
autoSkipIntroEnabled = PlayerSettingsStorage.loadAutoSkipIntroEnabled() ?: false
autoSkipRecapEnabled = PlayerSettingsStorage.loadAutoSkipRecapEnabled() ?: false
autoSkipOutroEnabled = PlayerSettingsStorage.loadAutoSkipOutroEnabled() ?: false
animeSkipEnabled = PlayerSettingsStorage.loadAnimeSkipEnabled() ?: false
animeSkipClientId = PlayerSettingsStorage.loadAnimeSkipClientId() ?: ""
introDbApiKey = PlayerSettingsStorage.loadIntroDbApiKey() ?: ""
@ -647,6 +659,30 @@ object PlayerSettingsRepository {
PlayerSettingsStorage.saveSkipIntroEnabled(enabled)
}
fun setAutoSkipIntroEnabled(enabled: Boolean) {
ensureLoaded()
if (autoSkipIntroEnabled == enabled) return
autoSkipIntroEnabled = enabled
publish()
PlayerSettingsStorage.saveAutoSkipIntroEnabled(enabled)
}
fun setAutoSkipRecapEnabled(enabled: Boolean) {
ensureLoaded()
if (autoSkipRecapEnabled == enabled) return
autoSkipRecapEnabled = enabled
publish()
PlayerSettingsStorage.saveAutoSkipRecapEnabled(enabled)
}
fun setAutoSkipOutroEnabled(enabled: Boolean) {
ensureLoaded()
if (autoSkipOutroEnabled == enabled) return
autoSkipOutroEnabled = enabled
publish()
PlayerSettingsStorage.saveAutoSkipOutroEnabled(enabled)
}
fun setAnimeSkipEnabled(enabled: Boolean) {
ensureLoaded()
if (animeSkipEnabled == enabled) return
@ -942,6 +978,9 @@ object PlayerSettingsRepository {
streamAutoPlayRegex = streamAutoPlayRegex,
streamAutoPlayTimeoutSeconds = streamAutoPlayTimeoutSeconds,
skipIntroEnabled = skipIntroEnabled,
autoSkipIntroEnabled = autoSkipIntroEnabled,
autoSkipRecapEnabled = autoSkipRecapEnabled,
autoSkipOutroEnabled = autoSkipOutroEnabled,
animeSkipEnabled = animeSkipEnabled,
animeSkipClientId = animeSkipClientId,
introDbApiKey = introDbApiKey,

View file

@ -85,6 +85,12 @@ internal expect object PlayerSettingsStorage {
fun saveStreamAutoPlayTimeoutSeconds(seconds: Int)
fun loadSkipIntroEnabled(): Boolean?
fun saveSkipIntroEnabled(enabled: Boolean)
fun loadAutoSkipIntroEnabled(): Boolean?
fun saveAutoSkipIntroEnabled(enabled: Boolean)
fun loadAutoSkipRecapEnabled(): Boolean?
fun saveAutoSkipRecapEnabled(enabled: Boolean)
fun loadAutoSkipOutroEnabled(): Boolean?
fun saveAutoSkipOutroEnabled(enabled: Boolean)
fun loadAnimeSkipEnabled(): Boolean?
fun saveAnimeSkipEnabled(enabled: Boolean)
fun loadAnimeSkipClientId(): String?

View file

@ -151,7 +151,7 @@ object SkipIntroRepository {
return chosen.values.toList()
}
private fun segmentCategory(type: String): String? = when (type.lowercase()) {
internal fun segmentCategory(type: String): String? = when (type.lowercase()) {
"intro", "op", "mixed-op" -> "opening"
"outro", "ed", "mixed-ed", "credits", "ending" -> "ending"
"recap" -> "recap"

View file

@ -1028,6 +1028,33 @@ private fun PlaybackSettingsSection(
onCheckedChange = PlayerSettingsRepository::setSkipIntroEnabled,
)
SettingsGroupDivider(isTablet = isTablet)
SettingsSwitchRow(
title = stringResource(Res.string.settings_playback_auto_skip_intro),
description = stringResource(Res.string.settings_playback_auto_skip_intro_description),
checked = autoPlayPlayerSettings.autoSkipIntroEnabled,
enabled = autoPlayPlayerSettings.skipIntroEnabled,
isTablet = isTablet,
onCheckedChange = PlayerSettingsRepository::setAutoSkipIntroEnabled,
)
SettingsGroupDivider(isTablet = isTablet)
SettingsSwitchRow(
title = stringResource(Res.string.settings_playback_auto_skip_recap),
description = stringResource(Res.string.settings_playback_auto_skip_recap_description),
checked = autoPlayPlayerSettings.autoSkipRecapEnabled,
enabled = autoPlayPlayerSettings.skipIntroEnabled,
isTablet = isTablet,
onCheckedChange = PlayerSettingsRepository::setAutoSkipRecapEnabled,
)
SettingsGroupDivider(isTablet = isTablet)
SettingsSwitchRow(
title = stringResource(Res.string.settings_playback_auto_skip_outro),
description = stringResource(Res.string.settings_playback_auto_skip_outro_description),
checked = autoPlayPlayerSettings.autoSkipOutroEnabled,
enabled = autoPlayPlayerSettings.skipIntroEnabled,
isTablet = isTablet,
onCheckedChange = PlayerSettingsRepository::setAutoSkipOutroEnabled,
)
SettingsGroupDivider(isTablet = isTablet)
SettingsSwitchRow(
title = stringResource(Res.string.settings_playback_anime_skip),
description = stringResource(Res.string.settings_playback_anime_skip_description),

View file

@ -637,6 +637,9 @@ internal fun settingsSearchEntries(
icon = Icons.Rounded.PlayArrow,
rows = listOf(
PlaybackSearchRow("skip-intro", stringResource(Res.string.settings_playback_skip_intro_outro_recap), stringResource(Res.string.settings_playback_skip_intro_outro_recap_description)),
PlaybackSearchRow("auto-skip-intro", stringResource(Res.string.settings_playback_auto_skip_intro), stringResource(Res.string.settings_playback_auto_skip_intro_description)),
PlaybackSearchRow("auto-skip-recap", stringResource(Res.string.settings_playback_auto_skip_recap), stringResource(Res.string.settings_playback_auto_skip_recap_description)),
PlaybackSearchRow("auto-skip-outro", stringResource(Res.string.settings_playback_auto_skip_outro), stringResource(Res.string.settings_playback_auto_skip_outro_description)),
PlaybackSearchRow("anime-skip", stringResource(Res.string.settings_playback_anime_skip), stringResource(Res.string.settings_playback_anime_skip_description)),
PlaybackSearchRow("anime-skip-client", stringResource(Res.string.settings_playback_anime_skip_client_id), stringResource(Res.string.settings_playback_anime_skip_client_id_description)),
PlaybackSearchRow("intro-submit", stringResource(Res.string.settings_playback_intro_submit_enabled), stringResource(Res.string.settings_playback_intro_submit_enabled_description)),

View file

@ -58,6 +58,9 @@ actual object PlayerSettingsStorage {
private const val streamAutoPlayRegexKey = "stream_auto_play_regex"
private const val streamAutoPlayTimeoutSecondsKey = "stream_auto_play_timeout_seconds"
private const val skipIntroEnabledKey = "skip_intro_enabled"
private const val autoSkipIntroEnabledKey = "auto_skip_intro_enabled"
private const val autoSkipRecapEnabledKey = "auto_skip_recap_enabled"
private const val autoSkipOutroEnabledKey = "auto_skip_outro_enabled"
private const val animeSkipEnabledKey = "animeskip_enabled"
private const val animeSkipClientIdKey = "animeskip_client_id"
private const val introDbApiKeyKey = "introdb_api_key"
@ -127,6 +130,9 @@ actual object PlayerSettingsStorage {
streamAutoPlayRegexKey,
streamAutoPlayTimeoutSecondsKey,
skipIntroEnabledKey,
autoSkipIntroEnabledKey,
autoSkipRecapEnabledKey,
autoSkipOutroEnabledKey,
animeSkipEnabledKey,
animeSkipClientIdKey,
streamAutoPlayNextEpisodeEnabledKey,
@ -656,6 +662,48 @@ actual object PlayerSettingsStorage {
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(skipIntroEnabledKey))
}
actual fun loadAutoSkipIntroEnabled(): Boolean? {
val defaults = NSUserDefaults.standardUserDefaults
val key = ProfileScopedKey.of(autoSkipIntroEnabledKey)
return if (defaults.objectForKey(key) != null) {
defaults.boolForKey(key)
} else {
null
}
}
actual fun saveAutoSkipIntroEnabled(enabled: Boolean) {
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(autoSkipIntroEnabledKey))
}
actual fun loadAutoSkipRecapEnabled(): Boolean? {
val defaults = NSUserDefaults.standardUserDefaults
val key = ProfileScopedKey.of(autoSkipRecapEnabledKey)
return if (defaults.objectForKey(key) != null) {
defaults.boolForKey(key)
} else {
null
}
}
actual fun saveAutoSkipRecapEnabled(enabled: Boolean) {
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(autoSkipRecapEnabledKey))
}
actual fun loadAutoSkipOutroEnabled(): Boolean? {
val defaults = NSUserDefaults.standardUserDefaults
val key = ProfileScopedKey.of(autoSkipOutroEnabledKey)
return if (defaults.objectForKey(key) != null) {
defaults.boolForKey(key)
} else {
null
}
}
actual fun saveAutoSkipOutroEnabled(enabled: Boolean) {
NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(autoSkipOutroEnabledKey))
}
actual fun loadAnimeSkipEnabled(): Boolean? {
val defaults = NSUserDefaults.standardUserDefaults
val key = ProfileScopedKey.of(animeSkipEnabledKey)
@ -936,6 +984,9 @@ actual object PlayerSettingsStorage {
loadStreamAutoPlayRegex()?.let { put(streamAutoPlayRegexKey, encodeSyncString(it)) }
loadStreamAutoPlayTimeoutSeconds()?.let { put(streamAutoPlayTimeoutSecondsKey, encodeSyncInt(it)) }
loadSkipIntroEnabled()?.let { put(skipIntroEnabledKey, encodeSyncBoolean(it)) }
loadAutoSkipIntroEnabled()?.let { put(autoSkipIntroEnabledKey, encodeSyncBoolean(it)) }
loadAutoSkipRecapEnabled()?.let { put(autoSkipRecapEnabledKey, encodeSyncBoolean(it)) }
loadAutoSkipOutroEnabled()?.let { put(autoSkipOutroEnabledKey, encodeSyncBoolean(it)) }
loadAnimeSkipEnabled()?.let { put(animeSkipEnabledKey, encodeSyncBoolean(it)) }
loadAnimeSkipClientId()?.let { put(animeSkipClientIdKey, encodeSyncString(it)) }
loadStreamAutoPlayNextEpisodeEnabled()?.let { put(streamAutoPlayNextEpisodeEnabledKey, encodeSyncBoolean(it)) }
@ -1009,6 +1060,9 @@ actual object PlayerSettingsStorage {
payload.decodeSyncString(streamAutoPlayRegexKey)?.let(::saveStreamAutoPlayRegex)
payload.decodeSyncInt(streamAutoPlayTimeoutSecondsKey)?.let(::saveStreamAutoPlayTimeoutSeconds)
payload.decodeSyncBoolean(skipIntroEnabledKey)?.let(::saveSkipIntroEnabled)
payload.decodeSyncBoolean(autoSkipIntroEnabledKey)?.let(::saveAutoSkipIntroEnabled)
payload.decodeSyncBoolean(autoSkipRecapEnabledKey)?.let(::saveAutoSkipRecapEnabled)
payload.decodeSyncBoolean(autoSkipOutroEnabledKey)?.let(::saveAutoSkipOutroEnabled)
payload.decodeSyncBoolean(animeSkipEnabledKey)?.let(::saveAnimeSkipEnabled)
payload.decodeSyncString(animeSkipClientIdKey)?.let(::saveAnimeSkipClientId)
payload.decodeSyncString(introDbApiKeyKey)?.let(::saveIntroDbApiKey)