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"