docs: mark AUR package as pending registration reopen
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
124
src/pages/Settings/DownloadManager.tsx
Normal file
124
src/pages/Settings/DownloadManager.tsx
Normal 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;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user