import { ReactiveStore } from "@luna/core"; import { LunaSettings, LunaSwitchSetting } from "@luna/ui"; import React from "react"; declare global { interface Window { applyColoramaLyrics?: () => void; } } // Define a typed onChange signature for the switch type SwitchChangeHandler = ( event: React.ChangeEvent | null, checked: boolean, ) => void; export type ColoramaMode = | "single" | "gradient-experimental" | "cover" | "cover-gradient"; export const settings = await ReactiveStore.getPluginStorage("ColoramaLyrics", { enabled: true, mode: "single" as ColoramaMode, // Store colors as RGB hex (#RRGGBB) and opacity separately (0-100) singleColor: "#FFFFFF", singleAlpha: 100, gradientStart: "#FFFFFF", gradientStartAlpha: 100, gradientEnd: "#AAFFFF", gradientEndAlpha: 100, gradientAngle: 0, customColors: [] as string[], excludeInactive: false, }); export const Settings = () => { // const [enabled, setEnabled] = React.useState(settings.enabled); const [mode, setMode] = React.useState(settings.mode); const [singleColor, setSingleColor] = React.useState(settings.singleColor); const [singleAlpha, setSingleAlpha] = React.useState( settings.singleAlpha ?? 100, ); const [gradientStart, setGradientStart] = React.useState( settings.gradientStart, ); const [gradientStartAlpha, setGradientStartAlpha] = React.useState( settings.gradientStartAlpha ?? 100, ); const [gradientEnd, setGradientEnd] = React.useState(settings.gradientEnd); const [gradientEndAlpha, setGradientEndAlpha] = React.useState( settings.gradientEndAlpha ?? 100, ); const [gradientAngle, setGradientAngle] = React.useState( settings.gradientAngle, ); const [customInput, setCustomInput] = React.useState(settings.singleColor); const [customColors, setCustomColors] = React.useState(settings.customColors); const [showPicker, setShowPicker] = React.useState(false); const [isAnimatingIn, setIsAnimatingIn] = React.useState(false); const [shouldRender, setShouldRender] = React.useState(false); const [excludeInactive, setExcludeInactive] = React.useState( settings.excludeInactive, ); const [activeEndpoint, setActiveEndpoint] = React.useState< "single" | "start" | "end" >("single"); const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType<{ title: string; desc?: string; checked: boolean; onChange: SwitchChangeHandler; }>; // Helper for HEX normalization const normalizeToRGB = ( hex: string, fallback: string = "#FFFFFF", ): string => { let v = hex.trim().toLowerCase(); if (!v.startsWith("#")) v = `#${v}`; // #rgb or #rgba -> expand if (/^#([0-9a-f]{3,4})$/.test(v)) { const m = v.slice(1); const r = m[0]; const g = m[1]; const b = m[2]; // ignore alpha if provided (#rgba) return `#${r}${r}${g}${g}${b}${b}`.toUpperCase(); } // #aarrggbb -> strip alpha if (/^#([0-9a-f]{8})$/.test(v)) { const rrggbb = v.slice(3); return `#${rrggbb}`.toUpperCase(); } // #rrggbb if (/^#([0-9a-f]{6})$/.test(v)) return v.toUpperCase(); return fallback; }; const colorPresets = [ "#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#FF8800", "#8800FF", "#0088FF", "#88FF00", "#FF0088", "#00FF88", "#444444", "#888888", "#CCCCCC", "#1DB954", "#E22134", "#1976D2", ]; const openPicker = (endpoint: "single" | "start" | "end" = "single") => { setActiveEndpoint(endpoint); setShowPicker(true); setShouldRender(true); setTimeout(() => setIsAnimatingIn(true), 10); }; const closePicker = () => { setIsAnimatingIn(false); setTimeout(() => { setShowPicker(false); setShouldRender(false); }, 200); }; const hexColorRegex = /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3,4})$/i; const applyCustomInputColor = (raw: string, updateInput: boolean): void => { const trimmed = raw.trim(); if (!hexColorRegex.test(trimmed)) return; if (mode === "single") { const next = normalizeToRGB(trimmed); settings.singleColor = next; setSingleColor(next); if (updateInput) setCustomInput(next); } else if (mode === "gradient-experimental") { const next = normalizeToRGB(trimmed); if (activeEndpoint === "end") { settings.gradientEnd = next; setGradientEnd(next); } else { settings.gradientStart = next; setGradientStart(next); } if (updateInput) setCustomInput(next); } requestApply(); }; const addCustomColor = () => { const trimmed = customInput.trim(); if ( hexColorRegex.test(trimmed) && !colorPresets.includes(trimmed) && !customColors.includes(normalizeToRGB(trimmed)) ) { const updated = [...customColors, normalizeToRGB(trimmed)]; setCustomColors(updated); settings.customColors = updated; } }; // const removeCustomColor = (color: string) => { // const updated = customColors.filter((c) => c !== color); // setCustomColors(updated); // settings.customColors = updated; // }; const allColors = [...colorPresets, ...customColors]; const requestApply = () => { window.applyColoramaLyrics?.(); }; return ( {/* Mode selection via dropdown (aligned right) */}
Mode
Choose how lyrics are colored
{/* Single color */}
Lyrics Color
Set lyrics color
{/* Gradient controls (open picker) */}
Gradient (Experimental)
Set colors & angle
{/* Cover gradient controls (open picker for angle) */}
Cover (Gradient) - Experimental
Set angle
{/* Modal for picking and managing colors (reused) */} {shouldRender && ( <> )} {mode !== "cover-gradient" && (
{allColors.map((color) => (
)} {mode !== "cover-gradient" && (
Custom Hex (#RRGGBB)
setCustomInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { applyCustomInputColor(customInput, true); addCustomColor(); } }} placeholder="#RRGGBB" style={{ flex: 1, padding: "8px 12px", borderRadius: 6, border: "1px solid rgba(255,255,255,0.2)", background: "rgba(255,255,255,0.1)", color: "#fff", fontSize: 14, fontFamily: "monospace", boxSizing: "border-box", }} />
)} {/* Sliders inside picker based on mode */} {mode === "single" && (
Alpha
{ const value = Number(e.target.value); settings.singleAlpha = value; setSingleAlpha(value); requestApply(); }} style={{ width: "100%" }} />
)} {mode === "gradient-experimental" && (
Start Alpha
{ const value = Number(e.target.value); settings.gradientStartAlpha = value; setGradientStartAlpha(value); requestApply(); }} style={{ width: "100%" }} />
End Alpha
{ const value = Number(e.target.value); settings.gradientEndAlpha = value; setGradientEndAlpha(value); requestApply(); }} style={{ width: "100%" }} />
Angle
{gradientAngle}°
{ const value = Number(e.target.value); settings.gradientAngle = value; setGradientAngle(value); requestApply(); }} style={{ width: "100%" }} />
)} {mode === "cover-gradient" && (
Angle
{gradientAngle}°
{ const value = Number(e.target.value); settings.gradientAngle = value; setGradientAngle(value); requestApply(); }} style={{ width: "100%" }} />
)}
)} | null, checked: boolean) => { settings.excludeInactive = checked; setExcludeInactive(checked); requestApply(); }} /> ); };