finished hygenie
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 = () => (
|
||||||
|
|||||||
@@ -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 = () => (
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 />;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user