From 1c777307faba43a7aa8e17749eb06c5033d67c78 Mon Sep 17 00:00:00 2001 From: Ra77a3l3-jar Date: Fri, 4 Sep 2026 21:19:35 +0200 Subject: [PATCH 1/4] feat(playback): seek back and forward with arrow keys --- crates/input/src/lib.rs | 4 ++++ crates/sonora/src/actions.rs | 14 +++++++++++++- crates/state/src/playback.rs | 21 +++++++++++++++++++++ crates/state/src/remote.rs | 29 ++++++++--------------------- crates/views/src/root.rs | 12 ++++++++++-- 5 files changed, 56 insertions(+), 24 deletions(-) diff --git a/crates/input/src/lib.rs b/crates/input/src/lib.rs index d0a38c06..8323b5a1 100644 --- a/crates/input/src/lib.rs +++ b/crates/input/src/lib.rs @@ -15,6 +15,8 @@ actions!( TogglePlayback, SongPrevious, SongNext, + SeekBack, + SeekForward, NavigateBack, NavigateForward, OpenFilter, @@ -56,6 +58,8 @@ pub fn bindings() -> Vec { KeyBinding::new("enter", Activate, Some(&browsing)), KeyBinding::new("delete", Remove, Some(&browsing)), KeyBinding::new("escape", Deselect, Some(&browsing)), + KeyBinding::new("left", SeekBack, None), + KeyBinding::new("right", SeekForward, None), KeyBinding::new("down", SelectNext, search), KeyBinding::new("up", SelectPrevious, search), KeyBinding::new("left", SelectLeft, Some(&results)), diff --git a/crates/sonora/src/actions.rs b/crates/sonora/src/actions.rs index 794d1646..a5952017 100644 --- a/crates/sonora/src/actions.rs +++ b/crates/sonora/src/actions.rs @@ -1,6 +1,8 @@ use gpui::{App, Menu, MenuItem}; use i18n::t; -use input::{Quit, RefreshLibrary, SignOut, SongNext, SongPrevious, TogglePlayback}; +use input::{ + Quit, RefreshLibrary, SeekBack, SeekForward, SignOut, SongNext, SongPrevious, TogglePlayback, +}; use router::Destination; use state::Sonora; @@ -54,6 +56,16 @@ pub fn register(lingers: bool, cx: &mut App) { playback.update(cx, |playback, cx| playback.next(cx)); }); + cx.on_action(|_: &SeekBack, cx: &mut App| { + let playback = Sonora::global(cx).playback.clone(); + playback.update(cx, |playback, cx| playback.seek_back(cx)); + }); + + cx.on_action(|_: &SeekForward, cx: &mut App| { + let playback = Sonora::global(cx).playback.clone(); + playback.update(cx, |playback, cx| playback.seek_forward(cx)); + }); + cx.set_menus(vec![Menu { name: "Sonora".into(), disabled: false, diff --git a/crates/state/src/playback.rs b/crates/state/src/playback.rs index 67d7c7d2..83b3ee93 100644 --- a/crates/state/src/playback.rs +++ b/crates/state/src/playback.rs @@ -64,6 +64,7 @@ const SKIP_DEBOUNCE: Duration = Duration::from_millis(250); const RESTART_WINDOW: Duration = Duration::from_secs(3); const KEY_COOLDOWN: Duration = Duration::from_secs(6); const RESUME_STEP: Duration = Duration::from_secs(5); +const SEEK_STEP: Duration = Duration::from_secs(5); const TAPER_DB: f32 = 50.; const LOCAL_FAVORITES: &str = "favorites"; const SIMILAR_LIMIT: usize = 20; @@ -1244,6 +1245,26 @@ impl Playback { self.seek(position, cx); } + pub fn seek_by(&mut self, step: Duration, forward: bool, cx: &mut Context) { + let Some(end) = self.track.as_ref().map(|track| track.duration) else { + return; + }; + let at = self.live_position(); + let target = match forward { + true => at.saturating_add(step).min(end), + false => at.saturating_sub(step), + }; + self.seek(target, cx); + } + + pub fn seek_back(&mut self, cx: &mut Context) { + self.seek_by(SEEK_STEP, false, cx); + } + + pub fn seek_forward(&mut self, cx: &mut Context) { + self.seek_by(SEEK_STEP, true, cx); + } + pub fn state(&self) -> &PlaybackState { &self.state } diff --git a/crates/state/src/remote.rs b/crates/state/src/remote.rs index c9445a83..9a6cffb9 100644 --- a/crates/state/src/remote.rs +++ b/crates/state/src/remote.rs @@ -12,7 +12,6 @@ use crate::{Playback, PlaybackState, Sonora}; const BUS_NAME: &str = "sonora"; const DISPLAY_NAME: &str = "Sonora"; -const SEEK_STEP: Duration = Duration::from_secs(5); struct Attached { _remote: Entity, @@ -94,8 +93,14 @@ impl Remote { MediaControlEvent::Next => playback.next(cx), MediaControlEvent::Previous => playback.previous(cx), MediaControlEvent::SetPosition(MediaPosition(at)) => playback.seek(at, cx), - MediaControlEvent::Seek(direction) => shift(playback, direction, SEEK_STEP, cx), - MediaControlEvent::SeekBy(direction, step) => shift(playback, direction, step, cx), + MediaControlEvent::Seek(SeekDirection::Forward) => playback.seek_forward(cx), + MediaControlEvent::Seek(SeekDirection::Backward) => playback.seek_back(cx), + MediaControlEvent::SeekBy(SeekDirection::Forward, step) => { + playback.seek_by(step, true, cx) + } + MediaControlEvent::SeekBy(SeekDirection::Backward, step) => { + playback.seek_by(step, false, cx) + } MediaControlEvent::SetVolume(level) => playback.set_volume(level as f32, cx), MediaControlEvent::OpenUri(_) | MediaControlEvent::Raise @@ -144,21 +149,3 @@ impl Remote { } } } - -fn shift( - playback: &mut Playback, - direction: SeekDirection, - step: Duration, - cx: &mut Context, -) { - let at = playback.position(); - let target = match direction { - SeekDirection::Forward => at.saturating_add(step), - SeekDirection::Backward => at.saturating_sub(step), - }; - let end = playback - .track() - .map(|track| track.duration) - .unwrap_or(target); - playback.seek(target.min(end), cx); -} diff --git a/crates/views/src/root.rs b/crates/views/src/root.rs index c94c614d..b67b0537 100644 --- a/crates/views/src/root.rs +++ b/crates/views/src/root.rs @@ -2,8 +2,8 @@ use gpui::{AnyView, Context, Entity, MouseButton, NavigationDirection, Render, T use gpui::{App, Font, FontFallbacks, SharedString, font, prelude::*}; use gpui::{Window, div}; use input::{ - NavigateBack, NavigateForward, OpenFilter, OpenSearch, OpenSettings, ToggleFullscreen, - ToggleLyrics, ToggleQueue, + NavigateBack, NavigateForward, OpenFilter, OpenSearch, OpenSettings, SeekBack, SeekForward, + ToggleFullscreen, ToggleLyrics, ToggleQueue, }; use router::{Destination, NavigationEvent, SettingsTab, back, forward, navigate}; use state::{ @@ -588,6 +588,14 @@ impl Render for Root { ) .on_action(cx.listener(|_, _: &NavigateBack, _, cx| back(cx))) .on_action(cx.listener(|_, _: &NavigateForward, _, cx| forward(cx))) + .on_action(cx.listener(|this, _: &SeekBack, _, cx| { + this.playback + .update(cx, |playback, cx| playback.seek_back(cx)); + })) + .on_action(cx.listener(|this, _: &SeekForward, _, cx| { + this.playback + .update(cx, |playback, cx| playback.seek_forward(cx)); + })) .on_action(cx.listener(|this, _: &OpenFilter, window, cx| this.open_filter(window, cx))) .on_action(cx.listener(|this, _: &OpenSearch, _, cx| this.open_search(cx))) .on_action(cx.listener(|this, _: &OpenSettings, _, cx| this.open_settings(cx))) From d1bf5a610e9cf4eb21c5010a07d70dee626fe67b Mon Sep 17 00:00:00 2001 From: Ra77a3l3-jar Date: Fri, 4 Sep 2026 21:52:12 +0200 Subject: [PATCH 2/4] feat(views): add seek buttons around play/pause --- CHANGELOG.md | 5 +++++ assets/i18n/en-US/main.ftl | 2 ++ assets/i18n/it/main.ftl | 2 ++ assets/i18n/pl/main.ftl | 2 ++ assets/icons/iconoir/rotate-ccw.svg | 1 + assets/icons/iconoir/rotate-cw.svg | 1 + assets/icons/lucide/rotate-ccw.svg | 14 ++++++++++++ assets/icons/lucide/rotate-cw.svg | 14 ++++++++++++ assets/icons/remix/rotate-ccw.svg | 1 + assets/icons/remix/rotate-cw.svg | 1 + assets/icons/solar/rotate-ccw.svg | 1 + assets/icons/solar/rotate-cw.svg | 1 + crates/views/src/shared/transport.rs | 32 ++++++++++++++++++++++++++++ scripts/fetch-icons.py | 2 ++ 14 files changed, 79 insertions(+) create mode 100644 assets/icons/iconoir/rotate-ccw.svg create mode 100644 assets/icons/iconoir/rotate-cw.svg create mode 100644 assets/icons/lucide/rotate-ccw.svg create mode 100644 assets/icons/lucide/rotate-cw.svg create mode 100644 assets/icons/remix/rotate-ccw.svg create mode 100644 assets/icons/remix/rotate-cw.svg create mode 100644 assets/icons/solar/rotate-ccw.svg create mode 100644 assets/icons/solar/rotate-cw.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe37acd..37e61d33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +- Seek a few seconds back or forward with the left and right arrow keys, or the buttons beside + play/pause. + ## [0.31.0] - 2026-09-05 ### Added diff --git a/assets/i18n/en-US/main.ftl b/assets/i18n/en-US/main.ftl index b9ed7595..e6c73c32 100644 --- a/assets/i18n/en-US/main.ftl +++ b/assets/i18n/en-US/main.ftl @@ -203,6 +203,8 @@ player-mute = Mute player-unmute = Unmute player-previous = Previous track player-next = Next track +player-seek-back = Seek back +player-seek-forward = Seek forward player-fullscreen = Fullscreen player-fullscreen-leave = Leave fullscreen fullscreen-artwork = Artwork diff --git a/assets/i18n/it/main.ftl b/assets/i18n/it/main.ftl index 724b876f..cdfedcc5 100644 --- a/assets/i18n/it/main.ftl +++ b/assets/i18n/it/main.ftl @@ -136,6 +136,8 @@ player-mute = Silenzia player-unmute = Riattiva audio player-previous = Brano precedente player-next = Brano successivo +player-seek-back = Torna indietro +player-seek-forward = Vai avanti player-fullscreen = Schermo intero player-fullscreen-leave = Esci dallo schermo intero fullscreen-artwork = Copertina diff --git a/assets/i18n/pl/main.ftl b/assets/i18n/pl/main.ftl index a6874001..93f56338 100644 --- a/assets/i18n/pl/main.ftl +++ b/assets/i18n/pl/main.ftl @@ -217,6 +217,8 @@ player-mute = Wycisz player-unmute = Wyłącz wyciszenie player-previous = Poprzedni utwór player-next = Następny utwór +player-seek-back = Przewiń wstecz +player-seek-forward = Przewiń do przodu player-fullscreen = Pełny ekran player-fullscreen-leave = Wyjdź z trybu pełnoekranowego fullscreen-artwork = Okładka diff --git a/assets/icons/iconoir/rotate-ccw.svg b/assets/icons/iconoir/rotate-ccw.svg new file mode 100644 index 00000000..659104e1 --- /dev/null +++ b/assets/icons/iconoir/rotate-ccw.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/iconoir/rotate-cw.svg b/assets/icons/iconoir/rotate-cw.svg new file mode 100644 index 00000000..6796ade1 --- /dev/null +++ b/assets/icons/iconoir/rotate-cw.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/lucide/rotate-ccw.svg b/assets/icons/lucide/rotate-ccw.svg new file mode 100644 index 00000000..eb0fe349 --- /dev/null +++ b/assets/icons/lucide/rotate-ccw.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/lucide/rotate-cw.svg b/assets/icons/lucide/rotate-cw.svg new file mode 100644 index 00000000..6795f157 --- /dev/null +++ b/assets/icons/lucide/rotate-cw.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/remix/rotate-ccw.svg b/assets/icons/remix/rotate-ccw.svg new file mode 100644 index 00000000..d7180f1e --- /dev/null +++ b/assets/icons/remix/rotate-ccw.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/remix/rotate-cw.svg b/assets/icons/remix/rotate-cw.svg new file mode 100644 index 00000000..0ea16ec9 --- /dev/null +++ b/assets/icons/remix/rotate-cw.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/solar/rotate-ccw.svg b/assets/icons/solar/rotate-ccw.svg new file mode 100644 index 00000000..94f5c6d8 --- /dev/null +++ b/assets/icons/solar/rotate-ccw.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/solar/rotate-cw.svg b/assets/icons/solar/rotate-cw.svg new file mode 100644 index 00000000..31f73bc6 --- /dev/null +++ b/assets/icons/solar/rotate-cw.svg @@ -0,0 +1 @@ + diff --git a/crates/views/src/shared/transport.rs b/crates/views/src/shared/transport.rs index c6008344..52f7b5c5 100644 --- a/crates/views/src/shared/transport.rs +++ b/crates/views/src/shared/transport.rs @@ -62,7 +62,9 @@ pub(crate) fn transport( .gap_2() .child(shuffle(queue, cx)) .child(previous(playback, cx)) + .child(seek_back(playback, cx)) .child(toggle(playback, big, cx)) + .child(seek_forward(playback, cx)) .child(next(playback, queue, cx)) .child(repeat(playback, cx)) } @@ -134,6 +136,36 @@ fn repeat(playback: &Entity, cx: &App) -> Button { }) } +fn seek_back(playback: &Entity, cx: &App) -> Button { + let idle = playback.read(cx).track().is_none(); + let playback = playback.clone(); + + Button::new("seek-back") + .ghost() + .small() + .icon("icons/rotate-ccw.svg") + .tooltip_above("player-seek-back") + .disabled(idle) + .on_click(move |_, _, cx| { + playback.update(cx, |playback, cx| playback.seek_back(cx)); + }) +} + +fn seek_forward(playback: &Entity, cx: &App) -> Button { + let idle = playback.read(cx).track().is_none(); + let playback = playback.clone(); + + Button::new("seek-forward") + .ghost() + .small() + .icon("icons/rotate-cw.svg") + .tooltip_above("player-seek-forward") + .disabled(idle) + .on_click(move |_, _, cx| { + playback.update(cx, |playback, cx| playback.seek_forward(cx)); + }) +} + fn previous(playback: &Entity, cx: &App) -> Button { let enabled = playback.read(cx).has_previous(cx); let playback = playback.clone(); diff --git a/scripts/fetch-icons.py b/scripts/fetch-icons.py index 78f38bea..09eb5cce 100755 --- a/scripts/fetch-icons.py +++ b/scripts/fetch-icons.py @@ -76,6 +76,7 @@ "shuffle": ("shuffle", "shuffle-linear", "shuffle-line"), "skip-back": ("skip-prev", "skip-previous-linear", "skip-back-line"), "skip-forward": ("skip-next", "skip-next-linear", "skip-forward-line"), + "rotate-cw": ("restart", "restart-linear", "restart-line"), "sliders-horizontal": ("control-slider", "slider-horizontal-linear", "equalizer-line"), "text-select": (None, "text-selection-linear", None), "trash-2": ("trash", "trash-bin-minimalistic-linear", "delete-bin-line"), @@ -93,6 +94,7 @@ MIRROR = { "panel-right-close": "panel-left-close", "panel-right-open": "panel-left-open", + "rotate-ccw": "rotate-cw", } SLASH = { From adf0de78248cb4e285b14ef57a941b8c683e1acc Mon Sep 17 00:00:00 2001 From: Ra77a3l3-jar Date: Sat, 5 Sep 2026 15:15:00 +0200 Subject: [PATCH 3/4] fix(playback): keep one scrubber thumb while seeking --- crates/state/src/playback.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/state/src/playback.rs b/crates/state/src/playback.rs index 83b3ee93..879acc0c 100644 --- a/crates/state/src/playback.rs +++ b/crates/state/src/playback.rs @@ -65,6 +65,8 @@ const RESTART_WINDOW: Duration = Duration::from_secs(3); const KEY_COOLDOWN: Duration = Duration::from_secs(6); const RESUME_STEP: Duration = Duration::from_secs(5); const SEEK_STEP: Duration = Duration::from_secs(5); +const SEEK_CATCHUP: Duration = Duration::from_millis(400); +const SEEK_HOLD: Duration = Duration::from_secs(2); const TAPER_DB: f32 = 50.; const LOCAL_FAVORITES: &str = "favorites"; const SIMILAR_LIMIT: usize = 20; @@ -121,6 +123,13 @@ impl LiveClock { } } +fn away(left: Duration, right: Duration) -> Duration { + match left >= right { + true => left - right, + false => right - left, + } +} + fn signed_gap(to: Duration, from: Duration) -> f64 { match to >= from { true => (to - from).as_secs_f64(), @@ -277,6 +286,7 @@ pub struct Playback { refused: Option, resume_at: Option, seek_on_play: Option, + sought: Option<(Duration, Instant)>, resume_ready: bool, awaiting_reconnect: bool, stored: Duration, @@ -353,6 +363,7 @@ impl Playback { refused: None, resume_at: None, seek_on_play: None, + sought: None, resume_ready: false, awaiting_reconnect: false, stored: Duration::ZERO, @@ -1218,6 +1229,7 @@ impl Playback { } self.position = position; self.clock.reset(position, false); + self.sought = Some((position, Instant::now())); self.remember(true, cx); cx.notify(); return; @@ -1227,6 +1239,7 @@ impl Playback { self.position = position; self.clock .reset(position, self.state == PlaybackState::Playing); + self.sought = Some((position, Instant::now())); cx.notify(); } } @@ -1265,6 +1278,13 @@ impl Playback { self.seek_by(SEEK_STEP, true, cx); } + fn holding_seek(&self, reported: Duration) -> bool { + let Some((target, since)) = self.sought else { + return false; + }; + since.elapsed() < SEEK_HOLD && away(reported, target) > SEEK_CATCHUP + } + pub fn state(&self) -> &PlaybackState { &self.state } @@ -1525,7 +1545,9 @@ impl Playback { self.clock.reset(position, false); self.remember(true, cx); } + BackendEvent::Position(position) if self.holding_seek(position) => {} BackendEvent::Position(position) => { + self.sought = None; self.position = position; match self.state == PlaybackState::Playing { true => self.clock.correct(position), @@ -1602,6 +1624,7 @@ impl Playback { self.clock.reset(Duration::ZERO, false); self.resume_at = None; self.seek_on_play = None; + self.sought = None; self.resume_ready = false; self.awaiting_reconnect = false; self.stored = Duration::ZERO; From 0cbe594283c6300bf14502e487c007941b038a0a Mon Sep 17 00:00:00 2001 From: Ra77a3l3-jar Date: Sat, 5 Sep 2026 15:27:23 +0200 Subject: [PATCH 4/4] feat(playback): add a 5/10/30s seek step --- CHANGELOG.md | 2 +- assets/i18n/en-US/main.ftl | 5 +++ assets/i18n/it/main.ftl | 5 +++ assets/i18n/pl/main.ftl | 5 +++ crates/state/src/lib.rs | 2 +- crates/state/src/playback.rs | 47 ++++++++++++++++++++++++++-- crates/state/src/settings.rs | 16 +++++++++- crates/views/src/screens/settings.rs | 41 +++++++++++++++++++++++- 8 files changed, 116 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37e61d33..3633bb4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added - Seek a few seconds back or forward with the left and right arrow keys, or the buttons beside - play/pause. + play/pause. The jump is 5, 10 or 30 seconds, set under Settings > Playback. ## [0.31.0] - 2026-09-05 diff --git a/assets/i18n/en-US/main.ftl b/assets/i18n/en-US/main.ftl index e6c73c32..a25fb162 100644 --- a/assets/i18n/en-US/main.ftl +++ b/assets/i18n/en-US/main.ftl @@ -480,6 +480,11 @@ settings-normalisation = Normalize loudness settings-normalisation-detail = Keeps tracks at a consistent volume settings-gapless = Gapless playback settings-gapless-detail = Runs one track into the next without a pause, the way an album was sequenced +settings-seek-step = Seek step +settings-seek-step-detail = How far the seek buttons and arrow keys jump +seek-step-5 = 5 seconds +seek-step-10 = 10 seconds +seek-step-30 = 30 seconds settings-panel-lyrics-size = Lyrics size (panel) settings-panel-lyrics-size-detail = Size of the lyrics text in the side panel, on top of the base font size settings-fullscreen-lyrics-size = Lyrics size (fullscreen) diff --git a/assets/i18n/it/main.ftl b/assets/i18n/it/main.ftl index cdfedcc5..5b4b3cba 100644 --- a/assets/i18n/it/main.ftl +++ b/assets/i18n/it/main.ftl @@ -405,6 +405,11 @@ settings-normalisation = Normalizza volume settings-normalisation-detail = Mantiene le tracce a un volume uniforme settings-gapless = Riproduzione senza pause settings-gapless-detail = Fa scorrere un brano nell'altro senza pausa, come è stato sequenziato l'album +settings-seek-step = Passo di scorrimento +settings-seek-step-detail = Di quanto saltano i pulsanti e i tasti freccia +seek-step-5 = 5 secondi +seek-step-10 = 10 secondi +seek-step-30 = 30 secondi settings-karaoke-lyrics = Testo karaoke settings-karaoke-lyrics-detail = Evidenzia il testo parola per parola quando la sincronizzazione è disponibile settings-romanized-lyrics = Testo romanizzato diff --git a/assets/i18n/pl/main.ftl b/assets/i18n/pl/main.ftl index 93f56338..6211ec4e 100644 --- a/assets/i18n/pl/main.ftl +++ b/assets/i18n/pl/main.ftl @@ -506,6 +506,11 @@ settings-normalisation = Normalizacja głośności settings-normalisation-detail = Utrzymuje stałą głośność utworów settings-gapless = Odtwarzanie bez przerw settings-gapless-detail = Przechodzi z utworu do następnego bez pauzy, zgodnie z układem albumu +settings-seek-step = Skok przewijania +settings-seek-step-detail = O ile przeskakują przyciski i klawisze strzałek +seek-step-5 = 5 sekund +seek-step-10 = 10 sekund +seek-step-30 = 30 sekund settings-panel-lyrics-size = Rozmiar tekstu utworu (panel) settings-panel-lyrics-size-detail = Rozmiar wierszy tekstu utworu w panelu bocznym ponad bazowy rozmiar czcionki settings-fullscreen-lyrics-size = Rozmiar tekstu utworu (pełny ekran) diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index a9b3d9bb..b065e09f 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -30,7 +30,7 @@ pub use history::{History, HistoryState}; pub use home::Home; pub use library::{Library, LibraryEvent, LibraryPart, LibraryState, Problem}; pub use lyrics::{Lyrics, LyricsState}; -pub use playback::{Origin, Playback, PlaybackState, Repeat, Whence}; +pub use playback::{Origin, Playback, PlaybackState, Repeat, SeekStep, Whence}; pub use profile::Profile; pub use queue::{Named, Queue, Resume, Stub}; pub use remote::{Remote, attach as attach_remote}; diff --git a/crates/state/src/playback.rs b/crates/state/src/playback.rs index 879acc0c..3e6f9cea 100644 --- a/crates/state/src/playback.rs +++ b/crates/state/src/playback.rs @@ -64,7 +64,6 @@ const SKIP_DEBOUNCE: Duration = Duration::from_millis(250); const RESTART_WINDOW: Duration = Duration::from_secs(3); const KEY_COOLDOWN: Duration = Duration::from_secs(6); const RESUME_STEP: Duration = Duration::from_secs(5); -const SEEK_STEP: Duration = Duration::from_secs(5); const SEEK_CATCHUP: Duration = Duration::from_millis(400); const SEEK_HOLD: Duration = Duration::from_secs(2); const TAPER_DB: f32 = 50.; @@ -174,6 +173,46 @@ pub enum Repeat { One, } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum SeekStep { + Five, + #[default] + Ten, + Thirty, +} + +impl SeekStep { + pub const ALL: [Self; 3] = [Self::Five, Self::Ten, Self::Thirty]; + + pub fn id(self) -> &'static str { + match self { + Self::Five => "5", + Self::Ten => "10", + Self::Thirty => "30", + } + } + + pub fn secs(self) -> u16 { + match self { + Self::Five => 5, + Self::Ten => 10, + Self::Thirty => 30, + } + } + + pub fn duration(self) -> Duration { + Duration::from_secs(u64::from(self.secs())) + } + + pub fn from_secs(secs: u16) -> Self { + match secs { + 5 => Self::Five, + 30 => Self::Thirty, + _ => Self::Ten, + } + } +} + #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Whence { @@ -1271,11 +1310,13 @@ impl Playback { } pub fn seek_back(&mut self, cx: &mut Context) { - self.seek_by(SEEK_STEP, false, cx); + let step = self.settings.read(cx).seek_step().duration(); + self.seek_by(step, false, cx); } pub fn seek_forward(&mut self, cx: &mut Context) { - self.seek_by(SEEK_STEP, true, cx); + let step = self.settings.read(cx).seek_step().duration(); + self.seek_by(step, true, cx); } fn holding_seek(&self, reported: Duration) -> bool { diff --git a/crates/state/src/settings.rs b/crates/state/src/settings.rs index 20fe4c17..ef8003c8 100644 --- a/crates/state/src/settings.rs +++ b/crates/state/src/settings.rs @@ -27,7 +27,7 @@ use ui::{ }; use crate::queue::{Resume, gap_target}; -use crate::{Repeat, Sonora}; +use crate::{Repeat, SeekStep, Sonora}; /// Which panel the right sidebar shows. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -179,6 +179,7 @@ struct Values { version: u32, normalisation: bool, gapless: bool, + seek_step: u16, lyrics_for_local_files: bool, karaoke_lyrics: bool, blur_lyrics: bool, @@ -229,6 +230,7 @@ impl Default for Values { version: SETTINGS_VERSION, normalisation: false, gapless: true, + seek_step: SeekStep::default().secs(), lyrics_for_local_files: true, karaoke_lyrics: true, blur_lyrics: true, @@ -526,6 +528,10 @@ impl AppSettings { self.values.gapless } + pub fn seek_step(&self) -> SeekStep { + SeekStep::from_secs(self.values.seek_step) + } + pub fn lyrics_for_local_files(&self) -> bool { self.values.lyrics_for_local_files } @@ -743,6 +749,14 @@ impl AppSettings { self.schedule_save(cx); } + pub fn set_seek_step(&mut self, step: SeekStep, cx: &mut Context) { + if self.seek_step() == step { + return; + } + self.values.seek_step = step.secs(); + self.schedule_save(cx); + } + pub fn set_lyrics_for_local_files(&mut self, enabled: bool, cx: &mut Context) { self.values.lyrics_for_local_files = enabled; self.schedule_save(cx); diff --git a/crates/views/src/screens/settings.rs b/crates/views/src/screens/settings.rs index 52090c17..9dcd2da9 100644 --- a/crates/views/src/screens/settings.rs +++ b/crates/views/src/screens/settings.rs @@ -13,7 +13,9 @@ use gpui::{ScrollHandle, prelude::*, svg}; use i18n::{Language, t}; use music::{AccountChoice, SignIn, SignInPrompt, WritingSystem}; use router::{NavEntry, Screen, SettingsTab}; -use state::{AppSettings, Failure, Playback, SYSTEM_FONT, Session, SessionState, Sonora}; +use state::{ + AppSettings, Failure, Playback, SYSTEM_FONT, SeekStep, Session, SessionState, Sonora, +}; use ui::{ActiveTheme as _, Scrollbar, Scroller, eyebrow}; use ui::{ Avatar, Button, InfoCard, Initials, Input, Look, MAX_FONT, MAX_LYRICS_SCALE, MAX_TRANSPARENCY, @@ -41,6 +43,7 @@ const ENTRIES: &str = "entries"; const MOTION: &str = "motion"; const PACE: &str = "pace"; const SAVER: &str = "saver"; +const SEEK_STEP: &str = "seek-step"; enum Row { Item(AnyElement), @@ -74,6 +77,14 @@ fn offered(method: &SignIn, stored: bool, guest: bool) -> bool { } } +fn seek_step_label(step: SeekStep) -> SharedString { + match step { + SeekStep::Five => t!("seek-step-5"), + SeekStep::Ten => t!("seek-step-10"), + SeekStep::Thirty => t!("seek-step-30"), + } +} + #[derive(Clone, Copy)] struct Member { login: &'static str, @@ -220,6 +231,7 @@ impl SettingsView { SettingsTab::Playback => vec![ Row::Item(self.playback_row(cx).into_any_element()), Row::Item(self.gapless_row(cx).into_any_element()), + Row::Item(self.seek_step_row(cx).into_any_element()), self.title("settings-group-lyrics", cx), Row::Item(self.karaoke_lyrics_row(cx).into_any_element()), Row::Item(self.romanized_lyrics_row(cx).into_any_element()), @@ -1082,6 +1094,33 @@ impl SettingsView { ) } + fn seek_step_row(&self, cx: &mut Context) -> impl IntoElement { + let theme = *cx.theme(); + let muted = theme.muted_foreground; + let small = theme.text(Text::Small); + let current = self.settings.read(cx).seek_step(); + + let picker = Picker::new(SEEK_STEP, &self.popovers, seek_step_label(current)) + .width(Picker::NARROW) + .items(SeekStep::ALL.into_iter().map(|step| { + MenuItem::new(step.id(), seek_step_label(step)) + .selected(current == step) + .on_click(cx.listener(move |this, _, _, cx| { + this.settings + .update(cx, |settings, cx| settings.set_seek_step(step, cx)); + cx.notify(); + })) + })); + + self.row( + t!("settings-seek-step"), + t!("settings-seek-step-detail"), + muted, + small, + picker.into_any_element(), + ) + } + fn gapless_row(&self, cx: &mut Context) -> impl IntoElement { let theme = *cx.theme(); let muted = theme.muted_foreground;