diff --git a/README.md b/README.md
index bf7d148..be39dc8 100644
--- a/README.md
+++ b/README.md
@@ -161,6 +161,9 @@ All keys are remappable — edit `keybinds.jsonc` in your config directory
| `.` | Toggle hidden |
| `r` | Refresh |
| `x` | Unsubscribe the focused show (My Shows) |
+| `d` | Download the focused episode (Feed / My Shows detail pane) |
+| `D` | Delete the focused episode's download (if one exists) |
+| `w` | Toggle the focused show in/out of the auto-download whitelist (My Shows, whitelist scope) |
**Audio**
@@ -185,6 +188,13 @@ default (`$XDG_CONFIG_HOME/podtui` if set).
Legacy `feeds.json`, `sources.json`, and `app-state.json` are auto-migrated
into `config.json` on first run.
+**Auto-download** — in Settings → Preferences: `Auto Download` (master
+toggle) downloads the `Auto Download Count` most recent episodes (default 2,
+any positive integer — type it in the editor) of every show in the `Auto
+Download Scope` (all / none / whitelist, default all). With the whitelist
+scope, a search field appears under the setting to pick shows (Space toggles
+a suggestion in/out), and `w` in My Shows adds/removes the focused show.
+
Env overrides: `PODTUI_AUDIO_BACKEND`, `XDG_CONFIG_HOME`.
## Troubleshooting
diff --git a/src/config/keybinds.jsonc b/src/config/keybinds.jsonc
index a12ff10..4c3c60b 100644
--- a/src/config/keybinds.jsonc
+++ b/src/config/keybinds.jsonc
@@ -66,6 +66,11 @@
"refresh": ["r"],
"unsubscribe": ["x"], // unsubscribe focused show in My Shows
+ // ── Downloads & auto-download whitelist ───────────────────────────────────
+ "download": ["d"], // download the focused episode (detail pane)
+ "delete-download": ["D"], // delete the focused episode's download (if any)
+ "whitelist-toggle": ["w"], // add/remove the focused show from the auto-download whitelist (My Shows)
+
// ── Audio transport (preserved) ──────────────────────────────────────────
// Kept on shifted single keys so they never collide with the yazi core
// (space=select, s=search, f=filter, etc.). Edit freely in this file.
diff --git a/src/context/KeybindContext.tsx b/src/context/KeybindContext.tsx
index 2ec2878..21d6c3a 100644
--- a/src/context/KeybindContext.tsx
+++ b/src/context/KeybindContext.tsx
@@ -68,6 +68,9 @@ export type KeybindActionName =
| "toggle-hidden"
| "refresh"
| "unsubscribe"
+ | "download"
+ | "delete-download"
+ | "whitelist-toggle"
| "audio-toggle"
| "audio-next"
| "audio-prev"
diff --git a/src/pages/Feed/FeedPage.tsx b/src/pages/Feed/FeedPage.tsx
index 1c5f210..7c8bd4e 100644
--- a/src/pages/Feed/FeedPage.tsx
+++ b/src/pages/Feed/FeedPage.tsx
@@ -171,6 +171,18 @@ function FeedPage() {
const item = focusedItem();
if (item) nav.toggleSelected(item.episode.id);
},
+ download: () => {
+ const item = focusedItem();
+ if (item) downloadStore.startDownload(item.episode, item.feed.id);
+ },
+ "delete-download": () => {
+ const item = focusedItem();
+ if (!item) return;
+ const id = item.episode.id;
+ if (downloadStore.getDownloadStatus(id) === DownloadStatus.NONE) return;
+ downloadStore.cancelDownload(id);
+ downloadStore.removeDownload(id).catch(() => {});
+ },
refresh: () => {
feedStore.refreshAllFeeds().catch(() => {});
},
@@ -378,7 +390,14 @@ function FeedPage() {
{(item().episode.description?.length ?? 0) > 400 ? "…" : ""}
- enter: play · space: select · h back
+
+ enter: play · d: download
+ {downloadStore.getDownloadStatus(item().episode.id) !==
+ DownloadStatus.NONE
+ ? " · D: delete"
+ : ""}{" "}
+ · space: select · h back
+
)}
diff --git a/src/pages/MyShows/MyShowsPage.tsx b/src/pages/MyShows/MyShowsPage.tsx
index fc00573..f894e5d 100644
--- a/src/pages/MyShows/MyShowsPage.tsx
+++ b/src/pages/MyShows/MyShowsPage.tsx
@@ -14,6 +14,7 @@
import { createMemo, For, Show, onMount, onCleanup } from "solid-js";
import { useFeedStore } from "@/stores/feed";
import { useDownloadStore } from "@/stores/download";
+import { useAppStore } from "@/stores/app";
import { DownloadStatus } from "@/types/episode";
import { format } from "date-fns";
import { useTheme } from "@/context/ThemeContext";
@@ -40,6 +41,7 @@ export const MyShowsPaneCount = 1;
export function MyShowsPage() {
const feedStore = useFeedStore();
const downloadStore = useDownloadStore();
+ const app = useAppStore();
const audioNav = useAudioNavStore();
const audio = useAudio();
const { theme } = useTheme();
@@ -161,6 +163,33 @@ export function MyShowsPage() {
if (ep) nav.toggleSelected(ep.id);
}
},
+ download: () => {
+ if (depth() < 1) return;
+ const ep = focusedEpisode();
+ if (ep) downloadStore.startDownload(ep, drilledShowId());
+ },
+ "delete-download": () => {
+ if (depth() < 1) return;
+ const ep = focusedEpisode();
+ if (!ep) return;
+ const id = ep.id;
+ if (downloadStore.getDownloadStatus(id) === DownloadStatus.NONE) return;
+ downloadStore.cancelDownload(id);
+ downloadStore.removeDownload(id).catch(() => {});
+ },
+ "whitelist-toggle": () => {
+ if (depth() < 1) return;
+ const id = drilledShowId();
+ if (!id) return;
+ const prefs = app.state().preferences;
+ if (prefs.autoDownloadScope !== "whitelist") return;
+ const cur = prefs.autoDownloadWhitelist ?? [];
+ const next = cur.includes(id)
+ ? cur.filter((x) => x !== id)
+ : [...cur, id];
+ app.updatePreferences({ autoDownloadWhitelist: next });
+ feedStore.runAutoDownload();
+ },
refresh: () => {
const show = selectedShow();
if (show) feedStore.refreshFeed(show.id).catch(() => {});
@@ -421,7 +450,21 @@ export function MyShowsPage() {
{(ep().description?.length ?? 0) > 400 ? "…" : ""}
- enter: play · space: select · h: back
+
+ enter: play · d: download
+ {downloadStore.getDownloadStatus(ep().id) !==
+ DownloadStatus.NONE
+ ? " · D: delete"
+ : ""}
+ {app.state().preferences.autoDownloadScope === "whitelist"
+ ? (app.state().preferences.autoDownloadWhitelist ?? []).includes(
+ drilledShowId(),
+ )
+ ? " · w: un-whitelist"
+ : " · w: whitelist"
+ : ""}{" "}
+ · space: select · h: back
+
)}
diff --git a/src/pages/Settings/PreferencesPanel.tsx b/src/pages/Settings/PreferencesPanel.tsx
index 90c2059..968708a 100644
--- a/src/pages/Settings/PreferencesPanel.tsx
+++ b/src/pages/Settings/PreferencesPanel.tsx
@@ -2,10 +2,33 @@
* PreferencesPanel — exposes theme/font/speed/explicit/auto-download as
* SettingItems for the yazi depth-stack. No own useKeyboard; all movement is
* driven by the Shell router via nav.action.
+ *
+ * Auto-download (global setting, see stores/feed.ts runAutoDownload):
+ * • Auto Download — master toggle (default: off)
+ * • Auto Download Count — X most recent episodes per show (default: 2,
+ * any positive integer — type it in the editor)
+ * • Auto Download Scope — which shows: all / none / whitelist (default: all)
+ * • Auto Download Whitelist — shown only when scope is "whitelist": search
+ * field over subscribed shows; suggestions toggle
+ * in/out with Space (j/k to move, Esc to browse).
*/
+import { createSignal, Show, For, onMount, onCleanup } from "solid-js";
+import { RenderableEvents, type InputRenderable } from "@opentui/core";
import { useAppStore } from "@/stores/app";
-import type { ThemeName } from "@/types/settings";
+import { useFeedStore } from "@/stores/feed";
+import { useTheme } from "@/context/ThemeContext";
+import { useInputFocusNav } from "@/hooks/useInputFocusNav";
+import {
+ NavMode,
+ DEPTH_CENTER_PANE,
+ type PaneId,
+} from "@/context/NavigationContext";
+import { on } from "@/utils/event-bus";
+import type { KeybindActionName } from "@/context/KeybindContext";
+import { TABS } from "@/utils/navigation";
+import type { AutoDownloadScope, ThemeName } from "@/types/settings";
+import type { Feed } from "@/types/feed";
import type { SettingItem } from "./types";
const THEME_LABELS: Array<{ value: ThemeName; label: string }> = [
@@ -17,13 +40,24 @@ const THEME_LABELS: Array<{ value: ThemeName; label: string }> = [
{ value: "custom", label: "Custom" },
];
+const SCOPE_LABELS: Array<{ value: AutoDownloadScope; label: string }> = [
+ { value: "all", label: "All" },
+ { value: "none", label: "None" },
+ { value: "whitelist", label: "Whitelist" },
+];
+
+function scopeLabel(scope: AutoDownloadScope): string {
+ return SCOPE_LABELS.find((s) => s.value === scope)?.label ?? scope;
+}
+
export function usePreferencesItems(): SettingItem[] {
const app = useAppStore();
+ const feedStore = useFeedStore();
const settings = () => app.state().settings;
const prefs = () => app.state().preferences;
- return [
+ const items: SettingItem[] = [
{
id: "theme",
label: "Theme",
@@ -97,11 +131,52 @@ export function usePreferencesItems(): SettingItem[] {
kind: "toggle",
display: () => (prefs().autoDownload ? "On" : "Off"),
help: () =>
- `Download new episodes automatically.\nType: toggle\nDefault: false\nCurrent: ${prefs().autoDownload}\nSpace/Enter to toggle.`,
- toggle: () =>
- app.updatePreferences({
- autoDownload: !prefs().autoDownload,
- }),
+ `Download the ${prefs().autoDownloadCount} most recent episodes of your shows automatically (see Count/Scope below).\nType: toggle\nDefault: false\nCurrent: ${prefs().autoDownload ? "On" : "Off"}\nSpace/Enter to toggle.`,
+ toggle: () => {
+ app.updatePreferences({ autoDownload: !prefs().autoDownload });
+ feedStore.runAutoDownload();
+ },
+ },
+ {
+ id: "autoDownloadCount",
+ label: "Auto Download Count",
+ kind: "number",
+ display: () => `${prefs().autoDownloadCount} per show`,
+ help: () =>
+ `How many of the most recent episodes to auto-download per in-scope show.\nType: number (any positive integer)\nDefault: 2\nCurrent: ${prefs().autoDownloadCount}\nj/k to −/+1 · Enter to type a value.`,
+ cycle: (dir) => {
+ const next = Math.max(1, prefs().autoDownloadCount + dir);
+ app.updatePreferences({ autoDownloadCount: next });
+ feedStore.runAutoDownload();
+ },
+ renderEditor: () => (
+ prefs().autoDownloadCount}
+ commit={(n) => {
+ app.updatePreferences({ autoDownloadCount: n });
+ feedStore.runAutoDownload();
+ }}
+ />
+ ),
+ },
+ {
+ id: "autoDownloadScope",
+ label: "Auto Download Scope",
+ kind: "select",
+ display: () => scopeLabel(prefs().autoDownloadScope),
+ help: () =>
+ `Which shows auto-download applies to.\nAll: every subscribed show.\nNone: nothing.\nWhitelist: only the shows you add (in My Shows press ${"w"} on an episode; or open the Whitelist item below).\nType: select\nDefault: all\nCurrent: ${scopeLabel(prefs().autoDownloadScope)}\nCycle with j/k; Enter to apply.`,
+ cycle: (dir) => {
+ const idx = SCOPE_LABELS.findIndex(
+ (s) => s.value === prefs().autoDownloadScope,
+ );
+ const next =
+ SCOPE_LABELS[(idx + dir + SCOPE_LABELS.length) % SCOPE_LABELS.length]
+ .value;
+ app.updatePreferences({ autoDownloadScope: next });
+ feedStore.runAutoDownload();
+ },
},
{
id: "autoJumpToPlayer",
@@ -130,4 +205,225 @@ export function usePreferencesItems(): SettingItem[] {
},
},
];
+
+ // Whitelist management only appears while scope is set to "whitelist".
+ if (prefs().autoDownloadScope === "whitelist") {
+ items.push({
+ id: "autoDownloadWhitelist",
+ label: "Auto Download Whitelist",
+ kind: "editor",
+ display: () => `${prefs().autoDownloadWhitelist.length} shows`,
+ help: () =>
+ `Shows included in auto-download (scope: whitelist).\nSearch your subscribed shows; suggestions toggle in/out with Space.\nType: editor\nCurrent: ${prefs().autoDownloadWhitelist.length} shows`,
+ renderEditor: () => ,
+ });
+ }
+
+ return items;
+}
+
+// ── Number editor ────────────────────────────────────────────────────────────
+// Lets the user type any positive integer (Enter commits; Esc defocuses and
+// j/k ±1 cycling takes over — SettingsPage's depth-2 step handler).
+
+function NumberInputEditor(props: {
+ label: string;
+ value: () => number;
+ commit: (n: number) => void;
+}) {
+ const { theme } = useTheme();
+ const ref = useInputFocusNav();
+ const [draft, setDraft] = createSignal(String(props.value()));
+ const [error, setError] = createSignal(null);
+
+ const submit = () => {
+ const n = Number(draft().trim());
+ if (!Number.isInteger(n) || n < 1) {
+ setError("Enter a whole number ≥ 1");
+ return;
+ }
+ props.commit(n);
+ setError(null);
+ };
+
+ return (
+
+
+ {props.label}
+
+
+ Episodes per show:
+ {
+ setDraft(v);
+ setError(null);
+ }}
+ onSubmit={submit}
+ focused
+ width={8}
+ textColor={theme.text}
+ focusedTextColor={theme.accent}
+ />
+
+
+ {error()}
+
+
+ Type a number, Enter to apply · Esc to browse (j/k ±1) · h back
+
+
+ );
+}
+
+// ── Whitelist editor ─────────────────────────────────────────────────────────
+// Search field over subscribed shows + a navigable suggestion list. Space
+// (toggle-select) toggles the focused show in/out of the whitelist; Enter
+// does the same. While the input is focused, keys type; Esc (handled in the
+// Shell) defocuses so j/k move the list.
+//
+// Transient UI state lives at module level so preference updates (which
+// rebuild the item list) never reset the search or yank focus back into the
+// input mid-browse.
+//
+// The nav.action listener is registered ONCE at module level, not per
+// component instance: toggling a show updates preferences, which remounts
+// the editor (SettingsPage re-resolves the item's renderEditor), and
+// re-registering the listener via onMount/onCleanup during a bus emit
+// mutates the handler set mid-iteration — the event bus then re-delivers to
+// the fresh listener forever. A single stable listener guarded by an active
+// flag sidesteps that entirely.
+
+const [wlQuery, setWlQuery] = createSignal("");
+const [wlCursor, setWlCursor] = createSignal(0);
+const [wlTyping, setWlTyping] = createSignal(true);
+let wlEditorActive = false;
+
+function wlSuggestions(): Feed[] {
+ const q = wlQuery().trim().toLowerCase();
+ const all = useFeedStore().getFilteredFeeds();
+ if (!q) return all;
+ return all.filter((f) =>
+ (f.customName || f.podcast.title).toLowerCase().includes(q),
+ );
+}
+
+/** Keep the cursor inside the (possibly shrinking) suggestion list. */
+function wlCursorClamped(): number {
+ return Math.min(wlCursor(), Math.max(wlSuggestions().length - 1, 0));
+}
+
+function wlToggle(feedId: string): void {
+ const app = useAppStore();
+ const cur = app.state().preferences.autoDownloadWhitelist ?? [];
+ const next = cur.includes(feedId)
+ ? cur.filter((id) => id !== feedId)
+ : [...cur, feedId];
+ app.updatePreferences({ autoDownloadWhitelist: next });
+ useFeedStore().runAutoDownload();
+}
+
+const wlOnAction = (data: {
+ action: KeybindActionName;
+ tab: TABS;
+ pane: PaneId;
+ mode: NavMode;
+}) => {
+ // Fire at most once per dispatch: the editor is only ever open inside the
+ // Settings tab's depth-2 pane, so scope on tab + pane and gate on the
+ // mount flag (which flips during remounts without re-registering).
+ if (!wlEditorActive) return;
+ if (data.tab !== TABS.SETTINGS) return;
+ if (data.pane !== DEPTH_CENTER_PANE) return;
+ const list = wlSuggestions();
+ if (list.length === 0) return;
+ switch (data.action) {
+ case "move-down":
+ setWlCursor((c) => Math.min(c + 1, list.length - 1));
+ break;
+ case "move-up":
+ setWlCursor((c) => Math.max(c - 1, 0));
+ break;
+ case "toggle-select":
+ case "open":
+ wlToggle(list[wlCursorClamped()].id);
+ break;
+ }
+};
+on("nav.action", wlOnAction);
+
+function WhitelistEditor() {
+ const { theme } = useTheme();
+ const feedStore = useFeedStore();
+ const app = useAppStore();
+
+ const whitelist = () => app.state().preferences.autoDownloadWhitelist ?? [];
+ const inList = (feedId: string) => whitelist().includes(feedId);
+
+ onMount(() => {
+ wlEditorActive = true;
+ onCleanup(() => {
+ wlEditorActive = false;
+ });
+ });
+
+ const focusNavRef = useInputFocusNav();
+ const inputRef = (el: InputRenderable | null | undefined) => {
+ focusNavRef(el);
+ if (el) {
+ el.on(RenderableEvents.FOCUSED, () => setWlTyping(true));
+ el.on(RenderableEvents.BLURRED, () => setWlTyping(false));
+ }
+ };
+
+ return (
+
+
+ Auto Download Whitelist
+
+
+ Search:
+
+
+
+
+ No subscribed shows match.
+
+
+
+ {(feed, index) => {
+ const focused = index() === wlCursorClamped();
+ const bg = () => (focused ? theme.primary : undefined);
+ const fg = () => (focused ? theme.surface : theme.text);
+ return (
+
+ {focused ? "❯" : " "}
+ {inList(feed.id) ? "●" : "○"}
+
+ {feed.customName || feed.podcast.title}
+
+
+ );
+ }}
+
+
+ Type to search · Esc to browse · j/k move · Space toggles · h back
+
+
+ );
}
diff --git a/src/stores/app.ts b/src/stores/app.ts
index d09e1d7..f56a130 100644
--- a/src/stores/app.ts
+++ b/src/stores/app.ts
@@ -36,6 +36,9 @@ const defaultSettings: AppSettings = {
const defaultPreferences: UserPreferences = {
showExplicit: false,
autoDownload: false,
+ autoDownloadCount: 2,
+ autoDownloadScope: "all",
+ autoDownloadWhitelist: [],
autoJumpToPlayer: true,
fetchMoreMode: "manual",
};
diff --git a/src/stores/feed.ts b/src/stores/feed.ts
index 996f018..47bc211 100644
--- a/src/stores/feed.ts
+++ b/src/stores/feed.ts
@@ -18,6 +18,7 @@ import {
saveSourcesToFile,
} from "../utils/feeds-persistence";
import { useDownloadStore } from "./download";
+import { useAppStore } from "./app";
import { DownloadStatus } from "../types/episode";
/** Max episodes to load per page/chunk */
@@ -209,29 +210,41 @@ function createFeedStore() {
saveFeeds(updated);
return updated;
});
+ // Global auto-download: newly subscribed shows join the next pass.
+ runAutoDownload();
return newFeed;
};
- /** Auto-download newest episodes for a feed */
- const autoDownloadEpisodes = (
- feedId: string,
- newEpisodes: Episode[],
- count: number,
- ) => {
+ /** Download the N most recent episodes of every in-scope show, per the
+ * global auto-download preferences (master toggle + scope + whitelist +
+ * count). Skips episodes already downloaded, queued, or in flight;
+ * retries failed ones. Idempotent — safe to run after any settings
+ * change, feed refresh, or subscribe. */
+ const runAutoDownload = (): void => {
+ const app = useAppStore();
+ const prefs = app.state().preferences;
+ if (!prefs.autoDownload || prefs.autoDownloadScope === "none") return;
+ const whitelist = prefs.autoDownloadWhitelist ?? [];
+ const count = Math.max(1, prefs.autoDownloadCount ?? 2);
const dlStore = useDownloadStore();
- // Sort by pubDate descending (newest first)
- const sorted = [...newEpisodes].sort(
- (a, b) => b.pubDate.getTime() - a.pubDate.getTime(),
- );
- // count = 0 means download all new episodes
- const toDownload = count > 0 ? sorted.slice(0, count) : sorted;
- for (const ep of toDownload) {
- const status = dlStore.getDownloadStatus(ep.id);
+ for (const feed of feeds()) {
if (
- status === DownloadStatus.NONE ||
- status === DownloadStatus.FAILED
+ prefs.autoDownloadScope === "whitelist" &&
+ !whitelist.includes(feed.id)
) {
- dlStore.startDownload(ep, feedId);
+ continue;
+ }
+ const sorted = [...feed.episodes].sort(
+ (a, b) => b.pubDate.getTime() - a.pubDate.getTime(),
+ );
+ for (const ep of sorted.slice(0, count)) {
+ const status = dlStore.getDownloadStatus(ep.id);
+ if (
+ status === DownloadStatus.NONE ||
+ status === DownloadStatus.FAILED
+ ) {
+ dlStore.startDownload(ep, feed.id);
+ }
}
}
};
@@ -240,7 +253,6 @@ function createFeedStore() {
const refreshFeed = async (feedId: string) => {
const feed = getFeed(feedId);
if (!feed) return;
- const oldEpisodeIds = new Set(feed.episodes.map((e) => e.id));
const episodes = await fetchEpisodes(
feed.podcast.feedUrl,
MAX_EPISODES_REFRESH,
@@ -254,13 +266,9 @@ function createFeedStore() {
return updated;
});
- // Auto-download new episodes if enabled for this feed
- if (feed.autoDownload) {
- const newEpisodes = episodes.filter((e) => !oldEpisodeIds.has(e.id));
- if (newEpisodes.length > 0) {
- autoDownloadEpisodes(feedId, newEpisodes, feed.autoDownloadCount ?? 0);
- }
- }
+ // Global auto-download: ensure the N most recent episodes of in-scope
+ // shows are available offline after every refresh (idempotent).
+ runAutoDownload();
};
/** Refresh all feeds */
@@ -477,13 +485,9 @@ function createFeedStore() {
}
};
- /** Set auto-download settings for a feed */
- const setAutoDownload = (
- feedId: string,
- enabled: boolean,
- count: number = 0,
- ) => {
- updateFeed(feedId, { autoDownload: enabled, autoDownloadCount: count });
+ /** Run the global auto-download pass (see runAutoDownload above). */
+ const runAutoDownloadNow = (): void => {
+ runAutoDownload();
};
return {
@@ -520,7 +524,7 @@ function createFeedStore() {
removeSource,
toggleSource,
updateSource,
- setAutoDownload,
+ runAutoDownload: runAutoDownloadNow,
};
}
diff --git a/src/types/feed.ts b/src/types/feed.ts
index f28adce..1e31b8b 100644
--- a/src/types/feed.ts
+++ b/src/types/feed.ts
@@ -33,10 +33,6 @@ export interface Feed {
isPinned: boolean
/** Feed color for UI */
color?: string
- /** Whether auto-download is enabled for this feed */
- autoDownload?: boolean
- /** Number of newest episodes to auto-download (0 = all new) */
- autoDownloadCount?: number
}
/** Feed item for display in lists */
diff --git a/src/types/settings.ts b/src/types/settings.ts
index 64f3219..ceae72b 100644
--- a/src/types/settings.ts
+++ b/src/types/settings.ts
@@ -87,9 +87,18 @@ export type AppSettings = {
/** How the Feed list loads older episodes (default: manual "[Fetch More]"). */
export type FetchMoreMode = "manual" | "auto";
+/** Which shows the auto-download setting applies to (default: all). */
+export type AutoDownloadScope = "all" | "none" | "whitelist";
+
export type UserPreferences = {
showExplicit: boolean;
autoDownload: boolean;
+ /** Most recent episodes to auto-download per in-scope show (default: 2). */
+ autoDownloadCount: number;
+ /** Shows auto-download covers: all / none / whitelist (default: all). */
+ autoDownloadScope: AutoDownloadScope;
+ /** Feed ids in the auto-download whitelist (used when scope is "whitelist"). */
+ autoDownloadWhitelist: string[];
/** Jump to the Player view automatically when playback starts (default: true) */
autoJumpToPlayer: boolean;
/** Load older episodes from the Feed list: manual button or automatic at the bottom (default: manual). */
diff --git a/src/utils/app-persistence.ts b/src/utils/app-persistence.ts
index 8878caf..a469ea2 100644
--- a/src/utils/app-persistence.ts
+++ b/src/utils/app-persistence.ts
@@ -39,6 +39,9 @@ const defaultSettings: AppSettings = {
const defaultPreferences: UserPreferences = {
showExplicit: false,
autoDownload: false,
+ autoDownloadCount: 2,
+ autoDownloadScope: "all",
+ autoDownloadWhitelist: [],
autoJumpToPlayer: true,
fetchMoreMode: "manual",
};
diff --git a/src/utils/dispatch.ts b/src/utils/dispatch.ts
index b8cc885..7c4160b 100644
--- a/src/utils/dispatch.ts
+++ b/src/utils/dispatch.ts
@@ -76,6 +76,9 @@ export const PAGE_ACTIONS: ReadonlySet =
"toggle-hidden",
"refresh",
"unsubscribe",
+ "download",
+ "delete-download",
+ "whitelist-toggle",
]);
/** Resolve a `tab-goto-N` digit action (1..TabsCount) to a TABS value, or null. */
diff --git a/src/utils/keybinds-persistence.ts b/src/utils/keybinds-persistence.ts
index 8f97602..8f20f08 100644
--- a/src/utils/keybinds-persistence.ts
+++ b/src/utils/keybinds-persistence.ts
@@ -65,6 +65,10 @@ const DEFAULT_KEYBINDS: KeybindsResolved = {
"toggle-hidden": ["."],
refresh: ["r"],
unsubscribe: ["x"],
+ // downloads
+ download: ["d"],
+ "delete-download": ["D"],
+ "whitelist-toggle": ["w"],
// audio transport (preserved; shifted single keys, no collisions)
"audio-toggle": ["P"],
"audio-next": ["N"],