/** * DiscoverPage — yazi depth-stack view of discoverable podcasts. * * depth 0 (current) — category list. Parent pane shows the muted * placeholder (1/5 slot kept). * depth 1 (current) — podcast results for the drilled category. Parent * pane = the categories list. * preview — detail of the hovered item (category summary, or * podcast detail + subscribe action). * * Renders entirely through ``; no bespoke 3-column flexbox JSX * remains. `l`/Enter drills in (category → results) or subscribes (on a * podcast); `h` pops a depth (noop at 0). j/k move only within the current * column. Moving through categories at depth 0 updates the store's selected * category so the preview follows. */ import { createMemo, For, Show, onMount, onCleanup } from "solid-js"; import { useDiscoverStore, DISCOVER_CATEGORIES } from "@/stores/discover"; import { format } from "date-fns"; import { useTheme } from "@/context/ThemeContext"; import { useNavigation, NavMode, DEPTH_CENTER_PANE, type PaneId, type DepthFrame, } from "@/context/NavigationContext"; import { on, off } from "@/utils/event-bus"; import type { KeybindActionName } from "@/context/KeybindContext"; import { PaneRow } from "@/components/PaneRow"; import { TabListPane } from "@/components/TabPanel"; import { useScrollIntoView } from "@/hooks/useScrollIntoView"; export const DiscoverPaneCount = 1; function DiscoverPage() { const discoverStore = useDiscoverStore(); const { theme } = useTheme(); const muted = () => theme.muted || theme.text; const nav = useNavigation(); const depth = nav.currentDepth; const focus = (d: number = depth()) => nav.depthFocus(d); const categories = () => DISCOVER_CATEGORIES; const podcasts = () => discoverStore.filteredPodcasts(); const focusedCatIdx = () => categories().length === 0 ? 0 : Math.min(focus(0), categories().length - 1); const focusedCategory = createMemo(() => categories()[focusedCatIdx()]); const focusedPodIdx = () => podcasts().length === 0 ? 0 : Math.min(focus(1), podcasts().length - 1); const focusedPodcast = createMemo(() => podcasts()[focusedPodIdx()]); const curLen = () => depth() === 0 ? categories().length : podcasts().length; const ensureFocus = () => { if (categories().length > 0 && focus(0) >= categories().length) nav.setDepthFocus(categories().length - 1, 0); if (podcasts().length > 0 && focus(1) >= podcasts().length) nav.setDepthFocus(podcasts().length - 1, 1); }; onMount(ensureFocus); // Auto-fetch the featured-shows manifest on first mount (network failure is // non-fatal — the list stays empty until the user hits refresh). onMount(() => { discoverStore.refresh().catch(() => {}); }); onMount(() => { nav.registerResolver(`${nav.activeTab()}:${DEPTH_CENTER_PANE}`, (i) => { if (depth() === 0) return categories()[i]?.id; return podcasts()[i]?.id; }); }); // ── helpers ──────────────────────────────────────────────────────────────── const formatDate = (d: Date) => format(d, "MMM d, yyyy"); // ── drill / open ─────────────────────────────────────────────────────────── function open() { if (depth() === 0) { const c = focusedCategory(); if (!c) return; discoverStore.setSelectedCategory(c.id); nav.pushDepth({ kind: "results", ctx: c.id, focus: 0 } as DepthFrame); nav.setActivePane(DEPTH_CENTER_PANE); return; } if (depth() >= 1) { const pod = focusedPodcast(); if (pod) discoverStore.toggleSubscription(pod.id); } } // ── nav.action handler ──────────────────────────────────────────────────── const PAGE_ACTIONS: Partial void>> = { "move-down": () => step(1), "move-up": () => step(-1), "jump-down": () => step(5), "jump-up": () => step(-5), "page-down": () => step(10), "page-up": () => step(-10), "goto-top": () => nav.gotoIndex(0, curLen()), "goto-bottom": () => nav.gotoIndex(curLen() - 1, curLen()), open: () => open(), "toggle-select": () => { if (depth() >= 1) { const pod = focusedPodcast(); if (pod) nav.toggleSelected(pod.id); } }, refresh: () => { discoverStore.refresh().catch(() => {}); }, }; function step(delta: number) { nav.move(delta, curLen()); // keep the store's selected category synced with the focused row at depth 0 if (depth() === 0) { const c = focusedCategory(); if (c) discoverStore.setSelectedCategory(c.id); } } const onAction = (data: { action: KeybindActionName; pane: PaneId; mode: NavMode; }) => { if (data.pane !== DEPTH_CENTER_PANE) return; if (nav.activePane() !== DEPTH_CENTER_PANE) return; ensureFocus(); PAGE_ACTIONS[data.action]?.(); }; onMount(() => { on("nav.action", onAction); onCleanup(() => off("nav.action", onAction)); }); // ── render ────────────────────────────────────────────────────────────────── const isActive = () => nav.activePane() === DEPTH_CENTER_PANE; const focusBg = (i: number, lf: number, active: boolean) => i === lf && active ? theme.primary : i === lf ? theme.border : undefined; const focusFg = (i: number, lf: number, active: boolean) => i === lf && active ? theme.surface : i === lf ? theme.selectedListItemText ?? theme.text : theme.text; const currentLabel = () => depth() === 0 ? "Categories" : `${focusedCategory()?.name ?? "Discover"} · ${podcasts().length}`; // ── parent pane: previous-depth list (muted/blank at depth 0) ───────────── // Stable gate (not a ternary root swap) so the parent list // mounts/unmounts cleanly on depth change. const parentContent = () => ( = 1} fallback={}> {(cat, index) => { const lf = () => nav.depthFocus(0); const ref = useScrollIntoView(() => index() === lf()); return ( {index() === nav.depthFocus(0) ? "❯" : " "} {cat.name} ); }} ); // ── current pane ─────────────────────────────────────────────────────────── const currentContent = () => ( <> {/* depth 0: categories */} {(cat, index) => { const lf = () => focusedCatIdx(); const ref = useScrollIntoView(() => index() === lf()); return ( { nav.setActivePane(DEPTH_CENTER_PANE); nav.setDepthFocus(index(), 0); discoverStore.setSelectedCategory(cat.id); }} > {index() === lf() ? "❯" : " "} {cat.name} ); }} {/* depth ≥1: results */} = 1}> 0} fallback={ No podcasts found. :refresh } > {(podcast, index) => { const lf = () => focusedPodIdx(); const ref = useScrollIntoView(() => index() === lf()); return ( { nav.setActivePane(DEPTH_CENTER_PANE); nav.setDepthFocus(index(), 1); }} > {index() === lf() ? "❯" : " "} {podcast.title} [+] by {podcast.author} ); }} ); // ── preview pane ─────────────────────────────────────────────────────────── const previewContent = () => depth() === 0 ? ( // depth 0 preview: shows for the hovered category No category focused } > {(cat) => ( {cat().name} {(cat() as any).description} 0} fallback={ No shows in this category yet. :refresh } > {(pod) => ( {pod.title} by {pod.author} )} )} ) : ( // depth ≥1 preview: hovered podcast + subscribe No podcast focused } > {(pod) => ( {pod().title} by {pod().author} ✓ Subscribed [+] Subscribe (enter) {pod().description?.slice(0, 400) ?? "No description available."} {(pod().description?.length ?? 0) > 400 ? "…" : ""} 0}> {(cat) => [{cat}]} Feed: {pod().feedUrl} Updated: {formatDate(pod().lastUpdated)} enter: subscribe · h: back · r: refresh )} ); return ( (depth() >= 1 ? "Categories" : "Up")} currentLabel={currentLabel} previewLabel="Detail" focused={isActive} /> ); } export { DiscoverPage };