From e0620f9fa959cfc4d9a4d3411f02972f7f611b11 Mon Sep 17 00:00:00 2001
From: siriusvoid <271671657+siriusvoid@users.noreply.github.com>
Date: Fri, 12 Jun 2026 15:05:01 +0000
Subject: [PATCH 1/3] feat: add per-segment auto-skip for intro/recap/outro
---
.../player/PlayerSettingsStorage.android.kt | 63 +++++++++++++++++++
.../composeResources/values/strings.xml | 6 ++
.../player/PlayerScreenRuntimeEffects.kt | 26 ++++++++
.../player/PlayerScreenRuntimeState.kt | 1 +
.../features/player/PlayerScreenRuntimeUi.kt | 4 +-
.../player/PlayerSettingsRepository.kt | 39 ++++++++++++
.../features/player/PlayerSettingsStorage.kt | 6 ++
.../features/settings/PlaybackSettingsPage.kt | 27 ++++++++
.../app/features/settings/SettingsSearch.kt | 3 +
.../player/PlayerSettingsStorage.ios.kt | 54 ++++++++++++++++
10 files changed, 228 insertions(+), 1 deletion(-)
diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.android.kt
index 2a98502d..5f17ecb6 100644
--- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.android.kt
+++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.android.kt
@@ -53,6 +53,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"
@@ -115,6 +118,9 @@ actual object PlayerSettingsStorage {
streamAutoPlayRegexKey,
streamAutoPlayTimeoutSecondsKey,
skipIntroEnabledKey,
+ autoSkipIntroEnabledKey,
+ autoSkipRecapEnabledKey,
+ autoSkipOutroEnabledKey,
animeSkipEnabledKey,
animeSkipClientIdKey,
streamAutoPlayNextEpisodeEnabledKey,
@@ -656,6 +662,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)
@@ -988,6 +1045,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)) }
@@ -1054,6 +1114,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)
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index be681d8d..671448cd 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -920,6 +920,12 @@
Vertical Offset
Skip Intro
Use introdb.app to detect intros and recaps.
+ Auto Skip Intro
+ Automatically skip the intro when detected.
+ Auto Skip Recap
+ Automatically skip the recap when detected.
+ Auto Skip Outro
+ Automatically skip the outro when detected.
Auto-play Source Scope
All installed addons
Auto-play only considers streams coming from your installed addons.
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt
index 886046c6..7cf7da84 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt
@@ -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. */
+internal fun PlayerSettingsUiState.autoSkips(interval: SkipInterval): Boolean =
+ when (interval.type.lowercase()) {
+ "intro", "op", "mixed-op" -> autoSkipIntroEnabled
+ "recap" -> autoSkipRecapEnabled
+ "outro", "ed", "mixed-ed", "credits" -> autoSkipOutroEnabled
+ else -> false
+ }
+
@Composable
internal fun PlayerScreenRuntime.BindPlayerRuntimeEffects() {
val currentFeedback = liveGestureFeedback ?: gestureFeedback
@@ -362,6 +372,7 @@ private fun PlayerScreenRuntime.BindPlayerMetadataAndSkipEffects() {
skipIntervals = emptyList()
activeSkipInterval = null
skipIntervalDismissed = false
+ autoSkippedIntervals = emptySet()
showNextEpisodeCard = false
nextEpisodeAutoPlayJob?.cancel()
nextEpisodeAutoPlaySearching = false
@@ -397,6 +408,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
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeState.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeState.kt
index 73145102..680b1a10 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeState.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeState.kt
@@ -155,6 +155,7 @@ internal class PlayerScreenRuntime(
var skipIntervals by mutableStateOf>(emptyList())
var activeSkipInterval by mutableStateOf(null)
var skipIntervalDismissed by mutableStateOf(false)
+ var autoSkippedIntervals by mutableStateOf(setOf())
var parentalWarnings by mutableStateOf>(emptyList())
var showParentalGuide by mutableStateOf(false)
var parentalGuideHasShown by mutableStateOf(false)
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt
index 3119c79f..e5b6a49c 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeUi.kt
@@ -327,7 +327,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 ->
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsRepository.kt
index a4c94820..06942194 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsRepository.kt
@@ -57,6 +57,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 = "",
@@ -116,6 +119,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 = ""
@@ -180,6 +186,9 @@ object PlayerSettingsRepository {
streamAutoPlayRegex = ""
streamAutoPlayTimeoutSeconds = 3
skipIntroEnabled = true
+ autoSkipIntroEnabled = false
+ autoSkipRecapEnabled = false
+ autoSkipOutroEnabled = false
animeSkipEnabled = false
animeSkipClientId = ""
introDbApiKey = ""
@@ -292,6 +301,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() ?: ""
@@ -561,6 +573,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
@@ -849,6 +885,9 @@ object PlayerSettingsRepository {
streamAutoPlayRegex = streamAutoPlayRegex,
streamAutoPlayTimeoutSeconds = streamAutoPlayTimeoutSeconds,
skipIntroEnabled = skipIntroEnabled,
+ autoSkipIntroEnabled = autoSkipIntroEnabled,
+ autoSkipRecapEnabled = autoSkipRecapEnabled,
+ autoSkipOutroEnabled = autoSkipOutroEnabled,
animeSkipEnabled = animeSkipEnabled,
animeSkipClientId = animeSkipClientId,
introDbApiKey = introDbApiKey,
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.kt
index 0196d338..13cd7fe5 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.kt
@@ -71,6 +71,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?
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PlaybackSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PlaybackSettingsPage.kt
index ed5ce9da..f38e174a 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PlaybackSettingsPage.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/PlaybackSettingsPage.kt
@@ -856,6 +856,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),
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt
index 5d0f8cf3..4f016230 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/SettingsSearch.kt
@@ -597,6 +597,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)),
diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.ios.kt
index 6da10aff..b9bcb4a7 100644
--- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.ios.kt
+++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/player/PlayerSettingsStorage.ios.kt
@@ -51,6 +51,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"
@@ -113,6 +116,9 @@ actual object PlayerSettingsStorage {
streamAutoPlayRegexKey,
streamAutoPlayTimeoutSecondsKey,
skipIntroEnabledKey,
+ autoSkipIntroEnabledKey,
+ autoSkipRecapEnabledKey,
+ autoSkipOutroEnabledKey,
animeSkipEnabledKey,
animeSkipClientIdKey,
streamAutoPlayNextEpisodeEnabledKey,
@@ -566,6 +572,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)
@@ -838,6 +886,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)) }
@@ -904,6 +955,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)
From 9b7ea9d89de782f3fab344eb2e38ab5f4415295b Mon Sep 17 00:00:00 2001
From: siriusvoid <271671657+siriusvoid@users.noreply.github.com>
Date: Wed, 24 Jun 2026 12:39:01 +0000
Subject: [PATCH 2/3] refactor: align auto-skip toggle names and descriptions
with NuvioTV
---
.../commonMain/composeResources/values/strings.xml | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index 671448cd..d90cfe1f 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -920,12 +920,12 @@
Vertical Offset
Skip Intro
Use introdb.app to detect intros and recaps.
- Auto Skip Intro
- Automatically skip the intro when detected.
- Auto Skip Recap
- Automatically skip the recap when detected.
- Auto Skip Outro
- Automatically skip the outro when detected.
+ Auto-skip Intro / Opening
+ Skip intros and anime openings automatically.
+ Auto-skip Recap
+ Skip recap segments automatically.
+ Auto-skip Outro / Ending
+ Skip outros and anime endings automatically.
Auto-play Source Scope
All installed addons
Auto-play only considers streams coming from your installed addons.
From 5a8fe85b67a226e6521e99e4445deafe2dcc0a92 Mon Sep 17 00:00:00 2001
From: siriusvoid <271671657+siriusvoid@users.noreply.github.com>
Date: Tue, 30 Jun 2026 14:55:27 +0000
Subject: [PATCH 3/3] refactor: reuse upstream segment categorization for
auto-skip
---
.../app/features/player/PlayerScreenRuntimeEffects.kt | 8 ++++----
.../nuvio/app/features/player/skip/SkipIntroRepository.kt | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt
index cad98ac8..0b18abe8 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeEffects.kt
@@ -23,12 +23,12 @@ 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. */
+/** Whether auto-skip is enabled for this segment's type (categorized by SkipIntroRepository). */
internal fun PlayerSettingsUiState.autoSkips(interval: SkipInterval): Boolean =
- when (interval.type.lowercase()) {
- "intro", "op", "mixed-op" -> autoSkipIntroEnabled
+ when (SkipIntroRepository.segmentCategory(interval.type)) {
+ "opening" -> autoSkipIntroEnabled
"recap" -> autoSkipRecapEnabled
- "outro", "ed", "mixed-ed", "credits" -> autoSkipOutroEnabled
+ "ending" -> autoSkipOutroEnabled
else -> false
}
diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/skip/SkipIntroRepository.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/skip/SkipIntroRepository.kt
index d99dc978..7063b3fb 100644
--- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/skip/SkipIntroRepository.kt
+++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/skip/SkipIntroRepository.kt
@@ -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"