feat(session): opt-in resume playback position on session restore (#317)
Adds a separate "Resume playback position" toggle in Settings > Playback. Default is OFF — opening qbz the next morning lands on a clean 0:00 of the saved track, which is the common preference. Users who do want bookmark-style resume turn the toggle on. Independent of "Restore session on startup" in the UI (top-level toggle, not a sub-setting). The seek only applies when both are on: without persistSession there's no saved track to resume to, so the flag is inert. Keeping the toggles separate matches the Settings UX elsewhere and lets users flip persistence on without inheriting the old #317 behavior we never had. Backend ------- - New `PlaybackPreferences::resume_playback_position: bool` (default false). Migration adds the column to the existing `playback_preferences.db` and the default row gets 0; existing installs upgrade transparently. - Setter `set_resume_playback_position(bool)` on both `PlaybackPreferencesStore` and the wrapping `State`. - Tauri command `v2_set_resume_playback_position` registered in lib.rs invoke handler. Get-side already reads the new field via the existing `v2_get_playback_preferences`. Frontend -------- - `playbackPreferencesStore.ts` — new field on the type and cache, new `setResumePlaybackPosition()` exported. - `SettingsView.svelte` — separate toggle row right below "Restore session on startup" (matches the existing Settings rhythm). - `playerStore.ts` — `pendingSessionRestore` extended with optional `positionSecs`. When present, after the restored track loads, the position is queued through the existing `pendingSeekPosition` / `flushPendingSeek` path so the seek runs against an already-loaded stream and respects the in-flight guard. Skip the queue when the saved position is at or near 0 (the load already starts there). - `routes/+page.svelte` — at session restore, reads `getCachedPreferences().resume_playback_position` and forwards `session.current_position_secs` only when the user opted in. The field has been persisted in the schema since v1.2.5 — what changes is whether we read it back. i18n ---- - New keys in 5 locales: `settings.playback.resumePlaybackPosition` / `…Desc`. - `persistSessionDesc` updated everywhere — it used to claim the feature restored playback position, which was always misleading (the code restored the track but paused at 0:00). The text now matches what the toggle does on its own: queue + view, paused at 0:00. Resume-to-position becomes the explicit ask of the new toggle. Tests ----- - Backend: 196 tests pass. Frontend: 0 TS errors. cc
This commit is contained in:
12 files changed
+181
-22
No files matched your search
@@ -96,6 +96,14 @@ pub fn v2_set_persist_session(
|
||||
state.set_persist_session(persist)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn v2_set_resume_playback_position(
|
||||
resume: bool,
|
||||
state: State<'_, PlaybackPreferencesState>,
|
||||
) -> Result<(), String> {
|
||||
state.set_resume_playback_position(resume)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn v2_get_playback_preferences(
|
||||
state: State<'_, PlaybackPreferencesState>,
|
||||
|
||||
@@ -50,6 +50,13 @@ pub struct PlaybackPreferences {
|
||||
pub autoplay_mode: AutoplayMode,
|
||||
pub show_context_icon: bool,
|
||||
pub persist_session: bool,
|
||||
/// Sub-preference of `persist_session`. When true, restoring a
|
||||
/// session also seeks to `current_position_secs` of the saved
|
||||
/// track. When false (default), the saved track is shown paused at
|
||||
/// 0:00 and the user starts the next listen fresh — the common
|
||||
/// preference for "open app the next morning to a clean slate"
|
||||
/// (issue #317).
|
||||
pub resume_playback_position: bool,
|
||||
}
|
||||
|
||||
impl Default for PlaybackPreferences {
|
||||
@@ -58,6 +65,7 @@ impl Default for PlaybackPreferences {
|
||||
autoplay_mode: AutoplayMode::ContinueWithinSource,
|
||||
show_context_icon: false,
|
||||
persist_session: false,
|
||||
resume_playback_position: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,10 +148,31 @@ impl PlaybackPreferencesStore {
|
||||
info!("[PlaybackPrefs] persist_session migration successful");
|
||||
}
|
||||
|
||||
// Step 3c: Check if resume_playback_position column exists
|
||||
let resume_position_exists: bool = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM pragma_table_info('playback_preferences') WHERE name='resume_playback_position'",
|
||||
[],
|
||||
|row| {
|
||||
let count: i32 = row.get(0)?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
)
|
||||
.unwrap_or(false);
|
||||
|
||||
if !resume_position_exists {
|
||||
info!("[PlaybackPrefs] Migrating: adding resume_playback_position column");
|
||||
conn.execute(
|
||||
"ALTER TABLE playback_preferences ADD COLUMN resume_playback_position INTEGER NOT NULL DEFAULT 0",
|
||||
[]
|
||||
).map_err(|e| format!("Failed to add resume_playback_position column: {}", e))?;
|
||||
info!("[PlaybackPrefs] resume_playback_position migration successful");
|
||||
}
|
||||
|
||||
// Step 4: Insert default row if it doesn't exist
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO playback_preferences (id, autoplay_mode, show_context_icon, persist_session)
|
||||
VALUES (1, 'continue', 0, 0)",
|
||||
"INSERT OR IGNORE INTO playback_preferences (id, autoplay_mode, show_context_icon, persist_session, resume_playback_position)
|
||||
VALUES (1, 'continue', 0, 0, 0)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| format!("Failed to insert default preferences: {}", e))?;
|
||||
@@ -165,16 +194,18 @@ impl PlaybackPreferencesStore {
|
||||
pub fn get_preferences(&self) -> Result<PlaybackPreferences, String> {
|
||||
self.conn
|
||||
.query_row(
|
||||
"SELECT autoplay_mode, show_context_icon, persist_session FROM playback_preferences WHERE id = 1",
|
||||
"SELECT autoplay_mode, show_context_icon, persist_session, resume_playback_position FROM playback_preferences WHERE id = 1",
|
||||
[],
|
||||
|row| {
|
||||
let autoplay_str: String = row.get(0)?;
|
||||
let show_icon: i32 = row.get(1)?;
|
||||
let persist: i32 = row.get(2)?;
|
||||
let resume_pos: i32 = row.get(3)?;
|
||||
Ok(PlaybackPreferences {
|
||||
autoplay_mode: AutoplayMode::from_db_value(&autoplay_str),
|
||||
show_context_icon: show_icon != 0,
|
||||
persist_session: persist != 0,
|
||||
resume_playback_position: resume_pos != 0,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -211,13 +242,28 @@ impl PlaybackPreferencesStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_resume_playback_position(&self, resume: bool) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"UPDATE playback_preferences SET resume_playback_position = ?1 WHERE id = 1",
|
||||
params![if resume { 1 } else { 0 }],
|
||||
)
|
||||
.map_err(|e| format!("Failed to set resume playback position: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reset all playback preferences to their default values
|
||||
pub fn reset_all(&self) -> Result<PlaybackPreferences, String> {
|
||||
let defaults = PlaybackPreferences::default();
|
||||
self.conn
|
||||
.execute(
|
||||
"UPDATE playback_preferences SET autoplay_mode = ?1, show_context_icon = ?2, persist_session = ?3 WHERE id = 1",
|
||||
params![defaults.autoplay_mode.to_db_value(), if defaults.show_context_icon { 1 } else { 0 }, if defaults.persist_session { 1 } else { 0 }],
|
||||
"UPDATE playback_preferences SET autoplay_mode = ?1, show_context_icon = ?2, persist_session = ?3, resume_playback_position = ?4 WHERE id = 1",
|
||||
params![
|
||||
defaults.autoplay_mode.to_db_value(),
|
||||
if defaults.show_context_icon { 1 } else { 0 },
|
||||
if defaults.persist_session { 1 } else { 0 },
|
||||
if defaults.resume_playback_position { 1 } else { 0 },
|
||||
],
|
||||
)
|
||||
.map_err(|e| format!("Failed to reset playback preferences: {}", e))?;
|
||||
Ok(defaults)
|
||||
@@ -297,6 +343,15 @@ impl PlaybackPreferencesState {
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_persist_session(persist)
|
||||
}
|
||||
|
||||
pub fn set_resume_playback_position(&self, resume: bool) -> Result<(), String> {
|
||||
let guard = self
|
||||
.store
|
||||
.lock()
|
||||
.map_err(|_| "Failed to lock playback preferences store".to_string())?;
|
||||
let store = guard.as_ref().ok_or("No active session - please log in")?;
|
||||
store.set_resume_playback_position(resume)
|
||||
}
|
||||
}
|
||||
|
||||
// Tauri commands
|
||||
@@ -337,3 +392,11 @@ pub fn set_persist_session(
|
||||
) -> Result<(), String> {
|
||||
state.set_persist_session(persist)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_resume_playback_position(
|
||||
resume: bool,
|
||||
state: tauri::State<PlaybackPreferencesState>,
|
||||
) -> Result<(), String> {
|
||||
state.set_resume_playback_position(resume)
|
||||
}
|
||||
@@ -1392,6 +1392,7 @@ pub fn run() {
|
||||
commands_v2::v2_set_autoplay_mode,
|
||||
commands_v2::v2_set_show_context_icon,
|
||||
commands_v2::v2_set_persist_session,
|
||||
commands_v2::v2_set_resume_playback_position,
|
||||
commands_v2::v2_get_playback_preferences,
|
||||
commands_v2::v2_get_favorites_preferences,
|
||||
commands_v2::v2_save_favorites_preferences,
|
||||
|
||||
Reference in new issue
Block a user