Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/docs/podcasts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const DEFAULT_SETTINGS: IPodNotesSettings = {
...QUEUE_SETTINGS,
episodes: [],
},
autoQueue: true,
playlists: {},
skipBackwardLength: 15,
skipForwardLength: 15,
Expand Down
63 changes: 62 additions & 1 deletion src/store/index.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -13,6 +13,7 @@ import {
downloadedEpisodes,
localFiles,
playedEpisodes,
plugin,
queue,
reorderEpisodes,
} from "./index";
Expand Down Expand Up @@ -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"]);
});
});
19 changes: 19 additions & 0 deletions src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions src/types/IPodNotesSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
30 changes: 19 additions & 11 deletions src/ui/PodcastView/EpisodePlayer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -338,17 +344,19 @@
on:seek={onChapterSeek}
/>

<EpisodeList
episodes={$queue.episodes}
showListMenu={false}
showThumbnails={true}
on:contextMenuEpisode={handleContextMenuEpisode}
on:clickEpisode={handleClickEpisode}
>
<svelte:fragment slot="header">
<h3>Queue</h3>
</svelte:fragment>
</EpisodeList>
{#if showQueue}
<EpisodeList
episodes={$queue.episodes}
showListMenu={false}
showThumbnails={true}
on:contextMenuEpisode={handleContextMenuEpisode}
on:clickEpisode={handleClickEpisode}
>
<svelte:fragment slot="header">
<h3>Queue</h3>
</svelte:fragment>
</EpisodeList>
{/if}
</div>
</div>

Expand Down
13 changes: 12 additions & 1 deletion src/ui/PodcastView/PodcastView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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) {
Expand Down
27 changes: 27 additions & 0 deletions src/ui/settings/PodNotesSettingsTab.ts
Comment thread
chhoumann marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
hidePlayedEpisodes,
localFiles,
playlists,
plugin,
queue,
savedFeeds,
volume,
Expand Down Expand Up @@ -82,6 +83,7 @@ export class PodNotesSettingsTab extends PluginSettingTab {
target: playlistManagerContainer,
});

this.addQueueSettings(settingsContainer);
this.addDefaultPlaybackRateSetting(settingsContainer);
this.addDefaultVolumeSetting(settingsContainer);
this.addSkipLengthSettings(settingsContainer);
Expand All @@ -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")
Expand Down Expand Up @@ -627,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.");
}
Expand Down
Loading