Skip to content

Commit cf3d73c

Browse files
authored
feat(queue): add setting to disable queue auto-population and auto-advance (#108) (#185)
* 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 * 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.
1 parent 12c503a commit cf3d73c

8 files changed

Lines changed: 157 additions & 13 deletions

File tree

docs/docs/podcasts.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ The new order is saved automatically and restored the next time you open Obsidia
5454

5555
Episodes in the queue are kept unique by title, so adding an episode that is already queued will not create a duplicate.
5656

57+
## Turning off the queue
58+
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.
59+
60+
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:
61+
62+
- episodes are no longer added to the queue automatically when you switch, and
63+
- playback no longer advances to the next queued episode on its own.
64+
65+
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.
66+
5767
## Player
5868
The player will automatically load and play the current episode.
5969

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const DEFAULT_SETTINGS: IPodNotesSettings = {
5151
...QUEUE_SETTINGS,
5252
episodes: [],
5353
},
54+
autoQueue: true,
5455
playlists: {},
5556
skipBackwardLength: 15,
5657
skipForwardLength: 15,

src/store/index.test.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { get } from "svelte/store";
2-
import { beforeEach, describe, expect, test, vi } from "vitest";
2+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
33

44
import type { Episode } from "src/types/Episode";
55
import type { IPodNotes } from "src/types/IPodNotes";
@@ -13,6 +13,7 @@ import {
1313
downloadedEpisodes,
1414
localFiles,
1515
playedEpisodes,
16+
plugin,
1617
queue,
1718
reorderEpisodes,
1819
} from "./index";
@@ -444,3 +445,63 @@ describe("queue store", () => {
444445
}
445446
});
446447
});
448+
449+
describe("queue automation toggle (issue #108)", () => {
450+
beforeEach(() => {
451+
queue.set({ ...QUEUE_SETTINGS, episodes: [] });
452+
currentEpisode.set(undefined as unknown as Episode, false);
453+
});
454+
455+
afterEach(() => {
456+
// Reset the shared module singletons so other suites keep the default
457+
// (enabled) behavior that an unset plugin store implies.
458+
plugin.set(undefined as never);
459+
currentEpisode.set(undefined as unknown as Episode, false);
460+
});
461+
462+
function setAutoQueue(value: boolean) {
463+
plugin.set({ settings: { autoQueue: value } } as never);
464+
}
465+
466+
test("enqueues the episode you switch away from when enabled", () => {
467+
setAutoQueue(true);
468+
currentEpisode.set(ep("A"));
469+
currentEpisode.set(ep("B"));
470+
471+
expect(queueTitles()).toEqual(["A"]);
472+
});
473+
474+
test("does not enqueue the previous episode when disabled", () => {
475+
setAutoQueue(false);
476+
currentEpisode.set(ep("A"));
477+
currentEpisode.set(ep("B"));
478+
479+
expect(queueTitles()).toEqual([]);
480+
});
481+
482+
test("treats an unset plugin store as enabled (historical default)", () => {
483+
currentEpisode.set(ep("A"));
484+
currentEpisode.set(ep("B"));
485+
486+
expect(queueTitles()).toEqual(["A"]);
487+
});
488+
489+
test("playNext advances to the front of the queue when enabled", () => {
490+
setAutoQueue(true);
491+
setQueue("A", "B");
492+
493+
queue.playNext();
494+
495+
expect(get(currentEpisode)?.title).toBe("A");
496+
expect(queueTitles()).toEqual(["B"]);
497+
});
498+
499+
test("playNext is a no-op when disabled", () => {
500+
setAutoQueue(false);
501+
setQueue("A", "B");
502+
503+
queue.playNext();
504+
505+
expect(queueTitles()).toEqual(["A", "B"]);
506+
});
507+
});

src/store/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,11 @@ export const queue = (() => {
525525
});
526526
},
527527
playNext: () => {
528+
// Auto-advance is part of queue automation (issue #108): when the user
529+
// has turned the queue off, finishing an episode must not pull the next
530+
// one in. The manual queue is left intact for when they re-enable it.
531+
if (!autoQueueEnabled()) return;
532+
528533
update((queue) => {
529534
const nextEp = queue.episodes.shift();
530535

@@ -663,7 +668,21 @@ export const viewState = (() => {
663668
};
664669
})();
665670

671+
/**
672+
* Whether the queue's automatic behavior is enabled (issue #108). Gates both
673+
* auto-population (enqueuing the episode you switch away from) and auto-advance
674+
* (queue.playNext on episode end). Reads the live plugin setting on each call so
675+
* toggling takes effect without a reload. Defaults to enabled so a missing
676+
* setting or an uninitialised plugin store preserves the historical behavior.
677+
*/
678+
function autoQueueEnabled(): boolean {
679+
return get(plugin)?.settings?.autoQueue !== false;
680+
}
681+
666682
function addEpisodeToQueue(episode: Episode) {
683+
// Gate at the operation, not the call site, so no future caller can bypass it.
684+
if (!autoQueueEnabled()) return;
685+
667686
queue.update((playlist) => {
668687
// Keep the queue title-unique: a previously-played episode that is already
669688
// queued stays in place rather than being re-prepended.

src/types/IPodNotesSettings.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ export interface IPodNotesSettings {
1616
skipForwardLength: number;
1717
playlists: { [playlistName: string]: Playlist };
1818
queue: Playlist;
19+
/**
20+
* Queue automation (issue #108). When `true` (default) switching episodes
21+
* keeps the one you left at the top of the queue and playback auto-advances to
22+
* the next queued episode. When `false` the queue stops filling and advancing
23+
* on its own; it remains usable as a manual playlist.
24+
*/
25+
autoQueue: boolean;
1926
favorites: Playlist;
2027
localFiles: Playlist;
2128
currentEpisode?: Episode;

src/ui/PodcastView/EpisodePlayer.svelte

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@
4848
//#endregion
4949
const clampVolume = (value: number): number => Math.min(1, Math.max(0, value));
5050
51+
// Hide the player's Queue list only when queue automation is off AND the queue
52+
// is empty (issue #108) — otherwise show it (default-on state or a non-empty
53+
// manual queue).
54+
$: showQueue =
55+
$plugin?.settings?.autoQueue !== false || $queue.episodes.length > 0;
56+
5157
let isHoveringArtwork: boolean = false;
5258
let isLoading: boolean = true;
5359
let playerVolume: number = 1;
@@ -338,17 +344,19 @@
338344
on:seek={onChapterSeek}
339345
/>
340346

341-
<EpisodeList
342-
episodes={$queue.episodes}
343-
showListMenu={false}
344-
showThumbnails={true}
345-
on:contextMenuEpisode={handleContextMenuEpisode}
346-
on:clickEpisode={handleClickEpisode}
347-
>
348-
<svelte:fragment slot="header">
349-
<h3>Queue</h3>
350-
</svelte:fragment>
351-
</EpisodeList>
347+
{#if showQueue}
348+
<EpisodeList
349+
episodes={$queue.episodes}
350+
showListMenu={false}
351+
showThumbnails={true}
352+
on:contextMenuEpisode={handleContextMenuEpisode}
353+
on:clickEpisode={handleClickEpisode}
354+
>
355+
<svelte:fragment slot="header">
356+
<h3>Queue</h3>
357+
</svelte:fragment>
358+
</EpisodeList>
359+
{/if}
352360
</div>
353361
</div>
354362

src/ui/PodcastView/PodcastView.svelte

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,16 @@
7272
onMount(() => {
7373
const updateDisplayedPlaylists = () => {
7474
const customPlaylists = Object.values(get(playlists));
75+
const queueValue = get(queue);
76+
// Hide the Queue tile only when queue automation is off AND the queue is
77+
// empty (issue #108), so turning the queue off doesn't leave a permanent
78+
// empty "Queue" tile. The default-on state and a non-empty manual queue
79+
// both keep it visible.
80+
const showQueue =
81+
get(plugin)?.settings?.autoQueue !== false ||
82+
queueValue.episodes.length > 0;
7583
displayedPlaylists = [
76-
get(queue),
84+
...(showQueue ? [queueValue] : []),
7785
get(favorites),
7886
get(localFiles),
7987
getPlayedPlaylist(),
@@ -86,6 +94,9 @@
8694
queue.subscribe(updateDisplayedPlaylists),
8795
favorites.subscribe(updateDisplayedPlaylists),
8896
localFiles.subscribe(updateDisplayedPlaylists),
97+
// Recompute when the plugin store re-emits so toggling the autoQueue
98+
// setting hides/shows the empty Queue tile immediately (issue #108).
99+
plugin.subscribe(updateDisplayedPlaylists),
89100
playedEpisodes.subscribe(() => {
90101
updateDisplayedPlaylists();
91102
if (isShowingPlayedEpisodes) {

src/ui/settings/PodNotesSettingsTab.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
hidePlayedEpisodes,
2323
localFiles,
2424
playlists,
25+
plugin,
2526
queue,
2627
savedFeeds,
2728
volume,
@@ -82,6 +83,7 @@ export class PodNotesSettingsTab extends PluginSettingTab {
8283
target: playlistManagerContainer,
8384
});
8485

86+
this.addQueueSettings(settingsContainer);
8587
this.addDefaultPlaybackRateSetting(settingsContainer);
8688
this.addDefaultVolumeSetting(settingsContainer);
8789
this.addSkipLengthSettings(settingsContainer);
@@ -104,6 +106,25 @@ export class PodNotesSettingsTab extends PluginSettingTab {
104106
}
105107
}
106108

109+
private addQueueSettings(container: HTMLElement): void {
110+
new Setting(container)
111+
.setName("Keep a queue of episodes you switch away from")
112+
.setDesc(
113+
"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.",
114+
)
115+
.addToggle((toggle) =>
116+
toggle
117+
.setValue(this.plugin.settings.autoQueue)
118+
.onChange(async (value) => {
119+
this.plugin.settings.autoQueue = value;
120+
await this.plugin.saveSettings();
121+
// Re-emit the plugin store so an open player/grid recomputes
122+
// the Queue tile/list visibility immediately (issue #108).
123+
plugin.set(this.plugin);
124+
}),
125+
);
126+
}
127+
107128
private addDefaultPlaybackRateSetting(container: HTMLElement): void {
108129
new Setting(container)
109130
.setName("Default Playback Rate")
@@ -627,6 +648,12 @@ export class PodNotesSettingsTab extends PluginSettingTab {
627648
volume.set(Math.min(1, Math.max(0, importedVolume)));
628649

629650
await this.plugin.saveSettings();
651+
// Re-emit the plugin store so an open player/grid recomputes Queue tile/list
652+
// visibility (and any other $plugin-derived UI) after an import, mirroring the
653+
// autoQueue toggle. Today the queue.set above already triggers that recompute;
654+
// this keeps the import path correct independent of that incidental emission
655+
// (issue #108).
656+
plugin.set(this.plugin);
630657
this.display();
631658
new Notice("Imported PodNotes settings.");
632659
}

0 commit comments

Comments
 (0)