finished hygenie

This commit is contained in:
2026-08-09 14:38:33 -04:00
parent 2abdbaa4e9
commit e1cdd6b2a5
14 changed files with 24 additions and 75 deletions

View File

@@ -3,7 +3,6 @@ import { useTheme } from "@/context/ThemeContext";
const spinnerChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; const spinnerChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
//TODO: Watch for actual loading state (fetching feeds)
export function LoadingIndicator() { export function LoadingIndicator() {
const { theme } = useTheme(); const { theme } = useTheme();
const [index, setIndex] = createSignal(0); const [index, setIndex] = createSignal(0);

View File

@@ -488,11 +488,7 @@ export function createNavigation() {
exitVisual, exitVisual,
// modes // modes
setActiveTabSignal: setActiveTab, setActiveTabSignal: setActiveTab,
setActiveDepth: setPane, // legacy alias
activeDepth: activePane, // legacy alias
setInputFocused, setInputFocused,
nextPane: () => {}, // legacy noop; swipe() replaces this
prevPane: () => {},
setMode, setMode,
enterCommand, enterCommand,
enterInput, enterInput,

View File

@@ -21,20 +21,6 @@ export type MediaKeyAction =
| "media.seekBackward" | "media.seekBackward"
| "media.speedCycle"; | "media.speedCycle";
/** Key-to-action mappings for multimedia controls */
const MEDIA_KEY_MAP: Record<string, MediaKeyAction> = {
// Common terminal media keys — these overlap with Player.tsx local
// bindings, but Player guards on `props.focused` so the global
// handler fires independently when the player tab is *not* active.
//
// When Player IS focused both handlers fire, but since the audio
// actions are idempotent (toggle = toggle, seek = additive) having
// them called twice for the same keypress is avoided by the event
// bus approach — the audio hook only processes event-bus events, and
// Player.tsx calls audio methods directly. We therefore guard with
// a "playerFocused" flag passed via options.
};
export interface MultimediaKeysOptions { export interface MultimediaKeysOptions {
/** When true, skip handling (Player.tsx handles keys locally) */ /** When true, skip handling (Player.tsx handles keys locally) */
playerFocused?: () => boolean; playerFocused?: () => boolean;

View File

@@ -154,7 +154,6 @@ function DiscoverPage() {
: `${focusedCategory()?.name ?? "Discover"} · ${podcasts().length}`; : `${focusedCategory()?.name ?? "Discover"} · ${podcasts().length}`;
// ── parent pane: previous-depth list (muted/blank at depth 0) ───────────── // ── parent pane: previous-depth list (muted/blank at depth 0) ─────────────
// ── parent pane: previous-depth list (muted/blank at depth 0) ──────────
// Stable <Show> gate (not a ternary root swap) so the parent list // Stable <Show> gate (not a ternary root swap) so the parent list
// mounts/unmounts cleanly on depth change. // mounts/unmounts cleanly on depth change.
const parentContent = () => ( const parentContent = () => (

View File

@@ -208,7 +208,6 @@ export function MyShowsPage() {
: `${selectedShow() ? showTitle(selectedShow()!) : "Episodes"} · ${episodes().length}`; : `${selectedShow() ? showTitle(selectedShow()!) : "Episodes"} · ${episodes().length}`;
// ── parent pane: previous-depth list (muted/blank at depth 0) ───────────── // ── parent pane: previous-depth list (muted/blank at depth 0) ─────────────
// ── parent pane: previous-depth list (muted/blank at depth 0) ──────────
// Stable <Show> gate (not a ternary root swap) so the parent list // Stable <Show> gate (not a ternary root swap) so the parent list
// mounts/unmounts cleanly on depth change. // mounts/unmounts cleanly on depth change.
const parentContent = () => ( const parentContent = () => (

View File

@@ -116,7 +116,6 @@ export function RealtimeWaveform(props: RealtimeWaveformProps) {
} }
reader.start(position, speed); reader.start(position, speed);
// Start render loop
frameTimer = setInterval(renderFrame, FRAME_INTERVAL); frameTimer = setInterval(renderFrame, FRAME_INTERVAL);
}; };
@@ -140,11 +139,9 @@ export function RealtimeWaveform(props: RealtimeWaveformProps) {
const renderFrame = () => { const renderFrame = () => {
if (!cava?.isReady || !reader?.running || !sampleBuffer) return; if (!cava?.isReady || !reader?.running || !sampleBuffer) return;
// Read available PCM samples from the stream
const count = reader.read(sampleBuffer); const count = reader.read(sampleBuffer);
if (count === 0) return; if (count === 0) return;
// Feed samples to cavacore → get frequency bars
const input = const input =
count < sampleBuffer.length count < sampleBuffer.length
? sampleBuffer.subarray(0, count) ? sampleBuffer.subarray(0, count)
@@ -198,7 +195,6 @@ export function RealtimeWaveform(props: RealtimeWaveformProps) {
}), }),
); );
// Cleanup on unmount
onCleanup(() => { onCleanup(() => {
stopVisualization(); stopVisualization();
if (reader) { if (reader) {
@@ -222,7 +218,6 @@ export function RealtimeWaveform(props: RealtimeWaveformProps) {
const bars = barData(); const bars = barData();
const count = numBars(); const count = numBars();
// If no data yet, show empty placeholder
if (bars.length === 0) { if (bars.length === 0) {
const placeholder = ".".repeat(count); const placeholder = ".".repeat(count);
return ( return (

View File

@@ -385,8 +385,7 @@ function SearchPage() {
</Show> </Show>
<Show when={result().podcast.description}> <Show when={result().podcast.description}>
<text fg={theme.textSecondary}> <text fg={theme.textSecondary}>
{result().podcast.description!.slice(0, 400) ?? {result().podcast.description!.slice(0, 400)}
"No description available."}
{(result().podcast.description?.length ?? 0) > 400 ? "…" : ""} {(result().podcast.description?.length ?? 0) > 400 ? "…" : ""}
</text> </text>
</Show> </Show>

View File

@@ -3,21 +3,13 @@
* Export dialogs render as depth-2 editors. No own useKeyboard. * Export dialogs render as depth-2 editors. No own useKeyboard.
*/ */
import { createSignal } from "solid-js";
import { ImportDialog } from "./ImportDialog"; import { ImportDialog } from "./ImportDialog";
import { ExportDialog } from "./ExportDialog"; import { ExportDialog } from "./ExportDialog";
import { SyncStatus } from "./SyncStatus";
import type { SettingItem } from "./types"; import type { SettingItem } from "./types";
// Module-level state so the action items can open their dialogs as depth-2 // closeSyncEditor kept for SettingsPage's cleanup hook; its backing state
// editors. The SettingsPage reads `syncEditor()` to decide which dialog to show. // (the syncEditor signal) was removed as dead — nothing ever read it.
const [syncEditor, setSyncEditor] = createSignal<"import" | "export" | null>( export function closeSyncEditor() {}
null,
);
export { syncEditor };
export function closeSyncEditor() {
setSyncEditor(null);
}
export function useSyncItems(): SettingItem[] { export function useSyncItems(): SettingItem[] {
return [ return [
@@ -49,9 +41,3 @@ export function useSyncItems(): SettingItem[] {
}, },
]; ];
} }
/** Renders the live sync status block (used by the Settings page header for the
* Sync section, when relevant). */
export function SyncStatusBlock() {
return <SyncStatus />;
}

View File

@@ -97,14 +97,6 @@ export interface FeedListOptions {
compact: boolean compact: boolean
} }
/** Default feed list options */
export const DEFAULT_FEED_LIST_OPTIONS: FeedListOptions = {
showEpisodeCount: true,
showLastUpdated: true,
showSource: false,
compact: false,
}
/** Feed statistics */ /** Feed statistics */
export interface FeedStats { export interface FeedStats {
/** Total feed count */ /** Total feed count */

View File

@@ -85,7 +85,6 @@ function init() {
); );
const suspended = () => suspendCount() > 0; const suspended = () => suspendCount() > 0;
// Handle keybind shortcuts
useKeyboard((evt) => { useKeyboard((evt) => {
if (suspended()) return; if (suspended()) return;
if (dialog.isOpen) return; if (dialog.isOpen) return;
@@ -258,7 +257,6 @@ function CommandDialog(props: {
return; return;
} }
// Handle text input
if (evt.name && evt.name.length === 1 && !evt.ctrl && !evt.meta) { if (evt.name && evt.name.length === 1 && !evt.ctrl && !evt.meta) {
setFilter((f) => f + evt.name); setFilter((f) => f + evt.name);
return; return;

View File

@@ -12,7 +12,7 @@ export type DialogSize = "medium" | "large"
/** /**
* Dialog component that renders a modal overlay with content. * Dialog component that renders a modal overlay with content.
*/ */
export function Dialog( function Dialog(
props: ParentProps<{ props: ParentProps<{
size?: DialogSize size?: DialogSize
onClose: () => void onClose: () => void

View File

@@ -11,8 +11,8 @@
* *
* Uses a real `Bun.spawn(["sleep", "60"])` subprocess as a stand-in for the * Uses a real `Bun.spawn(["sleep", "60"])` subprocess as a stand-in for the
* player process, injected into the (private) `proc` slot of an MpvBackend — * player process, injected into the (private) `proc` slot of an MpvBackend —
* mpv is the only real backend, and it uses the kill-on-dispose * mpv is the only real backend; its kill-on-dispose path exercises the
* exercising one is enough to guard the family. * whole family, so one test suffices to guard it.
*/ */
import { test, expect } from "bun:test"; import { test, expect } from "bun:test";
import { MpvBackend } from "../src/utils/audio-player"; import { MpvBackend } from "../src/utils/audio-player";

View File

@@ -47,7 +47,7 @@ test("createNavigation starts on the tab root (atRootTab true)", () => {
// ── depth-tab focus: reads/writes the top frame's focus ─────────────────────── // ── depth-tab focus: reads/writes the top frame's focus ───────────────────────
test("depth-tab focusedIndex(DEPTH_CENTER_PANE) returns top frame's focus", () => { test("depth-tab focusedIndex(DEPTH_CENTER_PANE) returns top frame's focus", () => {
withNav((nav) => { withNav((nav) => {
// FeeD is a depth-tab; its root frame is a the top frame on the stack. // FEED is a depth-tab; its root frame is the top frame on the stack.
nav.setActiveTab(TABS.FEED); nav.setActiveTab(TABS.FEED);
expect(nav.isDepthTab()).toBe(true); expect(nav.isDepthTab()).toBe(true);
expect(nav.focusedIndex(DEPTH_CENTER_PANE)).toBe(0); expect(nav.focusedIndex(DEPTH_CENTER_PANE)).toBe(0);

View File

@@ -2,23 +2,23 @@
* yazi-pages-depth.test.ts — task 03 page contract tests. * yazi-pages-depth.test.ts — task 03 page contract tests.
* *
* Every depth-stack tab (Feed / MyShows / Discover / Search / Player / * Every depth-stack tab (Feed / MyShows / Discover / Search / Player /
* Settings) renders through `<YaziPaneRow>` with the parent pane reading the * Settings) renders through `<PaneRow>` with the parent pane reading the
* previous-depth frame's list (blank placeholder at depth 0). Their `open()` * previous-depth frame's list (muted tab-list placeholder at depth 0). Their
* action calls `nav.pushDepth(frame)` to drill and the Shell calls * `open()` action calls `nav.pushDepth(frame)` to drill and the Shell calls
* `nav.popDepth()` on `h`. This file exercises the nav-store contract those * `nav.popDepth()` on `h`. This file exercises the nav-store contract those
* pages depend on for every depth-tab, asserting the parent-slot data model: * pages depend on for every depth-tab, asserting the parent-slot data model:
* *
* • depth 0 → stack has exactly the root frame (parent pane is blank) * • depth 0 → stack has exactly the root frame (parent pane is the muted tab list)
* • drill(l) → push a child frame; stack length 2, parent = previous list * • drill(l) → push a child frame; stack length 2, parent = previous list
* • drill(l) → push again; stack length 3 (Settings sections→items→editor) * • drill(l) → push again; stack length 3 (Settings sections→items→editor)
* • pop(h) → stack shrinks; parent returns to the previous list * • pop(h) → stack shrinks; parent returns to the previous list
* • pop(h) → back at root; parent is blank again * • pop(h) → back at root; parent is the muted tab list again
* *
* The visual "blank → list → list → blank" transition is the union of this * The visual "muted → list → list → muted" transition is the union of this
* data model (which list each depth renders) with `<YaziPaneRow>`'s null * data model (which list each depth renders) with `<PaneRow>`'s parent slot
* placeholder (covered by yazi-pane-row.test.tsx). Tested at the store level * (covered by yazi-pane-row.test.tsx). Tested at the store level because the
* because the page `open()` closures are not exported and the nav store is * page `open()` closures are not exported and the nav store is the shared
* the shared contract all four pages route through. * contract all six depth-tabs route through.
*/ */
import { test, expect } from "bun:test"; import { test, expect } from "bun:test";
@@ -37,7 +37,7 @@ function withNav(fn: (nav: ReturnType<typeof createNavigation>) => void) {
}); });
} }
/** The depth-tabs that render via <YaziPaneRow> (task 03 conversion). */ /** The depth-tabs that render via <PaneRow> (task 03 conversion). */
const CONVERTED_TABS = [ const CONVERTED_TABS = [
TABS.FEED, TABS.FEED,
TABS.MYSHOWS, TABS.MYSHOWS,
@@ -55,7 +55,7 @@ for (const tab of CONVERTED_TABS) {
nav.setActiveTab(tab); nav.setActiveTab(tab);
expect(nav.isDepthTab()).toBe(true); expect(nav.isDepthTab()).toBe(true);
// depth 0: exactly the root frame → parent pane renders blank. // depth 0: exactly the root frame → parent pane is the muted tab list.
expect(nav.currentDepth()).toBe(0); expect(nav.currentDepth()).toBe(0);
expect(nav.depthStack()).toHaveLength(1); expect(nav.depthStack()).toHaveLength(1);
@@ -92,7 +92,7 @@ for (const tab of CONVERTED_TABS) {
expect(nav.depthStack()).toHaveLength(2); expect(nav.depthStack()).toHaveLength(2);
expect(nav.topFrame()).toEqual(child); expect(nav.topFrame()).toEqual(child);
// pop (h): back to depth 0 — parent pane is blank again. // pop (h): back to depth 0 — parent pane is the muted tab list again.
expect(nav.popDepth()).toBe(true); expect(nav.popDepth()).toBe(true);
expect(nav.currentDepth()).toBe(0); expect(nav.currentDepth()).toBe(0);
expect(nav.depthStack()).toHaveLength(1); expect(nav.depthStack()).toHaveLength(1);
@@ -105,7 +105,7 @@ for (const tab of CONVERTED_TABS) {
expect(nav.currentDepth()).toBe(0); expect(nav.currentDepth()).toBe(0);
expect(nav.popDepth()).toBe(false); expect(nav.popDepth()).toBe(false);
expect(nav.currentDepth()).toBe(0); expect(nav.currentDepth()).toBe(0);
// the root frame is preserved (parent stays blank, not undefined). // the root frame is preserved (parent stays the muted tab list, not undefined).
expect(nav.depthStack()).toHaveLength(1); expect(nav.depthStack()).toHaveLength(1);
expect(nav.topFrame()).toBeDefined(); expect(nav.topFrame()).toBeDefined();
}); });