start revive

This commit is contained in:
2026-07-30 21:25:03 -04:00
parent b7c4938c54
commit d8f11040bc
25 changed files with 5344 additions and 2559 deletions

View File

@@ -1,112 +1,122 @@
/**
* PlayerPage — single-pane audio now-playing view.
*
* Audio transport (play/pause, next/prev, seek) is handled globally by the
* Shell router (P/N/B/</>). This page renders a single rich pane showing the
* current episode, waveform, and playback controls. Panes/swipe do nothing
* (PaneCount=1).
*/
import { Show } from "solid-js";
import { PlaybackControls } from "./PlaybackControls";
import { RealtimeWaveform } from "./RealtimeWaveform";
import { useAudio } from "@/hooks/useAudio";
import { useAppStore } from "@/stores/app";
import { useTheme } from "@/context/ThemeContext";
import { useNavigation } from "@/context/NavigationContext";
import { useKeybinds } from "@/context/KeybindContext";
import { useKeyboard } from "@opentui/solid";
import { onMount } from "solid-js";
enum PlayerPaneType {
PLAYER = 1,
}
export const PlayerPaneCount = 1;
export function PlayerPage() {
const audio = useAudio();
const { theme } = useTheme();
const nav = useNavigation();
const audio = useAudio();
const { theme } = useTheme();
const nav = useNavigation();
const muted = () => theme.muted || theme.text;
const keybind = useKeybinds();
// Single pane — always active.
const isActive = () => true;
const border = () => theme.accent;
onMount(() => {
useKeyboard(
(keyEvent: any) => {
const isInverting = keybind.isInverting(keyEvent);
const progressPercent = () => {
const d = audio.duration();
if (d <= 0) return 0;
return Math.min(100, Math.round((audio.position() / d) * 100));
};
if (keybind.match("audio-toggle", keyEvent)) {
audio.togglePlayback();
return;
}
const formatTime = (seconds: number) => {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${String(s).padStart(2, "0")}`;
};
if (keybind.match("audio-seek-forward", keyEvent)) {
audio.seek(audio.currentEpisode()?.duration ?? 0);
return;
}
return (
<box flexDirection="column" width="100%" height="100%">
{/* ── pane 0: now playing ─────────────────────────────────────────── */}
<box height={1} paddingLeft={1} backgroundColor={theme.background}>
<text fg={theme.textSecondary}>Player</text>
</box>
<scrollbox
height="100%"
focused={isActive()}
border
borderColor={border()}
backgroundColor={theme.background}
>
<box flexDirection="column" gap={1} padding={1}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text}>
<strong>Now Playing</strong>
</text>
<text fg={muted()}>
{formatTime(audio.position())} / {formatTime(audio.duration())} (
{progressPercent()}%)
</text>
</box>
if (keybind.match("audio-seek-backward", keyEvent)) {
audio.seek(0);
return;
}
},
{ release: false },
);
});
<Show when={audio.error()}>
{(err) => <text fg={theme.error}>{err()}</text>}
</Show>
const progressPercent = () => {
const d = audio.duration();
if (d <= 0) return 0;
return Math.min(100, Math.round((audio.position() / d) * 100));
};
<Show
when={audio.currentEpisode()}
fallback={
<box padding={1}>
<text fg={muted()}>No episode loaded.</text>
</box>
}
>
{(ep) => (
<box flexDirection="column" gap={1}>
<text fg={theme.text}>
<strong>{ep().title}</strong>
</text>
<text fg={muted()}>
{ep().description?.slice(0, 500) ??
"No description available."}
</text>
const formatTime = (seconds: number) => {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${String(s).padStart(2, "0")}`;
};
<RealtimeWaveform
visualizerConfig={(() => {
const viz = useAppStore().state().settings.visualizer;
return {
bars: viz.bars,
noiseReduction: viz.noiseReduction,
lowCutOff: viz.lowCutOff,
highCutOff: viz.highCutOff,
};
})()}
/>
</box>
)}
</Show>
return (
<box flexDirection="column" gap={1} width="100%">
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text}>
<strong>Now Playing</strong>
</text>
<text fg={theme.muted}>
{formatTime(audio.position())} / {formatTime(audio.duration())} (
{progressPercent()}%)
</text>
</box>
<PlaybackControls
isPlaying={audio.isPlaying()}
volume={audio.volume()}
speed={audio.speed()}
backendName={audio.backendName()}
hasAudioUrl={!!audio.currentEpisode()?.audioUrl}
onToggle={audio.togglePlayback}
onPrev={() => audio.seek(0)}
onNext={() => audio.seek(audio.currentEpisode()?.duration ?? 0)}
onSpeedChange={(s: number) => audio.setSpeed(s)}
onVolumeChange={(v: number) => audio.setVolume(v)}
/>
{audio.error() && <text fg={theme.error}>{audio.error()}</text>}
<box
border
borderColor={nav.activeDepth() == PlayerPaneType.PLAYER ? theme.accent : theme.border}
padding={1}
flexDirection="column"
gap={1}
>
<text fg={theme.text}>
<strong>{audio.currentEpisode()?.title}</strong>
</text>
<text fg={theme.muted}>{audio.currentEpisode()?.description}</text>
<RealtimeWaveform
visualizerConfig={(() => {
const viz = useAppStore().state().settings.visualizer;
return {
bars: viz.bars,
noiseReduction: viz.noiseReduction,
lowCutOff: viz.lowCutOff,
highCutOff: viz.highCutOff,
};
})()}
/>
</box>
<PlaybackControls
isPlaying={audio.isPlaying()}
volume={audio.volume()}
speed={audio.speed()}
backendName={audio.backendName()}
hasAudioUrl={!!audio.currentEpisode()?.audioUrl}
onToggle={audio.togglePlayback}
onPrev={() => audio.seek(0)}
onNext={() => audio.seek(audio.currentEpisode()?.duration ?? 0)} //TODO: get next chronological(if feed) or episode(if MyShows)
onSpeedChange={(s: number) => audio.setSpeed(s)}
onVolumeChange={(v: number) => audio.setVolume(v)}
/>
</box>
);
<box height={1} />
<text fg={muted()}>{"P play/pause N next B prev </ seek"}</text>
</box>
</scrollbox>
</box>
);
}