/** * PlayerPage — 2-pane yazi depth view of the now-playing episode. * * depth 0 (parent) — tab list (muted, read-only). * depth 0 (current) — the single now-playing pane (rich view + controls). * * 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. */ import { Show, onMount, onCleanup } from "solid-js"; import { PlaybackControls } from "./PlaybackControls"; import { ProgressBar } from "./ProgressBar"; import { RealtimeWaveform } from "./RealtimeWaveform"; import { useAudio } from "@/hooks/useAudio"; import { useVisualizer } from "@/stores/visualizer"; import { useAppStore } from "@/stores/app"; import { useTheme } from "@/context/ThemeContext"; import { useNavigation, DEPTH_CENTER_PANE } from "@/context/NavigationContext"; import { PaneRow } from "@/components/PaneRow"; import { TabListPane } from "@/components/TabPanel"; export const PlayerPaneCount = 1; export function PlayerPage() { const audio = useAudio(); const { theme } = useTheme(); const nav = useNavigation(); const viz = useVisualizer(); const app = useAppStore(); const muted = () => theme.muted || theme.text; // Settings master switch: off hides the waveform entirely (the store // also stops the decode+FFT pipeline, see stores/visualizer.ts). const vizEnabled = () => app.state().settings.visualizer.enabled; // The page is mounted exactly while the Player tab is in focus (Shell // renders only the active tab), so mount ⇔ focused. Report it to the // visualizer store: losing focus starts the unload grace timer instead // of killing the pipeline with the page; regaining focus restarts it. onMount(() => viz.setFocused(true)); onCleanup(() => viz.setFocused(false)); const isActive = () => nav.activePane() === DEPTH_CENTER_PANE; const progressPercent = () => { const d = audio.duration(); if (d <= 0) return 0; return Math.min(100, Math.round((audio.position() / d) * 100)); }; const formatTime = (seconds: number) => { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${String(s).padStart(2, "0")}`; }; // ── parent pane: the tab list (muted) ────────────────────────────────────── const parentContent = () => ; // ── current pane: now playing ─────────────────────────────────────────────── const currentContent = () => ( Now Playing {formatTime(audio.position())} / {formatTime(audio.duration())} ( {progressPercent()}%) {(err) => {err()}} No episode loaded. } > {(ep) => ( {ep().title} {ep().description?.slice(0, 500) ?? "No description available."} )} audio.seek(0)} onNext={() => audio.seek(audio.currentEpisode()?.duration ?? 0)} onSpeedChange={(s: number) => audio.setSpeed(s)} onVolumeChange={(v: number) => audio.setVolume(v)} /> {/* content prop (not a text child): the babel-preset-solid JSX * transform HTML-escapes static string children (`<` → `<`), * which opentui renders verbatim; content bypasses that. */} seek h back"} /> ); return ( ); }