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.
This commit is contained in:
skoruppa 2026-08-09 23:28:29 +02:00
parent 1d2ec889a0
commit f2c9b9f9e7
8 changed files with 71 additions and 1 deletions

View file

@ -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)

View file

@ -767,6 +767,8 @@
<string name="settings_playback_anime_skip_description">Wyszukuj również w AnimeSkip znaczniki pomijania (wymaga ID klienta).</string>
<string name="settings_playback_auto_play_next_episode">Automatyczne odtwarzanie następnego odcinka</string>
<string name="settings_playback_auto_play_next_episode_description">Automatycznie znajdź i odtwórz następny odcinek po osiągnięciu progu.</string>
<string name="settings_playback_auto_play_next_episode_fallback">Fallback gdy grupa binge nie zostanie znaleziona</string>
<string name="settings_playback_auto_play_next_episode_fallback_description">Automatycznie wybierz pierwszy dostępny strumień jeśli grupa binge nie zostanie znaleziona. Gdy wyłączone, wyświetla wybór źródła.</string>
<string name="settings_playback_decoder_device_only">Tylko urządzenie</string>
<string name="settings_playback_decoder_prefer_app">Preferuj aplikację (FFmpeg)</string>
<string name="settings_playback_decoder_prefer_device">Preferuj urządzenie</string>

View file

@ -928,6 +928,8 @@
<string name="settings_playback_anime_skip_description">Also search AnimeSkip for skip timestamps (requires client ID).</string>
<string name="settings_playback_auto_play_next_episode">Auto-play Next Episode</string>
<string name="settings_playback_auto_play_next_episode_description">Start next episode automatically when prompt appears.</string>
<string name="settings_playback_auto_play_next_episode_fallback">Fallback when binge group fails</string>
<string name="settings_playback_auto_play_next_episode_fallback_description">Auto-select first available stream if binge group is not found. When off, shows the stream picker instead.</string>
<string name="settings_playback_decoder_device_only">Device decoders only</string>
<string name="settings_playback_decoder_prefer_app">Prefer app decoders (FFmpeg)</string>
<string name="settings_playback_decoder_prefer_device">Prefer device decoders</string>

View file

@ -66,7 +66,8 @@ internal fun CoroutineScope.launchPlayerNextEpisodeAutoPlay(
val bingeGroupOnlyManualMode =
shouldAutoSelectInManualMode &&
!settings.streamAutoPlayNextEpisodeEnabled &&
(!settings.streamAutoPlayNextEpisodeEnabled ||
!settings.streamAutoPlayNextEpisodeFallbackEnabled) &&
settings.streamAutoPlayPreferBingeGroup
val effectiveMode = if (shouldAutoSelectInManualMode) {

View file

@ -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,

View file

@ -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?

View file

@ -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),

View file

@ -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)