From 7e5255062128be62a8a5c6e35a3368cde2e64e2a Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 15 Jun 2026 10:57:17 +0200 Subject: [PATCH 1/2] feat(queue): add setting to disable queue auto-population and auto-advance (#108) Adds an `autoQueue` setting (default on, preserving current behavior). When turned off: - switching episodes no longer enqueues the episode you were listening to, and - playback no longer auto-advances to the next queued episode at episode end. The queue stays usable as a manual playlist (context-menu add/remove/reorder are unaffected). When the queue is off and empty, its grid tile and player list are hidden so the disabled queue does not linger in the UI. The gate lives at the queue operations (addEpisodeToQueue, playNext) and reads the setting live, so toggling takes effect without a reload and no caller can bypass it. Closes #108 --- docs/docs/podcasts.md | 10 ++++ src/constants.ts | 1 + src/store/index.test.ts | 63 ++++++++++++++++++++++++- src/store/index.ts | 19 ++++++++ src/types/IPodNotesSettings.ts | 7 +++ src/ui/PodcastView/EpisodePlayer.svelte | 30 +++++++----- src/ui/PodcastView/PodcastView.svelte | 13 ++++- src/ui/settings/PodNotesSettingsTab.ts | 21 +++++++++ 8 files changed, 151 insertions(+), 13 deletions(-) diff --git a/docs/docs/podcasts.md b/docs/docs/podcasts.md index a4fa4974..e55aafd2 100644 --- a/docs/docs/podcasts.md +++ b/docs/docs/podcasts.md @@ -54,6 +54,16 @@ The new order is saved automatically and restored the next time you open Obsidia Episodes in the queue are kept unique by title, so adding an episode that is already queued will not create a duplicate. +## Turning off the queue +By default the queue fills itself: when you switch to a new episode, the one you were listening to is kept at the top of the queue, and playback continues with the next queued episode when the current one ends. + +If you would rather manage the queue yourself, turn off **Keep a queue of episodes you switch away from** in PodNotes settings. With it off: + +- episodes are no longer added to the queue automatically when you switch, and +- playback no longer advances to the next queued episode on its own. + +You can still add, remove, and reorder episodes in the queue manually from the episode context menu. While the queue is empty in this mode, its tile in the podcast grid and its list in the player are hidden. + ## Player The player will automatically load and play the current episode. diff --git a/src/constants.ts b/src/constants.ts index 09838bc4..b18414e1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -51,6 +51,7 @@ export const DEFAULT_SETTINGS: IPodNotesSettings = { ...QUEUE_SETTINGS, episodes: [], }, + autoQueue: true, playlists: {}, skipBackwardLength: 15, skipForwardLength: 15, diff --git a/src/store/index.test.ts b/src/store/index.test.ts index c77cd5ee..30f36deb 100644 --- a/src/store/index.test.ts +++ b/src/store/index.test.ts @@ -1,5 +1,5 @@ import { get } from "svelte/store"; -import { beforeEach, describe, expect, test, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import type { Episode } from "src/types/Episode"; import type { IPodNotes } from "src/types/IPodNotes"; @@ -13,6 +13,7 @@ import { downloadedEpisodes, localFiles, playedEpisodes, + plugin, queue, reorderEpisodes, } from "./index"; @@ -444,3 +445,63 @@ describe("queue store", () => { } }); }); + +describe("queue automation toggle (issue #108)", () => { + beforeEach(() => { + queue.set({ ...QUEUE_SETTINGS, episodes: [] }); + currentEpisode.set(undefined as unknown as Episode, false); + }); + + afterEach(() => { + // Reset the shared module singletons so other suites keep the default + // (enabled) behavior that an unset plugin store implies. + plugin.set(undefined as never); + currentEpisode.set(undefined as unknown as Episode, false); + }); + + function setAutoQueue(value: boolean) { + plugin.set({ settings: { autoQueue: value } } as never); + } + + test("enqueues the episode you switch away from when enabled", () => { + setAutoQueue(true); + currentEpisode.set(ep("A")); + currentEpisode.set(ep("B")); + + expect(queueTitles()).toEqual(["A"]); + }); + + test("does not enqueue the previous episode when disabled", () => { + setAutoQueue(false); + currentEpisode.set(ep("A")); + currentEpisode.set(ep("B")); + + expect(queueTitles()).toEqual([]); + }); + + test("treats an unset plugin store as enabled (historical default)", () => { + currentEpisode.set(ep("A")); + currentEpisode.set(ep("B")); + + expect(queueTitles()).toEqual(["A"]); + }); + + test("playNext advances to the front of the queue when enabled", () => { + setAutoQueue(true); + setQueue("A", "B"); + + queue.playNext(); + + expect(get(currentEpisode)?.title).toBe("A"); + expect(queueTitles()).toEqual(["B"]); + }); + + test("playNext is a no-op when disabled", () => { + setAutoQueue(false); + setQueue("A", "B"); + + queue.playNext(); + + expect(queueTitles()).toEqual(["A", "B"]); + }); +}); diff --git a/src/store/index.ts b/src/store/index.ts index 6a94bea3..40972ad6 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -525,6 +525,11 @@ export const queue = (() => { }); }, playNext: () => { + // Auto-advance is part of queue automation (issue #108): when the user + // has turned the queue off, finishing an episode must not pull the next + // one in. The manual queue is left intact for when they re-enable it. + if (!autoQueueEnabled()) return; + update((queue) => { const nextEp = queue.episodes.shift(); @@ -663,7 +668,21 @@ export const viewState = (() => { }; })(); +/** + * Whether the queue's automatic behavior is enabled (issue #108). Gates both + * auto-population (enqueuing the episode you switch away from) and auto-advance + * (queue.playNext on episode end). Reads the live plugin setting on each call so + * toggling takes effect without a reload. Defaults to enabled so a missing + * setting or an uninitialised plugin store preserves the historical behavior. + */ +function autoQueueEnabled(): boolean { + return get(plugin)?.settings?.autoQueue !== false; +} + function addEpisodeToQueue(episode: Episode) { + // Gate at the operation, not the call site, so no future caller can bypass it. + if (!autoQueueEnabled()) return; + queue.update((playlist) => { // Keep the queue title-unique: a previously-played episode that is already // queued stays in place rather than being re-prepended. diff --git a/src/types/IPodNotesSettings.ts b/src/types/IPodNotesSettings.ts index 70b80dc3..08afe596 100644 --- a/src/types/IPodNotesSettings.ts +++ b/src/types/IPodNotesSettings.ts @@ -16,6 +16,13 @@ export interface IPodNotesSettings { skipForwardLength: number; playlists: { [playlistName: string]: Playlist }; queue: Playlist; + /** + * Queue automation (issue #108). When `true` (default) switching episodes + * keeps the one you left at the top of the queue and playback auto-advances to + * the next queued episode. When `false` the queue stops filling and advancing + * on its own; it remains usable as a manual playlist. + */ + autoQueue: boolean; favorites: Playlist; localFiles: Playlist; currentEpisode?: Episode; diff --git a/src/ui/PodcastView/EpisodePlayer.svelte b/src/ui/PodcastView/EpisodePlayer.svelte index eb410ac8..5f847156 100644 --- a/src/ui/PodcastView/EpisodePlayer.svelte +++ b/src/ui/PodcastView/EpisodePlayer.svelte @@ -48,6 +48,12 @@ //#endregion const clampVolume = (value: number): number => Math.min(1, Math.max(0, value)); + // Hide the player's Queue list only when queue automation is off AND the queue + // is empty (issue #108) — otherwise show it (default-on state or a non-empty + // manual queue). + $: showQueue = + $plugin?.settings?.autoQueue !== false || $queue.episodes.length > 0; + let isHoveringArtwork: boolean = false; let isLoading: boolean = true; let playerVolume: number = 1; @@ -338,17 +344,19 @@ on:seek={onChapterSeek} /> - - -

Queue

-
-
+ {#if showQueue} + + +

Queue

+
+
+ {/if} diff --git a/src/ui/PodcastView/PodcastView.svelte b/src/ui/PodcastView/PodcastView.svelte index 9ab51d8a..e118a6fe 100644 --- a/src/ui/PodcastView/PodcastView.svelte +++ b/src/ui/PodcastView/PodcastView.svelte @@ -72,8 +72,16 @@ onMount(() => { const updateDisplayedPlaylists = () => { const customPlaylists = Object.values(get(playlists)); + const queueValue = get(queue); + // Hide the Queue tile only when queue automation is off AND the queue is + // empty (issue #108), so turning the queue off doesn't leave a permanent + // empty "Queue" tile. The default-on state and a non-empty manual queue + // both keep it visible. + const showQueue = + get(plugin)?.settings?.autoQueue !== false || + queueValue.episodes.length > 0; displayedPlaylists = [ - get(queue), + ...(showQueue ? [queueValue] : []), get(favorites), get(localFiles), getPlayedPlaylist(), @@ -86,6 +94,9 @@ queue.subscribe(updateDisplayedPlaylists), favorites.subscribe(updateDisplayedPlaylists), localFiles.subscribe(updateDisplayedPlaylists), + // Recompute when the plugin store re-emits so toggling the autoQueue + // setting hides/shows the empty Queue tile immediately (issue #108). + plugin.subscribe(updateDisplayedPlaylists), playedEpisodes.subscribe(() => { updateDisplayedPlaylists(); if (isShowingPlayedEpisodes) { diff --git a/src/ui/settings/PodNotesSettingsTab.ts b/src/ui/settings/PodNotesSettingsTab.ts index 7db6bec1..34c31149 100644 --- a/src/ui/settings/PodNotesSettingsTab.ts +++ b/src/ui/settings/PodNotesSettingsTab.ts @@ -22,6 +22,7 @@ import { hidePlayedEpisodes, localFiles, playlists, + plugin, queue, savedFeeds, volume, @@ -82,6 +83,7 @@ export class PodNotesSettingsTab extends PluginSettingTab { target: playlistManagerContainer, }); + this.addQueueSettings(settingsContainer); this.addDefaultPlaybackRateSetting(settingsContainer); this.addDefaultVolumeSetting(settingsContainer); this.addSkipLengthSettings(settingsContainer); @@ -104,6 +106,25 @@ export class PodNotesSettingsTab extends PluginSettingTab { } } + private addQueueSettings(container: HTMLElement): void { + new Setting(container) + .setName("Keep a queue of episodes you switch away from") + .setDesc( + "When on, the episode you switch away from is kept at the top of the queue and playback automatically continues with the next queued episode when one ends. Turn this off to stop the queue from filling and advancing on its own — you can still add episodes to the queue manually.", + ) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.autoQueue) + .onChange(async (value) => { + this.plugin.settings.autoQueue = value; + await this.plugin.saveSettings(); + // Re-emit the plugin store so an open player/grid recomputes + // the Queue tile/list visibility immediately (issue #108). + plugin.set(this.plugin); + }), + ); + } + private addDefaultPlaybackRateSetting(container: HTMLElement): void { new Setting(container) .setName("Default Playback Rate") From e0490aac826defd89e477aa9247cd83a1d268e4c Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 15 Jun 2026 11:09:08 +0200 Subject: [PATCH 2/2] refactor(settings): re-emit plugin store on settings import (#108) applyImportedSettings re-hydrates the individual stores but did not re-emit the `plugin` store, unlike the autoQueue toggle. Importing settings that change `autoQueue` already refreshes the Queue tile/list today because the unconditional `queue.set` triggers the same reactive statements, but that is incidental. Re-emit `plugin` after import so the autoQueue-gated UI stays correct independent of the queue emission (addresses Devin review note on #185). No observable behavior change. --- src/ui/settings/PodNotesSettingsTab.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ui/settings/PodNotesSettingsTab.ts b/src/ui/settings/PodNotesSettingsTab.ts index 34c31149..83e98613 100644 --- a/src/ui/settings/PodNotesSettingsTab.ts +++ b/src/ui/settings/PodNotesSettingsTab.ts @@ -648,6 +648,12 @@ export class PodNotesSettingsTab extends PluginSettingTab { volume.set(Math.min(1, Math.max(0, importedVolume))); await this.plugin.saveSettings(); + // Re-emit the plugin store so an open player/grid recomputes Queue tile/list + // visibility (and any other $plugin-derived UI) after an import, mirroring the + // autoQueue toggle. Today the queue.set above already triggers that recompute; + // this keeps the import path correct independent of that incidental emission + // (issue #108). + plugin.set(this.plugin); this.display(); new Notice("Imported PodNotes settings."); }