feat(search): stream unsubscribed episodes directly; a subscribes in place

This commit is contained in:
2026-08-11 14:01:46 -04:00
parent 1f0b9de456
commit 005ac8fde3
7 changed files with 380 additions and 4 deletions

View File

@@ -67,6 +67,7 @@
"sort": [","],
"toggle-hidden": ["."],
"refresh": ["r"],
"subscribe": ["a"], // subscribe focused show/episode result in place (Search)
"unsubscribe": ["x"], // unsubscribe focused show in My Shows
// ── Downloads & auto-download whitelist ───────────────────────────────────

View File

@@ -68,6 +68,7 @@ export type KeybindActionName =
| "sort"
| "toggle-hidden"
| "refresh"
| "subscribe"
| "unsubscribe"
| "download"
| "delete-download"

View File

@@ -264,6 +264,16 @@ function SearchPage() {
}
};
/** Subscribe the focused result's show in place (episode or podcast
* result). `enter` plays episodes regardless of subscription, so an
* unsubscribed show's episode needs this explicit path. */
const subscribeFocused = () => {
if (depth() !== 1) return;
const r = focusedResult();
if (!r || r.podcast.isSubscribed) return;
handleSubscribe(r);
};
// ── nav.action handler ──────────────────────────────────────────────────────
const PAGE_ACTIONS: Partial<Record<KeybindActionName, () => void>> = {
"move-down": () => step(1),
@@ -295,6 +305,7 @@ function SearchPage() {
downloadStore.removeDownload(id).catch(() => {});
},
unsubscribe: () => unsubscribeFocused(),
subscribe: () => subscribeFocused(),
search: () => {
// `s` refocuses the query input (typing mode) when on the query depth.
if (depth() === 0) nav.setInputFocused(true);
@@ -321,8 +332,10 @@ function SearchPage() {
if (depth() === 1) {
const r = focusedResult();
if (!r) return;
if (r.kind === "episode" && r.podcast.isSubscribed) {
// Subscribed show's episode stream it (matches Feed/My Shows).
if (r.kind === "episode") {
// Any episode result streams directly — subscribed or not
// (matches Feed/My Shows). `a` subscribes an unsubscribed
// show's episode in place.
playFocusedEpisode();
return;
}
@@ -706,7 +719,7 @@ function SearchPage() {
</Show>
<box height={1} />
<Show when={!r.podcast.isSubscribed}>
<text fg={theme.primary}>[+] Subscribe (enter)</text>
<text fg={theme.primary}>[+] Subscribe (a)</text>
</Show>
<Show when={r.podcast.isSubscribed}>
<text fg={theme.success}>
@@ -718,7 +731,7 @@ function SearchPage() {
when={r.podcast.isSubscribed}
fallback={
<text fg={muted()}>
enter: subscribe · d: download · h: back to query
enter: play · a: subscribe · d: download · h: back to query
</text>
}
>

View File

@@ -76,6 +76,7 @@ export const PAGE_ACTIONS: ReadonlySet<KeybindActionName> =
"sort",
"toggle-hidden",
"refresh",
"subscribe",
"unsubscribe",
"download",
"delete-download",

View File

@@ -65,6 +65,8 @@ const DEFAULT_KEYBINDS: KeybindsResolved = {
sort: [","],
"toggle-hidden": ["."],
refresh: ["r"],
// a subscribes the focused show/episode result in place (x unsubscribes)
subscribe: ["a"],
unsubscribe: ["x"],
// downloads
download: ["d"],