feat(player): toggleable waveform visualizer in settings, default on

This commit is contained in:
2026-08-11 14:15:48 -04:00
parent 8496922aaf
commit 8b7b38276e
9 changed files with 157 additions and 5 deletions

View File

@@ -16,6 +16,7 @@ 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";
@@ -28,7 +29,11 @@ export function PlayerPage() {
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
@@ -90,7 +95,9 @@ export function PlayerPage() {
<ProgressBar />
<RealtimeWaveform />
<Show when={vizEnabled()}>
<RealtimeWaveform />
</Show>
</box>
)}
</Show>

View File

@@ -64,7 +64,7 @@ const SECTIONS: SettingsSectionDef[] = [
{
id: 3,
label: "Visualizer",
description: "Audio visualizer: bars, sensitivity, cutoffs.",
description: "Audio visualizer: on/off, bars, sensitivity, cutoffs.",
icon: NF_ICONS.visualizer,
},
{

View File

@@ -11,6 +11,15 @@ export function useVisualizerItems(): SettingItem[] {
const viz = () => app.state().settings.visualizer;
return [
{
id: "enabled",
label: "Waveform",
kind: "toggle",
display: () => (viz().enabled ? "On" : "Off"),
help: () =>
`Realtime waveform visualizer in the player.\nType: toggle\nDefault: on\nCurrent: ${viz().enabled ? "on" : "off"}\nSpace/Enter to toggle.`,
toggle: () => app.updateVisualizer({ enabled: !viz().enabled }),
},
{
id: "bars",
label: "Bars",

View File

@@ -17,6 +17,7 @@ import {
} from "../utils/app-persistence";
const defaultVisualizerSettings: VisualizerSettings = {
enabled: true,
bars: 64,
sensitivity: 1,
noiseReduction: 0.77,

View File

@@ -250,9 +250,10 @@ function createVisualizerStore(): VisualizerStore {
audioPlaybackSignals.speed,
barCount,
focused,
() => useAppStore().state().settings.visualizer.enabled,
],
([playing, url, speed]) => {
if (!playing || !url) {
([playing, url, speed, , , enabled]) => {
if (!playing || !url || !enabled) {
stopVisualization();
return;
}
@@ -289,6 +290,7 @@ function createVisualizerStore(): VisualizerStore {
if (
audioPlaybackSignals.isPlaying() &&
audioPlaybackSignals.currentEpisode()?.audioUrl &&
useAppStore().state().settings.visualizer.enabled &&
frameTimer === null
) {
startVisualization(

View File

@@ -62,6 +62,8 @@ export type DesktopTheme = {
};
export type VisualizerSettings = {
/** Master on/off switch for the player's realtime waveform (default: on). */
enabled: boolean;
/** Number of frequency bars (8128, default: 64) */
bars: number;
/** Automatic sensitivity: 1 = enabled, 0 = disabled (default: 1) */

View File

@@ -21,6 +21,7 @@ import { DEFAULT_THEME } from "../constants/themes";
// --- Defaults ---
const defaultVisualizerSettings: VisualizerSettings = {
enabled: true,
bars: 32,
sensitivity: 1,
noiseReduction: 0.77,
@@ -64,7 +65,18 @@ export async function loadAppStateFromFile(): Promise<AppState> {
const cfg = await loadConfig();
if (!cfg || typeof cfg !== "object") return defaultState;
return {
settings: { ...defaultSettings, ...cfg.settings },
settings: {
...defaultSettings,
...cfg.settings,
// Visualizer is nested: a plain spread would let a config
// saved before a field was added (e.g. `enabled`) clobber
// the whole object and leave the new field undefined.
// Deep-merge so defaults backfill missing nested keys.
visualizer: {
...defaultVisualizerSettings,
...cfg.settings?.visualizer,
},
},
preferences: { ...defaultPreferences, ...cfg.preferences },
customTheme: { ...DEFAULT_THEME, ...cfg.customTheme },
};