This commit is contained in:
Sai Mukesh Cheekatla 2026-05-16 01:55:13 -05:00 committed by GitHub
commit 5201d1b443
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 265 additions and 3 deletions

View file

@ -56,6 +56,8 @@ actual object PlayerSettingsStorage {
private const val nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
private const val useLibassKey = "use_libass"
private const val libassRenderTypeKey = "libass_render_type"
private const val swipeToSeekEnabledKey = "swipe_to_seek_enabled"
private const val swipeToSeekSensitivityKey = "swipe_to_seek_sensitivity"
private val syncKeys = listOf(
showLoadingOverlayKey,
resizeModeKey,
@ -92,6 +94,8 @@ actual object PlayerSettingsStorage {
nextEpisodeThresholdMinutesBeforeEndKey,
useLibassKey,
libassRenderTypeKey,
swipeToSeekEnabledKey,
swipeToSeekSensitivityKey,
)
private var preferences: SharedPreferences? = null
@ -688,6 +692,8 @@ actual object PlayerSettingsStorage {
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
loadUseLibass()?.let { put(useLibassKey, encodeSyncBoolean(it)) }
loadLibassRenderType()?.let { put(libassRenderTypeKey, encodeSyncString(it)) }
loadSwipeToSeekEnabled()?.let { put(swipeToSeekEnabledKey, encodeSyncBoolean(it)) }
loadSwipeToSeekSensitivity()?.let { put(swipeToSeekSensitivityKey, encodeSyncString(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
@ -732,5 +738,34 @@ actual object PlayerSettingsStorage {
payload.decodeSyncFloat(nextEpisodeThresholdMinutesBeforeEndKey)?.let(::saveNextEpisodeThresholdMinutesBeforeEnd)
payload.decodeSyncBoolean(useLibassKey)?.let(::saveUseLibass)
payload.decodeSyncString(libassRenderTypeKey)?.let(::saveLibassRenderType)
payload.decodeSyncBoolean(swipeToSeekEnabledKey)?.let(::saveSwipeToSeekEnabled)
payload.decodeSyncString(swipeToSeekSensitivityKey)?.let(::saveSwipeToSeekSensitivity)
}
actual fun loadSwipeToSeekEnabled(): Boolean? =
preferences?.let { sharedPreferences ->
val key = ProfileScopedKey.of(swipeToSeekEnabledKey)
if (sharedPreferences.contains(key)) {
sharedPreferences.getBoolean(key, true)
} else {
null
}
}
actual fun saveSwipeToSeekEnabled(enabled: Boolean) {
preferences
?.edit()
?.putBoolean(ProfileScopedKey.of(swipeToSeekEnabledKey), enabled)
?.apply()
}
actual fun loadSwipeToSeekSensitivity(): String? =
preferences?.getString(ProfileScopedKey.of(swipeToSeekSensitivityKey), null)
actual fun saveSwipeToSeekSensitivity(sensitivity: String) {
preferences
?.edit()
?.putString(ProfileScopedKey.of(swipeToSeekSensitivityKey), sensitivity)
?.apply()
}
}

View file

@ -753,6 +753,13 @@
<string name="settings_playback_regex_matches_against">Matches against stream name/title/description/addon/url. Example: 4K|2160p|Remux</string>
<string name="settings_playback_regex_pattern">Regex Pattern</string>
<string name="settings_playback_regex_placeholder">No pattern set. Example: 4K|2160p|Remux</string>
<string name="settings_playback_swipe_to_seek">Swipe to Seek</string>
<string name="settings_playback_swipe_to_seek_description">Swipe horizontally on the player surface to seek forward or backward.</string>
<string name="settings_playback_swipe_to_seek_sensitivity">Swipe to Seek Sensitivity</string>
<string name="settings_playback_swipe_to_seek_sensitivity_description">Adjust the drag distance required to trigger a seek.</string>
<string name="settings_playback_swipe_to_seek_sensitivity_low">Low</string>
<string name="settings_playback_swipe_to_seek_sensitivity_medium">Medium</string>
<string name="settings_playback_swipe_to_seek_sensitivity_high">High</string>
<string name="settings_playback_regex_preset_any_1080p">Any 1080p+</string>
<string name="settings_playback_regex_preset_avc_x264">AVC / x264</string>
<string name="settings_playback_regex_preset_bluray_quality">BluRay Quality</string>

View file

@ -1552,8 +1552,10 @@ fun PlayerScreen(
if (gestureMode == null) {
val holdToSpeedActive = isHoldToSpeedGestureActiveState.value
val horizontalDominant =
!holdToSpeedActive &&
abs(totalDx) > viewConfiguration.touchSlop &&
playerSettingsUiState.swipeToSeekEnabled &&
!holdToSpeedActive &&
abs(totalDx) > viewConfiguration.touchSlop *
playerSettingsUiState.swipeToSeekSensitivity.triggerMultiplier &&
abs(totalDx) > abs(totalDy)
val verticalDominant =
!holdToSpeedActive &&
@ -1590,7 +1592,7 @@ fun PlayerScreen(
else -> 60f
}
val previewOffsetMs =
((totalDx / width) * sensitivitySeconds * 1000f).roundToLong()
((totalDx / width) * sensitivitySeconds * playerSettingsUiState.swipeToSeekSensitivity.speedMultiplier * 1000f).roundToLong()
val unclampedPreviewMs = horizontalSeekBaselineMs + previewOffsetMs
horizontalSeekPreviewMs = currentDurationMsState.value
.takeIf { it > 0L }

View file

@ -8,6 +8,24 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
enum class SwipeToSeekSensitivity {
LOW, MEDIUM, HIGH;
val triggerMultiplier: Float
get() = when (this) {
LOW -> 4.0f
MEDIUM -> 1.0f
HIGH -> 0.5f
}
val speedMultiplier: Float
get() = when (this) {
LOW -> 0.5f
MEDIUM -> 1.0f
HIGH -> 2.0f
}
}
data class PlayerSettingsUiState(
val showLoadingOverlay: Boolean = true,
val resizeMode: PlayerResizeMode = PlayerResizeMode.Fit,
@ -31,6 +49,8 @@ data class PlayerSettingsUiState(
val streamAutoPlaySelectedPlugins: Set<String> = emptySet(),
val streamAutoPlayRegex: String = "",
val streamAutoPlayTimeoutSeconds: Int = 3,
val swipeToSeekEnabled: Boolean = true,
val swipeToSeekSensitivity: SwipeToSeekSensitivity = SwipeToSeekSensitivity.MEDIUM,
val skipIntroEnabled: Boolean = true,
val animeSkipEnabled: Boolean = false,
val animeSkipClientId: String = "",
@ -84,6 +104,8 @@ object PlayerSettingsRepository {
private var nextEpisodeThresholdMinutesBeforeEnd = 2f
private var useLibass = false
private var libassRenderType = "CUES"
private var swipeToSeekEnabled = true
private var swipeToSeekSensitivity = SwipeToSeekSensitivity.MEDIUM
fun ensureLoaded() {
if (hasLoaded) return
@ -204,6 +226,12 @@ object PlayerSettingsRepository {
nextEpisodeThresholdMinutesBeforeEnd = PlayerSettingsStorage.loadNextEpisodeThresholdMinutesBeforeEnd() ?: 2f
useLibass = PlayerSettingsStorage.loadUseLibass() ?: false
libassRenderType = PlayerSettingsStorage.loadLibassRenderType() ?: "CUES"
swipeToSeekEnabled = PlayerSettingsStorage.loadSwipeToSeekEnabled() ?: true
swipeToSeekSensitivity = try {
SwipeToSeekSensitivity.valueOf(PlayerSettingsStorage.loadSwipeToSeekSensitivity() ?: "MEDIUM")
} catch (e: Exception) {
SwipeToSeekSensitivity.MEDIUM
}
publish()
}
@ -498,6 +526,22 @@ object PlayerSettingsRepository {
PlayerSettingsStorage.saveLibassRenderType(renderType)
}
fun setSwipeToSeekEnabled(enabled: Boolean) {
ensureLoaded()
if (swipeToSeekEnabled == enabled) return
swipeToSeekEnabled = enabled
publish()
PlayerSettingsStorage.saveSwipeToSeekEnabled(enabled)
}
fun setSwipeToSeekSensitivity(sensitivity: SwipeToSeekSensitivity) {
ensureLoaded()
if (swipeToSeekSensitivity == sensitivity) return
swipeToSeekSensitivity = sensitivity
publish()
PlayerSettingsStorage.saveSwipeToSeekSensitivity(sensitivity.name)
}
private fun publish() {
_uiState.value = PlayerSettingsUiState(
showLoadingOverlay = showLoadingOverlay,
@ -534,6 +578,8 @@ object PlayerSettingsRepository {
nextEpisodeThresholdMinutesBeforeEnd = nextEpisodeThresholdMinutesBeforeEnd,
useLibass = useLibass,
libassRenderType = libassRenderType,
swipeToSeekEnabled = swipeToSeekEnabled,
swipeToSeekSensitivity = swipeToSeekSensitivity,
)
}

View file

@ -78,6 +78,10 @@ internal expect object PlayerSettingsStorage {
fun saveUseLibass(enabled: Boolean)
fun loadLibassRenderType(): String?
fun saveLibassRenderType(renderType: String)
fun loadSwipeToSeekEnabled(): Boolean?
fun saveSwipeToSeekEnabled(enabled: Boolean)
fun loadSwipeToSeekSensitivity(): String?
fun saveSwipeToSeekSensitivity(sensitivity: String)
fun exportToSyncPayload(): JsonObject
fun replaceFromSyncPayload(payload: JsonObject)
}

View file

@ -56,6 +56,7 @@ import com.nuvio.app.features.player.AvailableLanguageOptions
import com.nuvio.app.features.player.ExternalPlayerApp
import com.nuvio.app.features.player.ExternalPlayerPlatform
import com.nuvio.app.features.player.PlayerSettingsRepository
import com.nuvio.app.features.player.SwipeToSeekSensitivity
import com.nuvio.app.features.player.SubtitleLanguageOption
import com.nuvio.app.features.player.formatPlaybackSpeedLabel
import com.nuvio.app.features.player.languageLabelForCode
@ -86,6 +87,8 @@ internal fun LazyListScope.playbackSettingsContent(
tunnelingEnabled: Boolean,
useLibass: Boolean,
libassRenderType: String,
swipeToSeekEnabled: Boolean,
swipeToSeekSensitivity: SwipeToSeekSensitivity,
) {
item {
PlaybackSettingsSection(
@ -104,6 +107,8 @@ internal fun LazyListScope.playbackSettingsContent(
tunnelingEnabled = tunnelingEnabled,
useLibass = useLibass,
libassRenderType = libassRenderType,
swipeToSeekEnabled = swipeToSeekEnabled,
swipeToSeekSensitivity = swipeToSeekSensitivity,
)
}
}
@ -166,6 +171,8 @@ private fun PlaybackSettingsSection(
tunnelingEnabled: Boolean,
useLibass: Boolean,
libassRenderType: String,
swipeToSeekEnabled: Boolean,
swipeToSeekSensitivity: SwipeToSeekSensitivity,
) {
var showPreferredAudioDialog by remember { mutableStateOf(false) }
var showSecondaryAudioDialog by remember { mutableStateOf(false) }
@ -181,6 +188,7 @@ private fun PlaybackSettingsSection(
var showAutoPlayAddonSelectionDialog by remember { mutableStateOf(false) }
var showAutoPlayPluginSelectionDialog by remember { mutableStateOf(false) }
var showAutoPlayRegexDialog by remember { mutableStateOf(false) }
var showSwipeSensitivityDialog by remember { mutableStateOf(false) }
val pluginsEnabled = AppFeaturePolicy.pluginsEnabled
val autoPlayPlayerSettings by PlayerSettingsRepository.uiState.collectAsStateWithLifecycle()
val availableExternalPlayers = ExternalPlayerPlatform.availablePlayers()
@ -262,6 +270,27 @@ private fun PlaybackSettingsSection(
onClick = { showHoldToSpeedValueDialog = true },
)
}
SettingsGroupDivider(isTablet = isTablet)
SettingsSwitchRow(
title = stringResource(Res.string.settings_playback_swipe_to_seek),
description = stringResource(Res.string.settings_playback_swipe_to_seek_description),
checked = swipeToSeekEnabled,
isTablet = isTablet,
onCheckedChange = PlayerSettingsRepository::setSwipeToSeekEnabled,
)
if (swipeToSeekEnabled) {
SettingsGroupDivider(isTablet = isTablet)
SettingsNavigationRow(
title = stringResource(Res.string.settings_playback_swipe_to_seek_sensitivity),
description = when (swipeToSeekSensitivity) {
SwipeToSeekSensitivity.LOW -> stringResource(Res.string.settings_playback_swipe_to_seek_sensitivity_low)
SwipeToSeekSensitivity.MEDIUM -> stringResource(Res.string.settings_playback_swipe_to_seek_sensitivity_medium)
SwipeToSeekSensitivity.HIGH -> stringResource(Res.string.settings_playback_swipe_to_seek_sensitivity_high)
},
isTablet = isTablet,
onClick = { showSwipeSensitivityDialog = true },
)
}
}
}
@ -865,6 +894,14 @@ private fun PlaybackSettingsSection(
)
}
if (showSwipeSensitivityDialog) {
SwipeToSeekSensitivityDialog(
selected = swipeToSeekSensitivity,
onSelect = PlayerSettingsRepository::setSwipeToSeekSensitivity,
onDismiss = { showSwipeSensitivityDialog = false },
)
}
if (showAutoPlayModeDialog) {
StreamAutoPlayModeDialog(
selectedMode = autoPlayPlayerSettings.streamAutoPlayMode,
@ -2324,3 +2361,89 @@ private fun libassRenderTypeRes(renderType: String): StringResource = when (rend
@Composable
private fun libassRenderTypeLabel(renderType: String): String = stringResource(libassRenderTypeRes(renderType))
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun SwipeToSeekSensitivityDialog(
selected: SwipeToSeekSensitivity,
onSelect: (SwipeToSeekSensitivity) -> Unit,
onDismiss: () -> Unit,
) {
val options = SwipeToSeekSensitivity.entries
BasicAlertDialog(onDismissRequest = onDismiss) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
color = MaterialTheme.colorScheme.surface,
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
text = stringResource(Res.string.settings_playback_swipe_to_seek_sensitivity),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.SemiBold,
)
Text(
text = stringResource(Res.string.settings_playback_swipe_to_seek_sensitivity_description),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
options.forEach { option ->
val isSelected = option == selected
val containerColor = if (isSelected) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)
} else {
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f)
}
Surface(
modifier = Modifier
.fillMaxWidth()
.clickable {
onSelect(option)
onDismiss()
},
shape = RoundedCornerShape(12.dp),
color = containerColor,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 14.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = when (option) {
SwipeToSeekSensitivity.LOW -> stringResource(Res.string.settings_playback_swipe_to_seek_sensitivity_low)
SwipeToSeekSensitivity.MEDIUM -> stringResource(Res.string.settings_playback_swipe_to_seek_sensitivity_medium)
SwipeToSeekSensitivity.HIGH -> stringResource(Res.string.settings_playback_swipe_to_seek_sensitivity_high)
else -> stringResource(Res.string.settings_playback_swipe_to_seek_sensitivity_medium)
},
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f),
)
if (isSelected) {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
}
}
}
}
}
}
}
}
}

View file

@ -68,6 +68,9 @@ import com.nuvio.app.features.mdblist.MdbListSettingsRepository
import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsRepository
import com.nuvio.app.features.notifications.EpisodeReleaseNotificationsUiState
import com.nuvio.app.features.player.PlayerSettingsRepository
import com.nuvio.app.features.player.PlayerResizeMode
import com.nuvio.app.features.player.SwipeToSeekSensitivity
import com.nuvio.app.features.player.skip.NextEpisodeThresholdMode
import com.nuvio.app.features.trakt.TraktAuthUiState
import com.nuvio.app.features.trakt.TraktAuthRepository
import com.nuvio.app.features.trakt.TraktCommentsSettings
@ -226,6 +229,8 @@ fun SettingsScreen(
tunnelingEnabled = playerSettingsUiState.tunnelingEnabled,
useLibass = playerSettingsUiState.useLibass,
libassRenderType = playerSettingsUiState.libassRenderType,
swipeToSeekEnabled = playerSettingsUiState.swipeToSeekEnabled,
swipeToSeekSensitivity = playerSettingsUiState.swipeToSeekSensitivity,
selectedTheme = selectedTheme,
onThemeSelected = ThemeSettingsRepository::setTheme,
amoledEnabled = amoledEnabled,
@ -274,6 +279,8 @@ fun SettingsScreen(
tunnelingEnabled = playerSettingsUiState.tunnelingEnabled,
useLibass = playerSettingsUiState.useLibass,
libassRenderType = playerSettingsUiState.libassRenderType,
swipeToSeekEnabled = playerSettingsUiState.swipeToSeekEnabled,
swipeToSeekSensitivity = playerSettingsUiState.swipeToSeekSensitivity,
selectedTheme = selectedTheme,
onThemeSelected = ThemeSettingsRepository::setTheme,
amoledEnabled = amoledEnabled,
@ -332,6 +339,8 @@ private fun MobileSettingsScreen(
tunnelingEnabled: Boolean,
useLibass: Boolean,
libassRenderType: String,
swipeToSeekEnabled: Boolean,
swipeToSeekSensitivity: SwipeToSeekSensitivity,
selectedTheme: AppTheme,
onThemeSelected: (AppTheme) -> Unit,
amoledEnabled: Boolean,
@ -493,6 +502,8 @@ private fun MobileSettingsScreen(
tunnelingEnabled = tunnelingEnabled,
useLibass = useLibass,
libassRenderType = libassRenderType,
swipeToSeekEnabled = swipeToSeekEnabled,
swipeToSeekSensitivity = swipeToSeekSensitivity,
)
SettingsPage.Appearance -> appearanceSettingsContent(
isTablet = false,
@ -639,6 +650,8 @@ private fun TabletSettingsScreen(
tunnelingEnabled: Boolean,
useLibass: Boolean,
libassRenderType: String,
swipeToSeekEnabled: Boolean,
swipeToSeekSensitivity: SwipeToSeekSensitivity,
selectedTheme: AppTheme,
onThemeSelected: (AppTheme) -> Unit,
amoledEnabled: Boolean,
@ -859,6 +872,8 @@ private fun TabletSettingsScreen(
tunnelingEnabled = tunnelingEnabled,
useLibass = useLibass,
libassRenderType = libassRenderType,
swipeToSeekEnabled = swipeToSeekEnabled,
swipeToSeekSensitivity = swipeToSeekSensitivity,
)
SettingsPage.Appearance -> appearanceSettingsContent(
isTablet = true,

View file

@ -54,6 +54,8 @@ actual object PlayerSettingsStorage {
private const val nextEpisodeThresholdMinutesBeforeEndKey = "next_episode_threshold_minutes_before_end_v2"
private const val useLibassKey = "use_libass"
private const val libassRenderTypeKey = "libass_render_type"
private const val swipeToSeekEnabledKey = "swipe_to_seek_enabled"
private const val swipeToSeekSensitivityKey = "swipe_to_seek_sensitivity"
private val syncKeys = listOf(
showLoadingOverlayKey,
resizeModeKey,
@ -90,6 +92,8 @@ actual object PlayerSettingsStorage {
nextEpisodeThresholdMinutesBeforeEndKey,
useLibassKey,
libassRenderTypeKey,
swipeToSeekEnabledKey,
swipeToSeekSensitivityKey,
)
actual fun loadShowLoadingOverlay(): Boolean? {
@ -588,6 +592,8 @@ actual object PlayerSettingsStorage {
loadNextEpisodeThresholdMinutesBeforeEnd()?.let { put(nextEpisodeThresholdMinutesBeforeEndKey, encodeSyncFloat(it)) }
loadUseLibass()?.let { put(useLibassKey, encodeSyncBoolean(it)) }
loadLibassRenderType()?.let { put(libassRenderTypeKey, encodeSyncString(it)) }
loadSwipeToSeekEnabled()?.let { put(swipeToSeekEnabledKey, encodeSyncBoolean(it)) }
loadSwipeToSeekSensitivity()?.let { put(swipeToSeekSensitivityKey, encodeSyncString(it)) }
}
actual fun replaceFromSyncPayload(payload: JsonObject) {
@ -631,5 +637,29 @@ actual object PlayerSettingsStorage {
payload.decodeSyncFloat(nextEpisodeThresholdMinutesBeforeEndKey)?.let(::saveNextEpisodeThresholdMinutesBeforeEnd)
payload.decodeSyncBoolean(useLibassKey)?.let(::saveUseLibass)
payload.decodeSyncString(libassRenderTypeKey)?.let(::saveLibassRenderType)
payload.decodeSyncBoolean(swipeToSeekEnabledKey)?.let(::saveSwipeToSeekEnabled)
payload.decodeSyncString(swipeToSeekSensitivityKey)?.let(::saveSwipeToSeekSensitivity)
}
actual fun loadSwipeToSeekEnabled(): Boolean? {
val defaults = NSUserDefaults.standardUserDefaults
val key = ProfileScopedKey.of(swipeToSeekEnabledKey)
return if (defaults.objectForKey(key) != null) {
defaults.boolForKey(key)
} else {
null
}
}
actual fun saveSwipeToSeekEnabled(enabled: Boolean) {
val defaults = NSUserDefaults.standardUserDefaults
defaults.setBool(enabled, ProfileScopedKey.of(swipeToSeekEnabledKey))
}
actual fun loadSwipeToSeekSensitivity(): String? =
NSUserDefaults.standardUserDefaults.stringForKey(ProfileScopedKey.of(swipeToSeekSensitivityKey))
actual fun saveSwipeToSeekSensitivity(sensitivity: String) {
NSUserDefaults.standardUserDefaults.setObject(sensitivity, ProfileScopedKey.of(swipeToSeekSensitivityKey))
}
}