docs: mark AUR package as pending registration reopen

This commit is contained in:
2026-08-08 07:09:07 -04:00
parent 8dbdebfd30
commit 13a31aabdc
21 changed files with 1103 additions and 606 deletions

View File

@@ -8,7 +8,7 @@
* preview — detail of the hovered item (category summary, or
* podcast detail + subscribe action).
*
* Renders entirely through `<YaziPaneRow>`; no bespoke 3-column flexbox JSX
* Renders entirely through `<PaneRow>`; 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
@@ -28,8 +28,9 @@ import {
} from "@/context/NavigationContext";
import { on, off } from "@/utils/event-bus";
import type { KeybindActionName } from "@/context/KeybindContext";
import { YaziPaneRow } from "@/components/YaziPaneRow";
import { PaneRow } from "@/components/PaneRow";
import { TabListPane } from "@/components/TabPanel";
import { useScrollIntoView } from "@/hooks/useScrollIntoView";
export const DiscoverPaneCount = 1;
@@ -39,7 +40,6 @@ function DiscoverPage() {
const muted = () => theme.muted || theme.text;
const nav = useNavigation();
const stack = nav.depthStack;
const depth = nav.currentDepth;
const focus = (d: number = depth()) => nav.depthFocus(d);
@@ -160,22 +160,27 @@ function DiscoverPage() {
const parentContent = () => (
<Show when={depth() >= 1} fallback={<TabListPane muted />}>
<For each={categories()}>
{(cat, index) => (
<box
flexDirection="row"
gap={1}
paddingLeft={1}
paddingRight={1}
backgroundColor={focusBg(index(), nav.depthFocus(0), false)}
>
<text fg={focusFg(index(), nav.depthFocus(0), false)}>
{index() === nav.depthFocus(0) ? "" : " "}
</text>
<text fg={focusFg(index(), nav.depthFocus(0), false)}>
{cat.name}
</text>
</box>
)}
{(cat, index) => {
const lf = () => nav.depthFocus(0);
const ref = useScrollIntoView(() => index() === lf());
return (
<box
ref={ref}
flexDirection="row"
gap={1}
paddingLeft={1}
paddingRight={1}
backgroundColor={focusBg(index(), lf(), false)}
>
<text fg={focusFg(index(), nav.depthFocus(0), false)}>
{index() === nav.depthFocus(0) ? "" : " "}
</text>
<text fg={focusFg(index(), nav.depthFocus(0), false)}>
{cat.name}
</text>
</box>
);
}}
</For>
</Show>
);
@@ -188,9 +193,10 @@ function DiscoverPage() {
<For each={categories()}>
{(cat, index) => {
const lf = () => focusedCatIdx();
const selected = () => cat.id === discoverStore.selectedCategory();
const ref = useScrollIntoView(() => index() === lf());
return (
<box
ref={ref}
flexDirection="row"
gap={1}
paddingLeft={1}
@@ -206,11 +212,6 @@ function DiscoverPage() {
{index() === lf() ? "" : " "}
</text>
<text fg={focusFg(index(), lf(), isActive())}>{cat.name}</text>
<Show when={selected()}>
<text fg={index() === lf() ? theme.surface : theme.accent}>
*
</text>
</Show>
</box>
);
}}
@@ -229,8 +230,10 @@ function DiscoverPage() {
<For each={podcasts()}>
{(podcast, index) => {
const lf = () => focusedPodIdx();
const ref = useScrollIntoView(() => index() === lf());
return (
<box
ref={ref}
flexDirection="column"
gap={0}
paddingLeft={1}
@@ -276,7 +279,7 @@ function DiscoverPage() {
// ── preview pane ───────────────────────────────────────────────────────────
const previewContent = () =>
depth() === 0 ? (
// depth 0 preview: hovered category
// depth 0 preview: shows for the hovered category
<Show
when={focusedCategory()}
fallback={
@@ -286,16 +289,35 @@ function DiscoverPage() {
}
>
{(cat) => (
<box flexDirection="column" gap={1} padding={1}>
<box flexDirection="column" gap={0} padding={1}>
<text fg={theme.textPrimary ?? theme.text}>
<strong>{cat().name}</strong>
</text>
<text fg={theme.textSecondary}>
{(cat() as any).description ??
`Browse top podcasts in ${cat().name}.`}
</text>
<Show when={(cat() as any).description}>
<text fg={theme.textSecondary}>{(cat() as any).description}</text>
</Show>
<box height={1} />
<text fg={muted()}>enter/l: open · h: back</text>
<Show
when={podcasts().length > 0}
fallback={
<text fg={muted()}>
No shows in this category yet. :refresh
</text>
}
>
<For each={podcasts()}>
{(pod) => (
<box flexDirection="column" gap={0}>
<text fg={theme.text}>{pod.title}</text>
<Show when={pod.author}>
<text fg={muted()} paddingLeft={2}>
by {pod.author}
</text>
</Show>
</box>
)}
</For>
</Show>
</box>
)}
</Show>
@@ -347,7 +369,7 @@ function DiscoverPage() {
);
return (
<YaziPaneRow
<PaneRow
parent={parentContent}
current={currentContent}
preview={previewContent}

View File

@@ -10,7 +10,7 @@
* duplicated My Shows (shows → episodes). Per design, the Feed tab now just
* shows the full flat episodes list immediately.
*
* Renders entirely through `<YaziPaneRow>` (the shared parent|current|preview
* Renders entirely through `<PaneRow>` (the shared parent|current|preview
* primitive). `l`/Enter plays the focused episode; `h` pops back to the tab
* root. j/k move only within the current column. The Shell router drives
* everything over `nav.action`; this page only handles list/preview data.
@@ -35,8 +35,9 @@ import type { KeybindActionName } from "@/context/KeybindContext";
import type { Episode } from "@/types/episode";
import type { Feed } from "@/types/feed";
import { LoadingIndicator } from "@/components/LoadingIndicator";
import { YaziPaneRow } from "@/components/YaziPaneRow";
import { PaneRow } from "@/components/PaneRow";
import { TabListPane } from "@/components/TabPanel";
import { useScrollIntoView } from "@/hooks/useScrollIntoView";
export const FeedPaneCount = 1;
@@ -187,8 +188,10 @@ function FeedPage() {
<For each={episodes()}>
{(item, index) => {
const fi = () => focusedEpIdx();
const ref = useScrollIntoView(() => index() === fi());
return (
<box
ref={ref}
flexDirection="column"
gap={0}
paddingLeft={1}
@@ -290,7 +293,7 @@ function FeedPage() {
);
return (
<YaziPaneRow
<PaneRow
parent={parentContent}
current={currentContent}
preview={previewContent}

View File

@@ -6,7 +6,7 @@
* depth 1 (current) — episodes of the drilled show. Parent pane = shows.
* preview — detail of the hovered item in the current column.
*
* Renders entirely through `<YaziPaneRow>`; no bespoke 3-column flexbox JSX
* Renders entirely through `<PaneRow>`; no bespoke 3-column flexbox JSX
* remains. `l`/Enter drills in (show → episodes); `h` pops a depth (noop at
* 0). j/k move only within the current column.
*/
@@ -31,8 +31,9 @@ import type { KeybindActionName } from "@/context/KeybindContext";
import type { Episode } from "@/types/episode";
import type { Feed } from "@/types/feed";
import { LoadingIndicator } from "@/components/LoadingIndicator";
import { YaziPaneRow } from "@/components/YaziPaneRow";
import { PaneRow } from "@/components/PaneRow";
import { TabListPane } from "@/components/TabPanel";
import { useScrollIntoView } from "@/hooks/useScrollIntoView";
export const MyShowsPaneCount = 1;
@@ -164,6 +165,16 @@ export function MyShowsPage() {
const show = selectedShow();
if (show) feedStore.refreshFeed(show.id).catch(() => {});
},
unsubscribe: () => {
if (depth() !== 0) return;
const show = selectedShow();
if (show) {
// unsubscribe = remove feed + purge its downloaded files
feedStore.removeFeed(show.id);
downloadStore.removeDownloadsForFeed(show.id).catch(() => {});
ensureFocus();
}
},
};
function step(delta: number) {
nav.move(delta, curLen());
@@ -205,8 +216,10 @@ export function MyShowsPage() {
<For each={shows()}>
{(feed, index) => {
const lf = () => nav.depthFocus(0);
const ref = useScrollIntoView(() => index() === lf());
return (
<box
ref={ref}
flexDirection="row"
gap={1}
paddingLeft={1}
@@ -243,8 +256,10 @@ export function MyShowsPage() {
<For each={shows()}>
{(feed, index) => {
const lf = () => focusedShowIdx();
const ref = useScrollIntoView(() => index() === lf());
return (
<box
ref={ref}
flexDirection="row"
gap={1}
paddingLeft={1}
@@ -283,8 +298,10 @@ export function MyShowsPage() {
<For each={episodes()}>
{(ep, index) => {
const lf = () => focusedEpIdx();
const ref = useScrollIntoView(() => index() === lf());
return (
<box
ref={ref}
flexDirection="column"
gap={0}
paddingLeft={1}
@@ -361,7 +378,7 @@ export function MyShowsPage() {
{show().podcast.description?.slice(0, 400) ?? "No description."}
</text>
<box height={1} />
<text fg={muted()}>enter/l: open · h: back</text>
<text fg={muted()}>enter/l: open · h: back · x: unsubscribe</text>
</box>
)}
</Show>
@@ -408,7 +425,7 @@ export function MyShowsPage() {
);
return (
<YaziPaneRow
<PaneRow
parent={parentContent}
current={currentContent}
preview={previewContent}

View File

@@ -4,7 +4,7 @@
* depth 0 (parent) — tab list (muted, read-only).
* depth 0 (current) — the single now-playing pane (rich view + controls).
*
* No preview pane (YaziPaneRow `panes={2}`). Audio transport (play/pause,
* No preview pane (PaneRow `panes={2}`). Audio transport (play/pause,
* next/prev, seek) is handled globally by the Shell router (P/N/B/</>); this
* page only renders the now-playing surface. `h` at depth 0 returns to the
* tab root.
@@ -17,7 +17,7 @@ import { useAudio } from "@/hooks/useAudio";
import { useAppStore } from "@/stores/app";
import { useTheme } from "@/context/ThemeContext";
import { useNavigation, DEPTH_CENTER_PANE } from "@/context/NavigationContext";
import { YaziPaneRow } from "@/components/YaziPaneRow";
import { PaneRow } from "@/components/PaneRow";
import { TabListPane } from "@/components/TabPanel";
export const PlayerPaneCount = 1;
@@ -116,7 +116,7 @@ export function PlayerPage() {
);
return (
<YaziPaneRow
<PaneRow
parent={parentContent}
current={currentContent}
parentLabel="Up"

View File

@@ -38,8 +38,9 @@ import {
import { on, off } from "@/utils/event-bus";
import type { KeybindActionName } from "@/context/KeybindContext";
import type { SearchResult } from "@/types/source";
import { YaziPaneRow } from "@/components/YaziPaneRow";
import { PaneRow } from "@/components/PaneRow";
import { TabListPane } from "@/components/TabPanel";
import { useScrollIntoView } from "@/hooks/useScrollIntoView";
export const SearchPaneCount = 1;
@@ -256,8 +257,10 @@ function SearchPage() {
<For each={recents()}>
{(query, index) => {
const lf = () => focus(0);
const ref = useScrollIntoView(() => index() === lf());
return (
<box
ref={ref}
flexDirection="row"
gap={1}
paddingLeft={1}
@@ -302,8 +305,10 @@ function SearchPage() {
<For each={results()}>
{(result, index) => {
const fi = () => focusedResultIdx();
const ref = useScrollIntoView(() => index() === fi());
return (
<box
ref={ref}
flexDirection="column"
gap={0}
paddingLeft={1}
@@ -419,7 +424,7 @@ function SearchPage() {
: `Results · ${results().length}`;
return (
<YaziPaneRow
<PaneRow
parent={parentContent}
current={currentContent}
preview={previewContent}

View File

@@ -0,0 +1,124 @@
/**
* DownloadManager — exposes downloads as SettingItems for the depth-stack.
*
* • "Delete All Downloads" — action item; Enter wipes every download.
* • one item per show — action item; Enter deletes all that show's
* downloads (file + metadata, aborts in-flight).
* • one item per episode — action item; Enter deletes a single download.
*
* Titles resolve from the feed store at render time (reactive), falling back
* to the episode id when the feed is no longer loaded. Movement flows through
* nav.action — no own useKeyboard (matches the other panels).
*/
import { useFeedStore } from "@/stores/feed";
import { useDownloadStore } from "@/stores/download";
import { DownloadStatus } from "@/types/episode";
import type { DownloadedEpisode } from "@/types/episode";
import type { SettingItem } from "./types";
/** Format a byte count as a compact human string. */
function fmtBytes(n: number): string {
if (n >= 1 << 20) return `${(n / (1 << 20)).toFixed(1)} MB`;
if (n >= 1 << 10) return `${(n / (1 << 10)).toFixed(0)} KB`;
return `${n} B`;
}
/** Short status badge for an episode download. */
function statusLabel(s: DownloadStatus): string {
switch (s) {
case DownloadStatus.QUEUED:
return "queued";
case DownloadStatus.DOWNLOADING:
return "downloading";
case DownloadStatus.COMPLETED:
return "done";
case DownloadStatus.FAILED:
return "failed";
default:
return "";
}
}
/** Episode title for a download, resolved from the feed store (reactive). */
function episodeTitle(
feedStore: ReturnType<typeof useFeedStore>,
d: DownloadedEpisode,
): string {
const feed = feedStore.getFeed(d.feedId);
const ep = feed?.episodes.find((e) => e.id === d.episodeId);
return ep?.title ?? d.episodeId;
}
/** Show title for a download's feed id. */
function feedTitle(
feedStore: ReturnType<typeof useFeedStore>,
feedId: string,
): string {
const feed = feedStore.getFeed(feedId);
return feed ? feed.customName || feed.podcast.title : feedId;
}
export function useDownloadItems(): SettingItem[] {
const downloadStore = useDownloadStore();
const feedStore = useFeedStore();
const downloads = () => downloadStore.getAllDownloads();
const items: SettingItem[] = [
{
id: "clear-all",
label: "Delete All Downloads",
kind: "action",
display: () => `${downloads().length} files`,
help: () =>
`Delete every downloaded episode (files + metadata) and clear the\nqueue. Enter to run.`,
run: () => {
for (const d of downloads()) {
downloadStore.cancelDownload(d.episodeId);
downloadStore.removeDownload(d.episodeId).catch(() => {});
}
},
},
];
// Group downloads by feed so each show gets a delete-by-show item.
const byFeed = new Map<string, DownloadedEpisode[]>();
for (const d of downloads()) {
const arr = byFeed.get(d.feedId) ?? [];
arr.push(d);
byFeed.set(d.feedId, arr);
}
for (const [feedId, eps] of byFeed) {
const size = eps.reduce((s, e) => s + e.fileSize, 0);
items.push({
id: `feed:${feedId}`,
label: `Show: ${feedTitle(feedStore, feedId)}`,
kind: "action",
display: () => `${eps.length} · ${fmtBytes(size)}`,
help: () =>
`Delete all ${eps.length} downloads for this show (files + metadata,\naborts any in-flight transfers). Enter to run.`,
run: () => {
downloadStore.removeDownloadsForFeed(feedId).catch(() => {});
},
});
}
// One item per individual episode download.
for (const d of downloads()) {
items.push({
id: `ep:${d.episodeId}`,
label: episodeTitle(feedStore, d),
kind: "action",
display: () =>
`${feedTitle(feedStore, d.feedId)} · ${statusLabel(d.status)} · ${fmtBytes(d.fileSize)}`,
help: () =>
`Delete this single download (file + metadata). Enter to run.`,
run: () => {
downloadStore.removeDownload(d.episodeId).catch(() => {});
},
});
}
return items;
}

View File

@@ -5,7 +5,7 @@
* depth 1 — the focused section's items as a navigable list
* depth 2 — per-item editor (for editor-kind items) or value adjuster
*
* Renders entirely through `<YaziPaneRow>` (parent | current | preview):
* Renders entirely through `<PaneRow>` (parent | current | preview):
* parent = previous depth's list (sections at depth 1, items at depth 2);
* blank placeholder at depth 0 (1/7 slot kept).
* current = the current-depth list (or editor at depth 2); the only
@@ -33,8 +33,10 @@ import { usePreferencesItems } from "./PreferencesPanel";
import { useVisualizerItems } from "./VisualizerSettings";
import { useSyncItems, closeSyncEditor } from "./SyncPanel";
import { useSourceItems } from "./SourceManager";
import { YaziPaneRow } from "@/components/YaziPaneRow";
import { useDownloadItems } from "./DownloadManager";
import { PaneRow } from "@/components/PaneRow";
import { TabListPane } from "@/components/TabPanel";
import { useScrollIntoView } from "@/hooks/useScrollIntoView";
export const SettingsPaneCount = 1;
@@ -64,6 +66,11 @@ const SECTIONS: SettingsSectionDef[] = [
label: "Account",
description: "Account login & OAuth (not yet implemented).",
},
{
id: 5,
label: "Downloads",
description: "Manage downloaded episodes — delete by show or individually.",
},
];
/** Resolve the items for a section id at render time. Section 4 (Account) has
@@ -78,6 +85,8 @@ function sectionItems(sectionId: number): SettingItem[] {
return usePreferencesItems();
case 3:
return useVisualizerItems();
case 5:
return useDownloadItems();
default:
return [];
}
@@ -377,7 +386,7 @@ export function SettingsPage() {
);
return (
<YaziPaneRow
<PaneRow
parent={parentContent}
current={currentContent}
preview={previewContent}
@@ -422,8 +431,10 @@ function Row(props: {
? theme.border
: undefined;
const fg = () => (props.focused && props.active ? theme.surface : theme.text);
const ref = useScrollIntoView(() => props.focused);
return (
<box
ref={ref}
flexDirection="row"
gap={1}
paddingLeft={1}