import { ReactiveStore } from "@luna/core"; import { LunaSettings, LunaNumberSetting, LunaSwitchSetting, LunaSelectSetting, LunaSelectItem, } from "@luna/ui"; import React from "react"; import { VISUALIZER_LABELS, type VisualizerType, ALL_SLOT_KEYS, ZONE_SLOTS, ZONE_LABELS, POSITION_LABELS, type ZoneId, type PositionId, type SlotKey, MINI_SUPPORTED, } from "./visualizers/types"; export const settings = await ReactiveStore.getPluginStorage( "AudioVisualizer", { navLeft1: "none" as VisualizerType, navLeft2: "none" as VisualizerType, navLeft3: "none" as VisualizerType, navRight1: "spectrum-bars" as VisualizerType, navRight2: "none" as VisualizerType, navRight3: "none" as VisualizerType, npLeft1: "none" as VisualizerType, npLeft2: "none" as VisualizerType, npLeft3: "none" as VisualizerType, npRight1: "oscilloscope" as VisualizerType, npRight2: "none" as VisualizerType, npRight3: "none" as VisualizerType, pbLeft1: "none" as VisualizerType, pbLeft2: "none" as VisualizerType, pbLeft3: "none" as VisualizerType, pbRight1: "none" as VisualizerType, pbRight2: "none" as VisualizerType, pbRight3: "none" as VisualizerType, barColor: "#ff69b4", barCount: 64, fftSize: 2048, reactivity: 30, gain: 1.5, barRounding: true, lineThickness: 2.0, fillOpacity: 0.6, opacityFalloff: 0.5, lissajous: false, scrollingOscilloscope: false, groupedSlots: false, transparentContainers: false, miniSlots: [] as string[], customColors: [] as string[], }, ); const VIZ_TYPES: VisualizerType[] = [ "none", "spectrum-bars", "spectrum-line", "oscilloscope", "vectorscope", "loudness-meter", ]; const getSlot = (key: SlotKey): VisualizerType => (settings as unknown as Record)[key] ?? "none"; const setSlot = (key: SlotKey, value: VisualizerType): void => { (settings as unknown as Record)[key] = value; }; export const Settings = () => { const [barColor, setBarColor] = React.useState(settings.barColor); const [barCount, setBarCount] = React.useState(settings.barCount); const [fftSize, setFftSize] = React.useState(settings.fftSize); const [reactivity, setReactivity] = React.useState(settings.reactivity); const [gain, setGain] = React.useState(settings.gain); const [barRounding, setBarRounding] = React.useState(settings.barRounding); const [lineThickness, setLineThickness] = React.useState(settings.lineThickness); const [fillOpacity, setFillOpacity] = React.useState(settings.fillOpacity); const [lissajous, setLissajous] = React.useState(settings.lissajous); const [scrollingOscilloscope, setScrollingOscilloscope] = React.useState(settings.scrollingOscilloscope); const [groupedSlots, setGroupedSlots] = React.useState(settings.groupedSlots); const [transparentContainers, setTransparentContainers] = React.useState( settings.transparentContainers, ); const [showColorPicker, setShowColorPicker] = React.useState(false); const [isColorAnimIn, setIsColorAnimIn] = React.useState(false); const [shouldRenderColor, setShouldRenderColor] = React.useState(false); const [customInput, setCustomInput] = React.useState(settings.barColor); const [customColors, setCustomColors] = React.useState(settings.customColors); const [hoveredColorIndex, setHoveredColorIndex] = React.useState(null); const [showSlotConfig, setShowSlotConfig] = React.useState(false); const [isSlotAnimIn, setIsSlotAnimIn] = React.useState(false); const [shouldRenderSlot, setShouldRenderSlot] = React.useState(false); const [activeZone, setActiveZone] = React.useState("nowPlaying"); const [slots, setSlots] = React.useState>(() => { const vals = {} as Record; for (const key of ALL_SLOT_KEYS) vals[key] = getSlot(key); return vals; }); const [miniSlots, setMiniSlots] = React.useState>(new Set(settings.miniSlots)); const closeColorPicker = () => { setIsColorAnimIn(false); setTimeout(() => { setShowColorPicker(false); setShouldRenderColor(false); }, 200); }; const openColorPicker = () => { setShowColorPicker(true); setShouldRenderColor(true); setTimeout(() => setIsColorAnimIn(true), 10); }; const closeSlotConfig = () => { setIsSlotAnimIn(false); setTimeout(() => { setShowSlotConfig(false); setShouldRenderSlot(false); }, 200); }; const openSlotConfig = () => { setShowSlotConfig(true); setShouldRenderSlot(true); setTimeout(() => setIsSlotAnimIn(true), 10); }; React.useEffect(() => { if (showColorPicker) { setShouldRenderColor(true); setTimeout(() => setIsColorAnimIn(true), 10); } }, [showColorPicker]); React.useEffect(() => { if (showSlotConfig) { setShouldRenderSlot(true); setTimeout(() => setIsSlotAnimIn(true), 10); } }, [showSlotConfig]); const colorPresets = [ "#ff69b4", "#ff1493", "#e91e8a", "#c71585", "#ff006e", "#ff4da6", "#ff85c8", "#ffb3d9", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", "#ff8800", "#8800ff", "#0088ff", "#1db954", "#444444", ]; const updateColor = (color: string) => { setBarColor(color); setCustomInput(color); settings.barColor = color; }; const addCustomColor = () => { if (customInput) { const trimmed = customInput.trim().toLowerCase(); const hexRe = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i; if (hexRe.test(trimmed) && !colorPresets.includes(trimmed) && !customColors.includes(trimmed)) { const nc = [...customColors, trimmed]; setCustomColors(nc); settings.customColors = nc; } } }; const removeCustomColor = (c: string) => { const nc = customColors.filter(x => x !== c); setCustomColors(nc); settings.customColors = nc; if (barColor === c) updateColor("#ff69b4"); }; const allColors = [...colorPresets, ...customColors]; const updateSlot = (key: SlotKey, value: VisualizerType) => { setSlots(prev => ({ ...prev, [key]: value })); setSlot(key, value); if (!MINI_SUPPORTED.has(value)) { setMiniSlots(prev => { const next = new Set(prev); if (next.delete(key)) settings.miniSlots = [...next]; return next; }); } }; const toggleMini = (key: SlotKey) => { setMiniSlots(prev => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); settings.miniSlots = [...next]; return next; }); }; type BaseSwitchProps = React.ComponentProps; type AnySwitchProps = Omit & { onChange: (_: unknown, checked: boolean) => void; checked: boolean; }; const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType; const hasBars = ALL_SLOT_KEYS.some(key => slots[key] === "spectrum-bars"); const zones: ZoneId[] = ["nowPlaying", "topNav", "playerBar"]; const zonePositions = (zone: ZoneId) => Object.keys(ZONE_SLOTS[zone]) as PositionId[]; const backdropStyle = (animIn: boolean): React.CSSProperties => ({ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, background: "rgba(0,0,0,0.6)", zIndex: 1000, opacity: animIn ? 1 : 0, transition: "opacity 0.2s ease", border: "none", padding: 0, cursor: "default", width: "100%", }); const panelBaseStyle = (animIn: boolean): React.CSSProperties => ({ position: "fixed", top: "50%", left: "50%", background: "rgba(20,20,20,0.98)", backdropFilter: "blur(20px)", WebkitBackdropFilter: "blur(20px)", border: "1px solid rgba(255,255,255,0.15)", borderRadius: "16px", padding: "20px", maxHeight: "90vh", overflowY: "auto", zIndex: 1001, boxShadow: "0 20px 40px rgba(0,0,0,0.7)", opacity: animIn ? 1 : 0, transform: animIn ? "translate(-50%, -50%) scale(1)" : "translate(-50%, -50%) scale(0.9)", transition: "all 0.2s ease", }); const selectStyle: React.CSSProperties = { width: "100%", padding: "6px 8px", borderRadius: "6px", border: "1px solid rgba(255,255,255,0.2)", background: "rgba(255,255,255,0.08)", color: "#fff", fontSize: "12px", cursor: "pointer", outline: "none", }; const optionStyle: React.CSSProperties = { background: "#1a1a1a", color: "#fff", }; return ( {/* Color & Layout */}
Color & Layout
Visualizer color and slot placement
{ setGroupedSlots(checked); settings.groupedSlots = checked; }} /> { setTransparentContainers(checked); settings.transparentContainers = checked; }} /> {/* Color picker modal */} {shouldRenderColor && ( <> )} ); })}
Add Custom Color
setCustomInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { updateColor(customInput); addCustomColor(); } }} placeholder="#ff69b4" style={{ flex: 1, padding: "8px 12px", borderRadius: "6px", border: "1px solid rgba(255,255,255,0.2)", background: "rgba(255,255,255,0.1)", color: "#fff", fontSize: "14px", fontFamily: "monospace", boxSizing: "border-box", }} />
)} {/* Slot configuration modal */} {shouldRenderSlot && ( <> ))} {/* Slot grid */}
{zonePositions(activeZone).map(pos => { const slotKeys = ZONE_SLOTS[activeZone][pos]; if (!slotKeys) return null; return (
{POSITION_LABELS[pos]}
{slotKeys.map((key, i) => (
{MINI_SUPPORTED.has(slots[key]) && ( )}
))}
); })}
)} { setReactivity(v); settings.reactivity = v; }} /> { setGain(v); settings.gain = v; }} /> ) => { const v = Number(e.target.value); setFftSize(v); settings.fftSize = v; }} > {[256, 512, 1024, 2048, 4096, 8192, 16384].map(s => ( {s} ))} { setBarCount(v); settings.barCount = v; }} /> {hasBars && ( { setBarRounding(checked); settings.barRounding = checked; }} /> )} { setLineThickness(v); settings.lineThickness = v; }} /> { setFillOpacity(v); settings.fillOpacity = v; }} /> { setScrollingOscilloscope(checked); settings.scrollingOscilloscope = checked; }} /> { setLissajous(checked); settings.lissajous = checked; }} />
); };