cleaning up code
This commit is contained in:
@@ -69,16 +69,13 @@ function resolveLabel(v: PaneLabel | undefined): string {
|
||||
}
|
||||
|
||||
/** Normalize a PaneContent (static JSX or accessor) into a reactive accessor.
|
||||
* We deliberately do NOT use Solid's `children()` helper here: that helper
|
||||
* flattens accessor children into a stable resolved-nodes array and is the
|
||||
* wrong tool for content whose ROOT swaps at runtime (e.g. the current pane
|
||||
* switching between a depth-1 list fragment and a depth-2 editor — both
|
||||
* truthy JSX roots). `children()` would not re-resolve on a truthy<@->truthy
|
||||
* root swap, freezing the previous subtree in place. Instead we hand the
|
||||
* raw accessor to a reactive `{ expr ?? <Placeholder/> }` expression below,
|
||||
* which Solid compiles into a tracked `insert` effect that disposes the old
|
||||
* subtree and mounts the new whenever the accessor returns a different
|
||||
* element identity. */
|
||||
* We deliberately avoid Solid's `children()` helper: it flattens accessor
|
||||
* children into a stable resolved-nodes array and won't re-resolve on a
|
||||
* truthy→truthy root swap (e.g. the current pane switching between a
|
||||
* depth-1 list fragment and a depth-2 editor), freezing the previous
|
||||
* subtree. Instead the raw accessor feeds a reactive `{ expr ?? <Placeholder/> }`
|
||||
* expression — a tracked `insert` effect that disposes the old subtree and
|
||||
* mounts the new whenever the accessor returns a different element identity. */
|
||||
function normalizeContent(
|
||||
v: PaneContent | undefined,
|
||||
): () => JSX.Element | undefined {
|
||||
@@ -129,20 +126,7 @@ function Pane(props: {
|
||||
borderColor={borderColor()}
|
||||
backgroundColor={theme.background}
|
||||
>
|
||||
{/*
|
||||
* Render the content accessor directly via a reactive expression.
|
||||
* `{ accessor() ?? <Placeholder/> }` compiles to a Solid `insert`
|
||||
* effect that re-runs whenever the accessor's tracked signals
|
||||
* change (e.g. `depth()` swapping the root from a list fragment to
|
||||
* an editor). Solid disposes the previously-rendered subtree and
|
||||
* mounts the new element identity. `null`/`undefined` falls back
|
||||
* to the muted placeholder so the parent pane keeps its 1/7 slot
|
||||
* visibly blank at depth 0. This is the correct tool for root
|
||||
* swapping — unlike Solid's `children()` / `<Show>`-children,
|
||||
* which only react to truthiness flips, not truthy<@->truthy root
|
||||
* identity changes.
|
||||
*/}
|
||||
{props.content() ?? <Placeholder color={muted} />}
|
||||
{props.content() ?? <Placeholder color={muted} />}
|
||||
</scrollbox>
|
||||
</box>
|
||||
);
|
||||
|
||||
@@ -17,10 +17,11 @@ import { useTheme } from "@/context/ThemeContext";
|
||||
import { useKeybinds, type KeybindActionName } from "@/context/KeybindContext";
|
||||
import { useNavigation, NavMode } from "@/context/NavigationContext";
|
||||
import { useAudio } from "@/hooks/useAudio";
|
||||
import { useAudioNavStore, AudioSource } from "@/stores/audio-nav";
|
||||
import { useAudioNavStore } from "@/stores/audio-nav";
|
||||
import { useFeedStore } from "@/stores/feed";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useToast } from "@/ui/toast";
|
||||
import { emit } from "@/utils/event-bus";
|
||||
import { emit, on } from "@/utils/event-bus";
|
||||
import { LayerGraph } from "@/utils/layer-graph";
|
||||
import { TABS, TabPaneCount } from "@/utils/navigation";
|
||||
import { createDispatcher } from "@/utils/dispatch";
|
||||
@@ -48,6 +49,18 @@ export function Shell() {
|
||||
|
||||
const [showHelp, setShowHelp] = createSignal(false);
|
||||
|
||||
// ── Auto jump to Player on podcast start ───────────────────────────────────
|
||||
// Honor the `autoJumpToPlayer` preference: when a NEW episode starts (see
|
||||
// "player.started" — distinct from "player.play", which also fires on
|
||||
// resume), switch to the Player tab and drop into its content pane.
|
||||
on("player.started", () => {
|
||||
const app = useAppStore();
|
||||
if (app.state().preferences.autoJumpToPlayer) {
|
||||
nav.setActiveTab(TABS.PLAYER);
|
||||
nav.enterTabContent(); // PLAYER is a depth-tab — enter its content.
|
||||
}
|
||||
});
|
||||
|
||||
/** Play the episode adjacent (offset ±1) to the currently-playing one,
|
||||
* within its feed's episode list. Updates audio-nav context accordingly. */
|
||||
function advanceEpisode(offset: number) {
|
||||
@@ -83,74 +96,60 @@ export function Shell() {
|
||||
}
|
||||
|
||||
// ── Command bar dispatch ────────────────────────────────────────────────────
|
||||
const COMMANDS: Record<string, (arg: string) => void> = {
|
||||
quit: () => process.exit(0),
|
||||
exit: () => process.exit(0),
|
||||
q: () => process.exit(0),
|
||||
refresh: () =>
|
||||
emit("nav.action", {
|
||||
action: "refresh",
|
||||
tab: nav.activeTab(),
|
||||
pane: nav.activePane(),
|
||||
mode: nav.mode(),
|
||||
}),
|
||||
r: () =>
|
||||
emit("nav.action", {
|
||||
action: "refresh",
|
||||
tab: nav.activeTab(),
|
||||
pane: nav.activePane(),
|
||||
mode: nav.mode(),
|
||||
}),
|
||||
play: () => audio.togglePlayback().catch(() => {}),
|
||||
pause: () => audio.togglePlayback().catch(() => {}),
|
||||
p: () => audio.togglePlayback().catch(() => {}),
|
||||
next: () => advanceEpisode(1),
|
||||
n: () => advanceEpisode(1),
|
||||
prev: () => advanceEpisode(-1),
|
||||
seek: (arg) => {
|
||||
const n = Number(arg) || 0;
|
||||
audio.seek(n).catch(() => {});
|
||||
},
|
||||
feed: () => nav.setActiveTab(TABS.FEED),
|
||||
f: () => nav.setActiveTab(TABS.FEED),
|
||||
shows: () => nav.setActiveTab(TABS.MYSHOWS),
|
||||
myshows: () => nav.setActiveTab(TABS.MYSHOWS),
|
||||
discover: () => nav.setActiveTab(TABS.DISCOVER),
|
||||
d: () => nav.setActiveTab(TABS.DISCOVER),
|
||||
search: () => nav.setActiveTab(TABS.SEARCH),
|
||||
player: () => nav.setActiveTab(TABS.PLAYER),
|
||||
settings: () => nav.setActiveTab(TABS.SETTINGS),
|
||||
set: () => nav.setActiveTab(TABS.SETTINGS),
|
||||
help: () => setShowHelp((v) => !v),
|
||||
h: () => setShowHelp((v) => !v),
|
||||
};
|
||||
|
||||
function runCommand(raw: string) {
|
||||
const cmd = raw.trim();
|
||||
if (!cmd) return;
|
||||
const name = cmd.split(/\s+/)[0].toLowerCase();
|
||||
const arg = cmd.slice(name.length).trim();
|
||||
switch (name) {
|
||||
case "q":
|
||||
case "quit":
|
||||
case "exit":
|
||||
return process.exit(0);
|
||||
case "refresh":
|
||||
case "r":
|
||||
emit("nav.action", {
|
||||
action: "refresh",
|
||||
tab: nav.activeTab(),
|
||||
pane: nav.activePane(),
|
||||
mode: nav.mode(),
|
||||
});
|
||||
break;
|
||||
case "play":
|
||||
case "pause":
|
||||
case "p":
|
||||
audio.togglePlayback().catch(() => {});
|
||||
break;
|
||||
case "next":
|
||||
case "n":
|
||||
advanceEpisode(1);
|
||||
break;
|
||||
case "prev":
|
||||
advanceEpisode(-1);
|
||||
break;
|
||||
case "seek": {
|
||||
const n = Number(arg) || 0;
|
||||
audio.seek(n).catch(() => {});
|
||||
break;
|
||||
}
|
||||
case "feed":
|
||||
case "f":
|
||||
nav.setActiveTab(TABS.FEED);
|
||||
break;
|
||||
case "shows":
|
||||
case "myshows":
|
||||
nav.setActiveTab(TABS.MYSHOWS);
|
||||
break;
|
||||
case "discover":
|
||||
case "d":
|
||||
nav.setActiveTab(TABS.DISCOVER);
|
||||
break;
|
||||
case "search":
|
||||
nav.setActiveTab(TABS.SEARCH);
|
||||
break;
|
||||
case "player":
|
||||
nav.setActiveTab(TABS.PLAYER);
|
||||
break;
|
||||
case "settings":
|
||||
case "set":
|
||||
nav.setActiveTab(TABS.SETTINGS);
|
||||
break;
|
||||
case "help":
|
||||
case "h":
|
||||
setShowHelp((v) => !v);
|
||||
break;
|
||||
default:
|
||||
nav.setCommandError(`unknown command: ${name}`);
|
||||
// re-enter command mode so the user sees the error + can correct
|
||||
nav.enterCommand();
|
||||
nav.setCommandBuffer(cmd);
|
||||
}
|
||||
const unknownCommand = () => {
|
||||
nav.setCommandError(`unknown command: ${name}`);
|
||||
// re-enter command mode so the user sees the error + can correct
|
||||
nav.enterCommand();
|
||||
nav.setCommandBuffer(cmd);
|
||||
};
|
||||
(COMMANDS[name] ?? unknownCommand)(arg);
|
||||
}
|
||||
|
||||
// ── Command-mode key handling ───────────────────────────────────────────────
|
||||
@@ -458,18 +457,5 @@ function k_match_escape(evt: any): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Exposed so App can route an externally-triggered "play episode" (e.g. from
|
||||
* search) into the player tab. */
|
||||
export function playEpisodeAndSwitch(
|
||||
nav: ReturnType<typeof useNavigation>,
|
||||
audio: ReturnType<typeof useAudio>,
|
||||
episode: import("@/types/episode").Episode,
|
||||
) {
|
||||
audio.play(episode);
|
||||
nav.setActiveTab(TABS.PLAYER);
|
||||
nav.enterTabContent(); // PLAYER is a depth-tab — drop into its content pane.
|
||||
useAudioNavStore().setSource(AudioSource.FEED);
|
||||
}
|
||||
|
||||
// Re-export Episode type for callers building pane trees.
|
||||
export type { Episode } from "@/types/episode";
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { For } from "solid-js";
|
||||
import { shortcuts } from "@/config/shortcuts";
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
|
||||
/** Yazi-style keybind reference. The Shell has its own overlay; this component
|
||||
* is kept for embedding inside Settings or other surfaces. */
|
||||
export function ShortcutHelp() {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<box
|
||||
border
|
||||
title="Shortcuts"
|
||||
style={{ flexDirection: "column", padding: 1 }}
|
||||
>
|
||||
<box style={{ flexDirection: "column" }}>
|
||||
<For each={shortcuts}>
|
||||
{(s) => (
|
||||
<box style={{ flexDirection: "row" }} gap={2}>
|
||||
<text fg={theme.accent}>{s.keys}</text>
|
||||
<text fg={theme.text}>{s.action}</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import { TABS, TabsCount } from "@/utils/navigation";
|
||||
import { For } from "solid-js";
|
||||
import { SelectableBox, SelectableText } from "@/components/Selectable";
|
||||
import { useNavigation } from "@/context/NavigationContext";
|
||||
|
||||
export const tabs: TabDefinition[] = [
|
||||
{ id: TABS.FEED, label: "Feed" },
|
||||
{ id: TABS.MYSHOWS, label: "My Shows" },
|
||||
{ id: TABS.DISCOVER, label: "Discover" },
|
||||
{ id: TABS.SEARCH, label: "Search" },
|
||||
{ id: TABS.PLAYER, label: "Player" },
|
||||
{ id: TABS.SETTINGS, label: "Settings" },
|
||||
];
|
||||
|
||||
export function TabNavigation() {
|
||||
const { theme } = useTheme();
|
||||
const { activeTab, setActiveTab, activeDepth } = useNavigation();
|
||||
return (
|
||||
<box
|
||||
border
|
||||
borderColor={activeDepth() !== 0 ? theme.border : theme.accent}
|
||||
backgroundColor={"transparent"}
|
||||
style={{
|
||||
flexDirection: "column",
|
||||
width: 12,
|
||||
height: TabsCount * 3 + 2,
|
||||
}}
|
||||
>
|
||||
<For each={tabs}>
|
||||
{(tab) => (
|
||||
<SelectableBox
|
||||
border
|
||||
height={3}
|
||||
selected={() => tab.id == activeTab()}
|
||||
onMouseDown={() => setActiveTab(tab.id)}
|
||||
>
|
||||
<SelectableText
|
||||
selected={() => tab.id == activeTab()}
|
||||
primary
|
||||
alignSelf="center"
|
||||
>
|
||||
{tab.label}
|
||||
</SelectableText>
|
||||
</SelectableBox>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
export type TabDefinition = {
|
||||
id: TABS;
|
||||
label: string;
|
||||
};
|
||||
Reference in New Issue
Block a user