From 3529ae1b787e47c4540f94fc04c6c19f85e027d9 Mon Sep 17 00:00:00 2001
From: The-R0 <2861295907@qq.com>
Date: Thu, 12 Nov 2026 10:55:20 +0800
Subject: [PATCH] feat(player,settings): add toggle for hold-speed dial
(enabled by default)
---
.../composeResources/values-en/strings.xml | 4 +++-
.../composeResources/values/strings.xml | 4 +++-
.../kotlin/dev/bilby/data/SettingsStore.kt | 7 +++++++
.../kotlin/dev/bilby/ui/BilbyApp.kt | 2 ++
.../kotlin/dev/bilby/ui/player/PlayerShell.kt | 8 ++++++--
.../bilby/ui/player/SpeedSelectionGestures.kt | 9 +++++----
.../bilby/ui/player/SpeedSelectionOverlay.kt | 19 +++++++++++--------
.../dev/bilby/ui/settings/SettingsPages.kt | 15 ++++++++++++++-
.../bilby/ui/settings/SettingsViewModel.kt | 7 +++++++
.../kotlin/dev/bilby/ui/video/VideoScreen.kt | 10 ++++++----
10 files changed, 64 insertions(+), 21 deletions(-)
diff --git a/shared/src/commonMain/composeResources/values-en/strings.xml b/shared/src/commonMain/composeResources/values-en/strings.xml
index 899e7eb..9365cad 100644
--- a/shared/src/commonMain/composeResources/values-en/strings.xml
+++ b/shared/src/commonMain/composeResources/values-en/strings.xml
@@ -427,8 +427,10 @@
Gestures
Double-tap, hold speed and swipe controls
Hold for temporary speed
- Hold the picture to speed up, drag to adjust or lock
+ Hold the picture to speed up
Default hold speed
+ Hold speed dial
+ Show the speed dial while holding to drag or lock
Swipe horizontally to seek
Drag left or right on the picture to scrub
Swipe vertically for brightness and volume
diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml
index 03e044f..e337c67 100644
--- a/shared/src/commonMain/composeResources/values/strings.xml
+++ b/shared/src/commonMain/composeResources/values/strings.xml
@@ -510,8 +510,10 @@
快捷手势
双击进退、长按倍速与滑动调节
长按倍速
- 按住画面临时加速播放,滑动可切换倍速并锁定
+ 按住画面临时加速播放
默认长按倍速
+ 长按倍速条
+ 长按时展开倍速选档条,滑动可切换倍速并锁定
横向滑动调节进度
在画面上左右拖动跳转播放位置
纵向滑动调节亮度与音量
diff --git a/shared/src/commonMain/kotlin/dev/bilby/data/SettingsStore.kt b/shared/src/commonMain/kotlin/dev/bilby/data/SettingsStore.kt
index dba0194..e46d455 100644
--- a/shared/src/commonMain/kotlin/dev/bilby/data/SettingsStore.kt
+++ b/shared/src/commonMain/kotlin/dev/bilby/data/SettingsStore.kt
@@ -148,6 +148,7 @@ class SettingsStore(private val store: DataStore) {
playerPickUpdatesDefault = p[KEY_PLAYER_PICK_UPDATES_DEFAULT] ?: false,
doubleTapSeek = p[KEY_DOUBLE_TAP_SEEK] ?: true,
longPressSpeed = p[KEY_LONG_PRESS_SPEED] ?: true,
+ longPressSpeedDial = p[KEY_LONG_PRESS_SPEED_DIAL] ?: true,
defaultHoldSpeed = (p[KEY_DEFAULT_HOLD_SPEED] ?: p[KEY_LEGACY_HOLD_SPEED] ?: DEFAULT_HOLD_SPEED)
.takeIf { it in HOLD_SPEED_OPTIONS } ?: DEFAULT_HOLD_SPEED,
swipeSeek = p[KEY_SWIPE_SEEK] ?: true,
@@ -180,6 +181,10 @@ class SettingsStore(private val store: DataStore) {
store.edit { p -> p[KEY_LONG_PRESS_SPEED] = enabled }
}
+ suspend fun saveLongPressSpeedDial(enabled: Boolean) {
+ store.edit { p -> p[KEY_LONG_PRESS_SPEED_DIAL] = enabled }
+ }
+
suspend fun saveDefaultHoldSpeed(speed: Float) {
val valid = speed.takeIf { it in HOLD_SPEED_OPTIONS } ?: DEFAULT_HOLD_SPEED
store.edit { p -> p[KEY_DEFAULT_HOLD_SPEED] = valid }
@@ -538,6 +543,7 @@ class SettingsStore(private val store: DataStore) {
private val KEY_DOUBLE_TAP_SEEK = booleanPreferencesKey("player_double_tap_seek")
private val KEY_LONG_PRESS_SPEED = booleanPreferencesKey("player_long_press_speed")
+ private val KEY_LONG_PRESS_SPEED_DIAL = booleanPreferencesKey("player_long_press_speed_dial")
// 沿用 v0.18.1 已发布的键名,升级时不丢用户已选的默认长按倍速。
private val KEY_DEFAULT_HOLD_SPEED = floatPreferencesKey("player_fast_forward_speed")
private val KEY_LEGACY_HOLD_SPEED = floatPreferencesKey("player_default_hold_speed")
@@ -758,6 +764,7 @@ data class PlayerPrefs(
val playerPickUpdatesDefault: Boolean = false,
val doubleTapSeek: Boolean = true,
val longPressSpeed: Boolean = true,
+ val longPressSpeedDial: Boolean = true,
val defaultHoldSpeed: Float = SettingsStore.DEFAULT_HOLD_SPEED,
val swipeSeek: Boolean = true,
val swipeBrightnessVolume: Boolean = true,
diff --git a/shared/src/commonMain/kotlin/dev/bilby/ui/BilbyApp.kt b/shared/src/commonMain/kotlin/dev/bilby/ui/BilbyApp.kt
index 56c574c..b3b8b3c 100644
--- a/shared/src/commonMain/kotlin/dev/bilby/ui/BilbyApp.kt
+++ b/shared/src/commonMain/kotlin/dev/bilby/ui/BilbyApp.kt
@@ -1791,6 +1791,7 @@ private fun SettingsPageRoute(
state = state,
onDoubleTapSeekChange = vm::setDoubleTapSeek,
onLongPressSpeedChange = vm::setLongPressSpeed,
+ onLongPressSpeedDialChange = vm::setLongPressSpeedDial,
onDefaultHoldSpeedChange = vm::setDefaultHoldSpeed,
onSwipeSeekChange = vm::setSwipeSeek,
onSwipeBrightnessVolumeChange = vm::setSwipeBrightnessVolume,
@@ -3186,6 +3187,7 @@ private fun VideoPane(
doubleTapSeek = playerPrefs.doubleTapSeek,
brightnessAndVolume = playerPrefs.swipeBrightnessVolume,
fastForward = playerPrefs.longPressSpeed,
+ fastForwardDial = playerPrefs.longPressSpeedDial,
),
defaultHoldSpeed = playerPrefs.defaultHoldSpeed,
panelPrefs = panelPrefs,
diff --git a/shared/src/commonMain/kotlin/dev/bilby/ui/player/PlayerShell.kt b/shared/src/commonMain/kotlin/dev/bilby/ui/player/PlayerShell.kt
index 3c020a6..f75cd8c 100644
--- a/shared/src/commonMain/kotlin/dev/bilby/ui/player/PlayerShell.kt
+++ b/shared/src/commonMain/kotlin/dev/bilby/ui/player/PlayerShell.kt
@@ -125,6 +125,8 @@ data class PlayerGestureOptions(
val brightnessAndVolume: Boolean = true,
/** 长按选择临时倍速。 */
val fastForward: Boolean = true,
+ /** 长按时展开倍速选档条并允许滑动切换或锁定倍速;关闭后仅按默认倍速临时加速并在画面顶部显示提示。 */
+ val fastForwardDial: Boolean = true,
)
/**
@@ -932,7 +934,8 @@ fun PlayerShell(
.speedSelectionLongPress(
speedController,
enabled = gestures.fastForward && !locked && !pip,
- forceHorizontalBelow = !showSpeedOverlay,
+ dialEnabled = gestures.fastForwardDial,
+ forceHorizontalBelow = !showSpeedOverlay && gestures.fastForwardDial,
onStart = {
controlsVisible = false
gesture = null
@@ -1249,10 +1252,11 @@ fun PlayerShell(
}
}
- if (showSpeedOverlay && speedController != null && !pip) {
+ if ((showSpeedOverlay || !gestures.fastForwardDial) && speedController != null && !pip) {
SpeedSelectionOverlay(
controller = speedController,
isFullscreen = isFullscreen,
+ showDial = gestures.fastForwardDial,
modifier = Modifier.matchParentSize(),
)
}
diff --git a/shared/src/commonMain/kotlin/dev/bilby/ui/player/SpeedSelectionGestures.kt b/shared/src/commonMain/kotlin/dev/bilby/ui/player/SpeedSelectionGestures.kt
index edc97ac..903bb5d 100644
--- a/shared/src/commonMain/kotlin/dev/bilby/ui/player/SpeedSelectionGestures.kt
+++ b/shared/src/commonMain/kotlin/dev/bilby/ui/player/SpeedSelectionGestures.kt
@@ -15,6 +15,7 @@ import androidx.compose.ui.platform.LocalWindowInfo
internal fun Modifier.speedSelectionLongPress(
controller: PlaybackSpeedController?,
enabled: Boolean,
+ dialEnabled: Boolean = true,
forceHorizontalBelow: Boolean = false,
onStart: () -> Unit,
): Modifier {
@@ -22,7 +23,7 @@ internal fun Modifier.speedSelectionLongPress(
val haptics = LocalHapticFeedback.current
val window = LocalWindowInfo.current.containerSize
val vertical = !forceHorizontalBelow && window.width > window.height
- return pointerInput(controller, enabled, vertical, forceHorizontalBelow) {
+ return pointerInput(controller, enabled, dialEnabled, vertical, forceHorizontalBelow) {
if (controller == null || !enabled) return@pointerInput
var owned = false
var dockEnd = false
@@ -31,10 +32,10 @@ internal fun Modifier.speedSelectionLongPress(
try {
detectDragGesturesAfterLongPress(
onDragStart = { startOffset ->
- dockEnd = vertical && startOffset.x < size.width / 2f
+ dockEnd = dialEnabled && vertical && startOffset.x < size.width / 2f
// 竖屏内嵌时选档块画在画面下方(forceHorizontalBelow),一律下滑锁定;
// 竖屏全屏时偏下长按把选档条放画面上沿(上滑锁定),偏上长按放画面下沿(下滑锁定)。
- dockTop = !vertical && !forceHorizontalBelow && startOffset.y >= size.height / 2f
+ dockTop = dialEnabled && !vertical && !forceHorizontalBelow && startOffset.y >= size.height / 2f
controller.open(temporary = true, dockEnd = dockEnd, dockTop = dockTop)
owned = controller.beginDrag(vertical = vertical)
displacement = Offset.Zero
@@ -51,7 +52,7 @@ internal fun Modifier.speedSelectionLongPress(
},
) { change, amount ->
change.consume()
- if (!owned) return@detectDragGesturesAfterLongPress
+ if (!owned || !dialEnabled) return@detectDragGesturesAfterLongPress
displacement += amount
val before = controller.selection
val acrossPx = when {
diff --git a/shared/src/commonMain/kotlin/dev/bilby/ui/player/SpeedSelectionOverlay.kt b/shared/src/commonMain/kotlin/dev/bilby/ui/player/SpeedSelectionOverlay.kt
index 997b915..a61a6cc 100644
--- a/shared/src/commonMain/kotlin/dev/bilby/ui/player/SpeedSelectionOverlay.kt
+++ b/shared/src/commonMain/kotlin/dev/bilby/ui/player/SpeedSelectionOverlay.kt
@@ -119,6 +119,7 @@ private data class SpeedHintState(
private fun rememberSpeedHintState(
controller: PlaybackSpeedController,
forceHorizontalBelow: Boolean,
+ showDial: Boolean = true,
): SpeedHintState {
val selection = controller.selection
val window = LocalWindowInfo.current.containerSize
@@ -129,6 +130,7 @@ private fun rememberSpeedHintState(
val dockTop = !vertical && !forceHorizontalBelow && selection?.dockTop == true
val text = when {
locked != null -> stringResource(Res.string.player_speed_locked, formatSpeed(locked / 100f))
+ !showDial -> formatSpeed(selection?.speed ?: controller.speed)
releaseToLock -> stringResource(Res.string.player_speed_release_to_lock)
!vertical && dockTop -> stringResource(Res.string.player_speed_coarse_horizontal_top)
!vertical -> stringResource(Res.string.player_speed_coarse_horizontal)
@@ -185,6 +187,7 @@ private fun SpeedHintCapsule(
internal fun SpeedSelectionOverlay(
controller: PlaybackSpeedController,
isFullscreen: Boolean = false,
+ showDial: Boolean = true,
modifier: Modifier = Modifier,
) {
var shownSelection by remember { mutableStateOf(null) }
@@ -200,10 +203,10 @@ internal fun SpeedSelectionOverlay(
val activeOrLast = controller.selection ?: shownSelection
val dockEnd = vertical && activeOrLast?.dockEnd == true
val dockTop = !vertical && activeOrLast?.dockTop == true
- val hintState = rememberSpeedHintState(controller, forceHorizontalBelow = false)
+ val hintState = rememberSpeedHintState(controller, forceHorizontalBelow = false, showDial = showDial)
val focus = remember { FocusRequester() }
- BackHandler(enabled = controller.active) { controller.cancel() }
- LaunchedEffect(controller.active) { if (controller.active) focus.requestFocus() }
+ BackHandler(enabled = controller.active && showDial) { controller.cancel() }
+ LaunchedEffect(controller.active, showDial) { if (controller.active && showDial) focus.requestFocus() }
PlayerTheme {
val motion = MaterialTheme.motionScheme
val spatialOffset = motion.fastSpatialSpec()
@@ -211,7 +214,7 @@ internal fun SpeedSelectionOverlay(
Box(
modifier.fillMaxSize()
.onKeyEvent { event ->
- if (event.type != KeyEventType.KeyDown || !controller.active) return@onKeyEvent false
+ if (event.type != KeyEventType.KeyDown || !controller.active || !showDial) return@onKeyEvent false
when (event.key) {
Key.Escape -> controller.cancel()
Key.Enter -> { controller.lock(); controller.finish() }
@@ -238,13 +241,13 @@ internal fun SpeedSelectionOverlay(
else -> return@onKeyEvent false
}
true
- }.focusRequester(focus).focusable(),
+ }.then(if (showDial) Modifier.focusRequester(focus).focusable() else Modifier),
) {
- if (controller.active) {
+ if (controller.active && showDial) {
Box(Modifier.fillMaxSize().pointerInput(controller) { detectTapGestures { controller.finish() } })
}
AnimatedVisibility(
- visible = controller.active,
+ visible = controller.active && showDial,
enter = (when {
!vertical && dockTop -> slideInVertically(spatialOffset) { -it / 2 }
!vertical -> slideInVertically(spatialOffset) { it / 2 }
@@ -308,7 +311,7 @@ internal fun SpeedSelectionOverlay(
)
}
}
- val hintAtBottom = !vertical && dockTop
+ val hintAtBottom = showDial && !vertical && dockTop
val hintInsetSides = WindowInsetsSides.Horizontal +
if (hintAtBottom) WindowInsetsSides.Bottom else WindowInsetsSides.Top
AnimatedVisibility(
diff --git a/shared/src/commonMain/kotlin/dev/bilby/ui/settings/SettingsPages.kt b/shared/src/commonMain/kotlin/dev/bilby/ui/settings/SettingsPages.kt
index 267bbbf..52d13a7 100644
--- a/shared/src/commonMain/kotlin/dev/bilby/ui/settings/SettingsPages.kt
+++ b/shared/src/commonMain/kotlin/dev/bilby/ui/settings/SettingsPages.kt
@@ -452,6 +452,7 @@ fun GestureSettingsPage(
state: SettingsUiState,
onDoubleTapSeekChange: (Boolean) -> Unit,
onLongPressSpeedChange: (Boolean) -> Unit,
+ onLongPressSpeedDialChange: (Boolean) -> Unit = {},
onDefaultHoldSpeedChange: (Float) -> Unit,
onSwipeSeekChange: (Boolean) -> Unit,
onSwipeBrightnessVolumeChange: (Boolean) -> Unit,
@@ -490,9 +491,21 @@ fun GestureSettingsPage(
if (holdSpeedOnScreen) {
row { position ->
AnimatedVisibility(visibleState = holdSpeedShown, enter = enter, exit = exit) {
- ChoiceRow(
+ ToggleSettingRow(
position = position,
icon = Icons.Outlined.Tune,
+ title = stringResource(Res.string.settings_long_press_speed_dial),
+ subtitle = stringResource(Res.string.settings_long_press_speed_dial_subtitle),
+ checked = state.longPressSpeedDial,
+ onCheckedChange = onLongPressSpeedDialChange,
+ )
+ }
+ }
+ row { position ->
+ AnimatedVisibility(visibleState = holdSpeedShown, enter = enter, exit = exit) {
+ ChoiceRow(
+ position = position,
+ icon = Icons.Outlined.Speed,
title = stringResource(Res.string.settings_default_hold_speed),
options = SettingsStore.HOLD_SPEED_OPTIONS,
selected = state.defaultHoldSpeed,
diff --git a/shared/src/commonMain/kotlin/dev/bilby/ui/settings/SettingsViewModel.kt b/shared/src/commonMain/kotlin/dev/bilby/ui/settings/SettingsViewModel.kt
index 59c78ba..be3f1f4 100644
--- a/shared/src/commonMain/kotlin/dev/bilby/ui/settings/SettingsViewModel.kt
+++ b/shared/src/commonMain/kotlin/dev/bilby/ui/settings/SettingsViewModel.kt
@@ -51,6 +51,7 @@ data class SettingsUiState(
val hardwareCodecIds: Set = emptySet(),
val doubleTapSeek: Boolean = true,
val longPressSpeed: Boolean = true,
+ val longPressSpeedDial: Boolean = true,
val defaultHoldSpeed: Float = SettingsStore.DEFAULT_HOLD_SPEED,
val swipeSeek: Boolean = true,
val swipeBrightnessVolume: Boolean = true,
@@ -185,6 +186,7 @@ class SettingsViewModel(
playerPickUpdatesDefault = prefs.playerPickUpdatesDefault,
doubleTapSeek = prefs.doubleTapSeek,
longPressSpeed = prefs.longPressSpeed,
+ longPressSpeedDial = prefs.longPressSpeedDial,
defaultHoldSpeed = prefs.defaultHoldSpeed,
swipeSeek = prefs.swipeSeek,
swipeBrightnessVolume = prefs.swipeBrightnessVolume,
@@ -314,6 +316,11 @@ class SettingsViewModel(
persist { settings.saveLongPressSpeed(value) }
}
+ fun setLongPressSpeedDial(value: Boolean) {
+ _state.update { it.copy(longPressSpeedDial = value) }
+ persist { settings.saveLongPressSpeedDial(value) }
+ }
+
fun setDefaultHoldSpeed(value: Float) {
_state.update { it.copy(defaultHoldSpeed = value) }
persist { settings.saveDefaultHoldSpeed(value) }
diff --git a/shared/src/commonMain/kotlin/dev/bilby/ui/video/VideoScreen.kt b/shared/src/commonMain/kotlin/dev/bilby/ui/video/VideoScreen.kt
index 95f370f..4d7b547 100644
--- a/shared/src/commonMain/kotlin/dev/bilby/ui/video/VideoScreen.kt
+++ b/shared/src/commonMain/kotlin/dev/bilby/ui/video/VideoScreen.kt
@@ -1324,10 +1324,12 @@ fun VideoScreen(
.background(MaterialTheme.colorScheme.surface)
.windowInsetsPadding(WindowInsets.horizontalCutout),
)
- BelowVideoSpeedOverlay(
- controller = speedController,
- modifier = Modifier.fillMaxSize(),
- )
+ if (gestures.fastForwardDial) {
+ BelowVideoSpeedOverlay(
+ controller = speedController,
+ modifier = Modifier.fillMaxSize(),
+ )
+ }
}
}
}