From f2c9b9f9e74b4a0c2dd9a24277d235a476fb2c59 Mon Sep 17 00:00:00 2001 From: skoruppa Date: Sun, 9 Aug 2026 23:28:29 +0200 Subject: [PATCH] feat: add binge group fallback toggle for manual mode auto-play When auto-play next episode is enabled in manual mode, add option to control whether first stream is auto-selected when binge group fails. Default on (preserves existing behavior). When off, shows the stream picker instead. --- .../player/PlayerSettingsStorage.android.kt | 21 +++++++++++++++++++ .../composeResources/values-pl/strings.xml | 2 ++ .../composeResources/values/strings.xml | 2 ++ .../player/PlayerNextEpisodeAutoPlay.kt | 3 ++- .../player/PlayerSettingsRepository.kt | 13 ++++++++++++ .../features/player/PlayerSettingsStorage.kt | 2 ++ .../features/settings/PlaybackSettingsPage.kt | 11 ++++++++++ .../player/PlayerSettingsStorage.ios.kt | 18 ++++++++++++++++ 8 files changed, 71 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 74d313473..2d4f62b0b 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 @@ -65,6 +65,7 @@ actual object PlayerSettingsStorage { private const val introDbApiKeyKey = "introdb_api_key" private const val introSubmitEnabledKey = "intro_submit_enabled" private const val streamAutoPlayNextEpisodeEnabledKey = "stream_auto_play_next_episode_enabled" + private const val streamAutoPlayNextEpisodeFallbackEnabledKey = "stream_auto_play_next_episode_fallback_enabled" private const val streamAutoPlayPreferBingeGroupKey = "stream_auto_play_prefer_binge_group" private const val streamAutoPlayReuseBingeGroupKey = "stream_auto_play_reuse_binge_group" private const val nextEpisodeThresholdModeKey = "next_episode_threshold_mode" @@ -132,6 +133,7 @@ actual object PlayerSettingsStorage { animeSkipEnabledKey, animeSkipClientIdKey, streamAutoPlayNextEpisodeEnabledKey, + streamAutoPlayNextEpisodeFallbackEnabledKey, streamAutoPlayPreferBingeGroupKey, streamAutoPlayReuseBingeGroupKey, nextEpisodeThresholdModeKey, @@ -846,6 +848,23 @@ actual object PlayerSettingsStorage { ?.apply() } + actual fun loadStreamAutoPlayNextEpisodeFallbackEnabled(): Boolean? = + preferences?.let { sharedPreferences -> + val key = ProfileScopedKey.of(streamAutoPlayNextEpisodeFallbackEnabledKey) + if (sharedPreferences.contains(key)) { + sharedPreferences.getBoolean(key, true) + } else { + null + } + } + + actual fun saveStreamAutoPlayNextEpisodeFallbackEnabled(enabled: Boolean) { + preferences + ?.edit() + ?.putBoolean(ProfileScopedKey.of(streamAutoPlayNextEpisodeFallbackEnabledKey), enabled) + ?.apply() + } + actual fun loadStreamAutoPlayPreferBingeGroup(): Boolean? = preferences?.let { sharedPreferences -> val key = ProfileScopedKey.of(streamAutoPlayPreferBingeGroupKey) @@ -1119,6 +1138,7 @@ actual object PlayerSettingsStorage { loadAnimeSkipEnabled()?.let { put(animeSkipEnabledKey, encodeSyncBoolean(it)) } loadAnimeSkipClientId()?.let { put(animeSkipClientIdKey, encodeSyncString(it)) } loadStreamAutoPlayNextEpisodeEnabled()?.let { put(streamAutoPlayNextEpisodeEnabledKey, encodeSyncBoolean(it)) } + loadStreamAutoPlayNextEpisodeFallbackEnabled()?.let { put(streamAutoPlayNextEpisodeFallbackEnabledKey, encodeSyncBoolean(it)) } loadStreamAutoPlayPreferBingeGroup()?.let { put(streamAutoPlayPreferBingeGroupKey, encodeSyncBoolean(it)) } loadStreamAutoPlayReuseBingeGroup()?.let { put(streamAutoPlayReuseBingeGroupKey, encodeSyncBoolean(it)) } loadNextEpisodeThresholdMode()?.let { put(nextEpisodeThresholdModeKey, encodeSyncString(it)) } @@ -1195,6 +1215,7 @@ actual object PlayerSettingsStorage { payload.decodeSyncString(introDbApiKeyKey)?.let(::saveIntroDbApiKey) payload.decodeSyncBoolean(introSubmitEnabledKey)?.let(::saveIntroSubmitEnabled) payload.decodeSyncBoolean(streamAutoPlayNextEpisodeEnabledKey)?.let(::saveStreamAutoPlayNextEpisodeEnabled) + payload.decodeSyncBoolean(streamAutoPlayNextEpisodeFallbackEnabledKey)?.let(::saveStreamAutoPlayNextEpisodeFallbackEnabled) payload.decodeSyncBoolean(streamAutoPlayPreferBingeGroupKey)?.let(::saveStreamAutoPlayPreferBingeGroup) payload.decodeSyncBoolean(streamAutoPlayReuseBingeGroupKey)?.let(::saveStreamAutoPlayReuseBingeGroup) payload.decodeSyncString(nextEpisodeThresholdModeKey)?.let(::saveNextEpisodeThresholdMode) diff --git a/composeApp/src/commonMain/composeResources/values-pl/strings.xml b/composeApp/src/commonMain/composeResources/values-pl/strings.xml index 10adfca3a..9b6756ebb 100644 --- a/composeApp/src/commonMain/composeResources/values-pl/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-pl/strings.xml @@ -767,6 +767,8 @@ Wyszukuj również w AnimeSkip znaczniki pomijania (wymaga ID klienta). Automatyczne odtwarzanie następnego odcinka Automatycznie znajdź i odtwórz następny odcinek po osiągnięciu progu. + Fallback gdy grupa binge nie zostanie znaleziona + Automatycznie wybierz pierwszy dostępny strumień jeśli grupa binge nie zostanie znaleziona. Gdy wyłączone, wyświetla wybór źródła. Tylko urządzenie Preferuj aplikację (FFmpeg) Preferuj urządzenie diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 6f54ce85a..876243274 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -928,6 +928,8 @@ Also search AnimeSkip for skip timestamps (requires client ID). Auto-play Next Episode Start next episode automatically when prompt appears. + Fallback when binge group fails + Auto-select first available stream if binge group is not found. When off, shows the stream picker instead. Device decoders only Prefer app decoders (FFmpeg) Prefer device decoders diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerNextEpisodeAutoPlay.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerNextEpisodeAutoPlay.kt index 535fc3410..a5f008170 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerNextEpisodeAutoPlay.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerNextEpisodeAutoPlay.kt @@ -66,7 +66,8 @@ internal fun CoroutineScope.launchPlayerNextEpisodeAutoPlay( val bingeGroupOnlyManualMode = shouldAutoSelectInManualMode && - !settings.streamAutoPlayNextEpisodeEnabled && + (!settings.streamAutoPlayNextEpisodeEnabled || + !settings.streamAutoPlayNextEpisodeFallbackEnabled) && settings.streamAutoPlayPreferBingeGroup val effectiveMode = if (shouldAutoSelectInManualMode) { 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 c0f4c4294..2814547a8 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 @@ -69,6 +69,7 @@ data class PlayerSettingsUiState( val introDbApiKey: String = "", val introSubmitEnabled: Boolean = false, val streamAutoPlayNextEpisodeEnabled: Boolean = false, + val streamAutoPlayNextEpisodeFallbackEnabled: Boolean = true, val streamAutoPlayPreferBingeGroup: Boolean = true, val streamAutoPlayReuseBingeGroup: Boolean = true, val nextEpisodeThresholdMode: NextEpisodeThresholdMode = NextEpisodeThresholdMode.PERCENTAGE, @@ -135,6 +136,7 @@ object PlayerSettingsRepository { private var introDbApiKey = "" private var introSubmitEnabled = false private var streamAutoPlayNextEpisodeEnabled = false + private var streamAutoPlayNextEpisodeFallbackEnabled = true private var streamAutoPlayPreferBingeGroup = true private var streamAutoPlayReuseBingeGroup = true private var nextEpisodeThresholdMode = NextEpisodeThresholdMode.PERCENTAGE @@ -206,6 +208,7 @@ object PlayerSettingsRepository { introDbApiKey = "" introSubmitEnabled = false streamAutoPlayNextEpisodeEnabled = false + streamAutoPlayNextEpisodeFallbackEnabled = true streamAutoPlayPreferBingeGroup = true streamAutoPlayReuseBingeGroup = true nextEpisodeThresholdMode = NextEpisodeThresholdMode.PERCENTAGE @@ -329,6 +332,7 @@ object PlayerSettingsRepository { introDbApiKey = PlayerSettingsStorage.loadIntroDbApiKey() ?: "" introSubmitEnabled = PlayerSettingsStorage.loadIntroSubmitEnabled() ?: false streamAutoPlayNextEpisodeEnabled = PlayerSettingsStorage.loadStreamAutoPlayNextEpisodeEnabled() ?: false + streamAutoPlayNextEpisodeFallbackEnabled = PlayerSettingsStorage.loadStreamAutoPlayNextEpisodeFallbackEnabled() ?: true streamAutoPlayPreferBingeGroup = PlayerSettingsStorage.loadStreamAutoPlayPreferBingeGroup() ?: true streamAutoPlayReuseBingeGroup = PlayerSettingsStorage.loadStreamAutoPlayReuseBingeGroup() ?: true nextEpisodeThresholdMode = PlayerSettingsStorage.loadNextEpisodeThresholdMode() @@ -687,6 +691,14 @@ object PlayerSettingsRepository { PlayerSettingsStorage.saveStreamAutoPlayNextEpisodeEnabled(enabled) } + fun setStreamAutoPlayNextEpisodeFallbackEnabled(enabled: Boolean) { + ensureLoaded() + if (streamAutoPlayNextEpisodeFallbackEnabled == enabled) return + streamAutoPlayNextEpisodeFallbackEnabled = enabled + publish() + PlayerSettingsStorage.saveStreamAutoPlayNextEpisodeFallbackEnabled(enabled) + } + fun setStreamAutoPlayPreferBingeGroup(enabled: Boolean) { ensureLoaded() if (streamAutoPlayPreferBingeGroup == enabled) return @@ -947,6 +959,7 @@ object PlayerSettingsRepository { introDbApiKey = introDbApiKey, introSubmitEnabled = introSubmitEnabled, streamAutoPlayNextEpisodeEnabled = streamAutoPlayNextEpisodeEnabled, + streamAutoPlayNextEpisodeFallbackEnabled = streamAutoPlayNextEpisodeFallbackEnabled, streamAutoPlayPreferBingeGroup = streamAutoPlayPreferBingeGroup, streamAutoPlayReuseBingeGroup = streamAutoPlayReuseBingeGroup, nextEpisodeThresholdMode = nextEpisodeThresholdMode, 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 312a0764b..d03a5f1f1 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 @@ -96,6 +96,8 @@ internal expect object PlayerSettingsStorage { fun saveIntroSubmitEnabled(enabled: Boolean) fun loadStreamAutoPlayNextEpisodeEnabled(): Boolean? fun saveStreamAutoPlayNextEpisodeEnabled(enabled: Boolean) + fun loadStreamAutoPlayNextEpisodeFallbackEnabled(): Boolean? + fun saveStreamAutoPlayNextEpisodeFallbackEnabled(enabled: Boolean) fun loadStreamAutoPlayPreferBingeGroup(): Boolean? fun saveStreamAutoPlayPreferBingeGroup(enabled: Boolean) fun loadStreamAutoPlayReuseBingeGroup(): Boolean? 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 3dcf5a169..28c3815e0 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 @@ -1100,6 +1100,17 @@ private fun PlaybackSettingsSection( isTablet = isTablet, onCheckedChange = PlayerSettingsRepository::setStreamAutoPlayNextEpisodeEnabled, ) + if (autoPlayPlayerSettings.streamAutoPlayNextEpisodeEnabled && + autoPlayPlayerSettings.streamAutoPlayMode == StreamAutoPlayMode.MANUAL) { + SettingsGroupDivider(isTablet = isTablet) + SettingsSwitchRow( + title = stringResource(Res.string.settings_playback_auto_play_next_episode_fallback), + description = stringResource(Res.string.settings_playback_auto_play_next_episode_fallback_description), + checked = autoPlayPlayerSettings.streamAutoPlayNextEpisodeFallbackEnabled, + isTablet = isTablet, + onCheckedChange = PlayerSettingsRepository::setStreamAutoPlayNextEpisodeFallbackEnabled, + ) + } SettingsGroupDivider(isTablet = isTablet) SettingsSwitchRow( title = stringResource(Res.string.settings_playback_prefer_binge_group), 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 4eeb657ae..e40332094 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 @@ -63,6 +63,7 @@ actual object PlayerSettingsStorage { private const val introDbApiKeyKey = "introdb_api_key" private const val introSubmitEnabledKey = "intro_submit_enabled" private const val streamAutoPlayNextEpisodeEnabledKey = "stream_auto_play_next_episode_enabled" + private const val streamAutoPlayNextEpisodeFallbackEnabledKey = "stream_auto_play_next_episode_fallback_enabled" private const val streamAutoPlayPreferBingeGroupKey = "stream_auto_play_prefer_binge_group" private const val streamAutoPlayReuseBingeGroupKey = "stream_auto_play_reuse_binge_group" private const val nextEpisodeThresholdModeKey = "next_episode_threshold_mode" @@ -130,6 +131,7 @@ actual object PlayerSettingsStorage { animeSkipEnabledKey, animeSkipClientIdKey, streamAutoPlayNextEpisodeEnabledKey, + streamAutoPlayNextEpisodeFallbackEnabledKey, streamAutoPlayPreferBingeGroupKey, streamAutoPlayReuseBingeGroupKey, nextEpisodeThresholdModeKey, @@ -718,6 +720,20 @@ actual object PlayerSettingsStorage { NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(streamAutoPlayNextEpisodeEnabledKey)) } + actual fun loadStreamAutoPlayNextEpisodeFallbackEnabled(): Boolean? { + val defaults = NSUserDefaults.standardUserDefaults + val key = ProfileScopedKey.of(streamAutoPlayNextEpisodeFallbackEnabledKey) + return if (defaults.objectForKey(key) != null) { + defaults.boolForKey(key) + } else { + null + } + } + + actual fun saveStreamAutoPlayNextEpisodeFallbackEnabled(enabled: Boolean) { + NSUserDefaults.standardUserDefaults.setBool(enabled, forKey = ProfileScopedKey.of(streamAutoPlayNextEpisodeFallbackEnabledKey)) + } + actual fun loadStreamAutoPlayPreferBingeGroup(): Boolean? { val defaults = NSUserDefaults.standardUserDefaults val key = ProfileScopedKey.of(streamAutoPlayPreferBingeGroupKey) @@ -939,6 +955,7 @@ actual object PlayerSettingsStorage { loadAnimeSkipEnabled()?.let { put(animeSkipEnabledKey, encodeSyncBoolean(it)) } loadAnimeSkipClientId()?.let { put(animeSkipClientIdKey, encodeSyncString(it)) } loadStreamAutoPlayNextEpisodeEnabled()?.let { put(streamAutoPlayNextEpisodeEnabledKey, encodeSyncBoolean(it)) } + loadStreamAutoPlayNextEpisodeFallbackEnabled()?.let { put(streamAutoPlayNextEpisodeFallbackEnabledKey, encodeSyncBoolean(it)) } loadStreamAutoPlayPreferBingeGroup()?.let { put(streamAutoPlayPreferBingeGroupKey, encodeSyncBoolean(it)) } loadStreamAutoPlayReuseBingeGroup()?.let { put(streamAutoPlayReuseBingeGroupKey, encodeSyncBoolean(it)) } loadNextEpisodeThresholdMode()?.let { put(nextEpisodeThresholdModeKey, encodeSyncString(it)) } @@ -1013,6 +1030,7 @@ actual object PlayerSettingsStorage { payload.decodeSyncString(animeSkipClientIdKey)?.let(::saveAnimeSkipClientId) payload.decodeSyncString(introDbApiKeyKey)?.let(::saveIntroDbApiKey) payload.decodeSyncBoolean(streamAutoPlayNextEpisodeEnabledKey)?.let(::saveStreamAutoPlayNextEpisodeEnabled) + payload.decodeSyncBoolean(streamAutoPlayNextEpisodeFallbackEnabledKey)?.let(::saveStreamAutoPlayNextEpisodeFallbackEnabled) payload.decodeSyncBoolean(streamAutoPlayPreferBingeGroupKey)?.let(::saveStreamAutoPlayPreferBingeGroup) payload.decodeSyncBoolean(streamAutoPlayReuseBingeGroupKey)?.let(::saveStreamAutoPlayReuseBingeGroup) payload.decodeSyncString(nextEpisodeThresholdModeKey)?.let(::saveNextEpisodeThresholdMode)