diff --git a/plugins/colorama-lyrics-luna/src/Settings.tsx b/plugins/colorama-lyrics-luna/src/Settings.tsx index 330b71f..0758c26 100644 --- a/plugins/colorama-lyrics-luna/src/Settings.tsx +++ b/plugins/colorama-lyrics-luna/src/Settings.tsx @@ -2,16 +2,20 @@ import { ReactiveStore } from "@luna/core"; import { LunaSettings, LunaNumberSetting, LunaSwitchSetting, LunaTextSetting } from "@luna/ui"; import React from "react"; -export type ColoramaMode = "single" | "gradient" | "auto-single" | "auto-gradient"; +export type ColoramaMode = "single" | "gradient" | "auto-single" | "auto-gradient" | "rainbow"; export const settings = await ReactiveStore.getPluginStorage("ColoramaLyrics", { enabled: true, mode: "single" as ColoramaMode, - // Store colors as ARGB hex (#AARRGGBB) - singleColor: "#FFFFFFFF", - gradientStart: "#FFFFFFFF", - gradientEnd: "#88AAFFFF", + // 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, + rainbowSpeed: 8, customColors: [] as string[], excludeInactive: false }); @@ -20,59 +24,67 @@ 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 [rainbowSpeed, setRainbowSpeed] = React.useState(settings.rainbowSpeed ?? 8); 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; - // Helpers for ARGB <-> components + // Helpers for HEX parsing and alpha extraction const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n)); - const normalizeToARGB = (hex: string, fallback: string = "#FFFFFFFF"): string => { + 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 a = m.length === 4 ? m[3] : 'f'; const r = m[0]; const g = m[1]; const b = m[2]; - v = `#${a}${r}${g}${b}${r}${g}${b}${a}`; // temporary, will reformat below + // 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)) { - const m = v.slice(1); - const rrggbb = m; - const aa = 'ff'; - return `#${aa}${rrggbb}`.toUpperCase(); - } - // #aarrggbb - if (/^#([0-9a-f]{8})$/.test(v)) return v.toUpperCase(); + if (/^#([0-9a-f]{6})$/.test(v)) return v.toUpperCase(); return fallback; }; - const setAlphaOnARGB = (argb: string, alpha01: number): string => { - const a = clamp(Math.round(alpha01 * 255), 0, 255).toString(16).padStart(2, '0'); - const body = argb.replace('#', '').slice(2); - return (`#${a}${body}`).toUpperCase(); - }; - const getAlpha01 = (argb: string): number => { - const v = normalizeToARGB(argb); - const a = parseInt(v.slice(1, 3), 16); - return clamp(a / 255, 0, 1); + const extractAlphaPercent = (hex: string, fallbackPercent: number = 100): number => { + let v = hex.trim().toLowerCase(); + if (!v.startsWith('#')) v = `#${v}`; + if (/^#([0-9a-f]{4})$/.test(v)) { + const a = v[4]; + return Math.round((parseInt(a + a, 16) / 255) * 100); + } + if (/^#([0-9a-f]{8})$/.test(v)) { + const a = v.slice(1, 3); + return Math.round((parseInt(a, 16) / 255) * 100); + } + return fallbackPercent; }; const colorPresets = [ - "#FFFFFFFF", "#FF0000FF", "#00FF00FF", "#0000FFFF", "#FFFF00FF", "#FF00FFFF", "#00FFFFFF", - "#FF8800FF", "#8800FFFF", "#0088FFFF", "#88FF00FF", "#FF0088FF", "#00FF88FF", - "#444444FF", "#888888FF", "#CCCCCCFF", "#1DB954FF", "#E22134FF", "#1976D2FF" + "#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", + "#FF8800", "#8800FF", "#0088FF", "#88FF00", "#FF0088", "#00FF88", + "#444444", "#888888", "#CCCCCC", "#1DB954", "#E22134", "#1976D2" ]; - const openPicker = () => { + const openPicker = (endpoint: 'single' | 'start' | 'end' = 'single') => { + setActiveEndpoint(endpoint); setShowPicker(true); setShouldRender(true); setTimeout(() => setIsAnimatingIn(true), 10); @@ -85,16 +97,16 @@ export const Settings = () => { }, 200); }; - const argbColorRegex = /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3,4})$/i; + const hexColorRegex = /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3,4})$/i; const addCustomColor = () => { const trimmed = customInput.trim(); if ( - argbColorRegex.test(trimmed) && + hexColorRegex.test(trimmed) && !colorPresets.includes(trimmed) && - !customColors.includes(normalizeToARGB(trimmed)) + !customColors.includes(normalizeToRGB(trimmed)) ) { - const updated = [...customColors, normalizeToARGB(trimmed)]; + const updated = [...customColors, normalizeToRGB(trimmed)]; setCustomColors(updated); settings.customColors = updated; } @@ -115,12 +127,10 @@ export const Settings = () => { return ( - {/* Mode selection via dropdown */} -
-
-
Mode
-
Choose how lyrics are colored
-
+ {/* Mode selection via dropdown (aligned right) */} +
+
Mode
+
Choose how lyrics are colored
@@ -148,101 +161,71 @@ export const Settings = () => {
Lyrics Color
-
Solid color (HEX/ARGB HEX)
+
Solid color (configure inside picker)
-
- { - const next = setAlphaOnARGB(normalizeToARGB(singleColor), value / 100); - setSingleColor((settings.singleColor = next)); - if (customInput) setCustomInput(next); - requestApply(); - }} - /> -
+ - {/* Gradient controls */} + {/* Gradient controls (triggers only) */}
Gradient
-
Pick start/end and angle
+
Pick start/end and angle (inside picker)
- { - const next = setAlphaOnARGB(normalizeToARGB(gradientStart), value / 100); - setGradientStart((settings.gradientStart = next)); - requestApply(); - }} - /> - { - const next = setAlphaOnARGB(normalizeToARGB(gradientEnd), value / 100); - setGradientEnd((settings.gradientEnd = next)); - requestApply(); - }} - /> - { - setGradientAngle((settings.gradientAngle = value)); - requestApply(); - }} - />
+ {/* Auto gradient controls (open picker for angle) */} +
+
+
Auto Gradient
+
Configure angle inside the picker
+
+ +
+ + {/* Rainbow controls removed: mode exists but has no UI */} + {/* Modal for picking and managing colors (reused) */} {shouldRender && ( <> @@ -281,112 +264,254 @@ export const Settings = () => { transition: "all 0.2s ease" }} > -
Choose Color (ARGB HEX)
-
- {allColors.map((color, index) => ( - + +
+ )} + {mode !== 'auto-gradient' && ( +
+ {allColors.map((color, index) => ( +
+ )} + {mode !== 'auto-gradient' && ( +
+
Custom Hex (#RRGGBB)
+
+ setCustomInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + const trimmed = customInput.trim(); + if (hexColorRegex.test(trimmed)) { + if (mode === "single") { + const next = normalizeToRGB(trimmed); + setSingleColor((settings.singleColor = next)); + setCustomInput(next); + } else if (mode === "gradient") { + const norm = normalizeToRGB(trimmed); + if (activeEndpoint === 'end') { + setGradientEnd((settings.gradientEnd = norm)); + } else { + setGradientStart((settings.gradientStart = norm)); + } + setCustomInput(norm); + } + requestApply(); + } + 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" + }} + /> + + }} + style={{ + width: 32, + height: 32, + borderRadius: 6, + border: "1px solid rgba(255,255,255,0.3)", + background: "rgba(255,255,255,0.15)", + color: "#fff", + cursor: "pointer", + fontSize: 16, + display: "flex", + alignItems: "center", + justifyContent: "center", + transition: "all 0.2s ease" + }} + > + + + +
-
+ )} + {/* Sliders inside picker based on mode */} + {mode === 'single' && ( +
+
Alpha
+ { + const value = Number(e.target.value); + setSingleAlpha((settings.singleAlpha = value)); + requestApply(); + }} + style={{ width: '100%' }} + /> +
+ )} + + {mode === 'gradient' && ( +
+
+
+
+
Start Alpha
+
+ { + const value = Number(e.target.value); + setGradientStartAlpha((settings.gradientStartAlpha = value)); + requestApply(); + }} + style={{ width: '100%' }} + /> +
+
+
+
+
End Alpha
+
+ { + const value = Number(e.target.value); + setGradientEndAlpha((settings.gradientEndAlpha = value)); + requestApply(); + }} + style={{ width: '100%' }} + /> +
+
+
+
Angle
+
{gradientAngle}°
+
+ { + const value = Number(e.target.value); + setGradientAngle((settings.gradientAngle = value)); + requestApply(); + }} + style={{ width: '100%' }} + /> +
+
+ )} + + {mode === 'auto-gradient' && ( +
+
+
Angle
+
{gradientAngle}°
+
+ { + const value = Number(e.target.value); + setGradientAngle((settings.gradientAngle = value)); + requestApply(); + }} + style={{ width: '100%' }} + /> +
+ )} +
)} - hookRadiantUpdates(), 0); +// Observe active lyric span changes and restart rainbow animation to avoid freezes +// Rainbow mode disabled: no lyrics observer needed + diff --git a/plugins/colorama-lyrics-luna/src/styles.css b/plugins/colorama-lyrics-luna/src/styles.css index 999bf25..13a023b 100644 --- a/plugins/colorama-lyrics-luna/src/styles.css +++ b/plugins/colorama-lyrics-luna/src/styles.css @@ -32,6 +32,9 @@ -webkit-text-fill-color: transparent !important; } +/* Ensure active line keeps rainbow in only-active mode */ +/* Only-active: apply container class only on the active line via JS */ + /* Slight emphasis on current line (uniform to single mode) */ .colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"], .colorama-gradient [class^="_lyricsContainer"] > div > div > span[data-current="true"] { @@ -69,12 +72,24 @@ /* Only color active line mode */ body.colorama-only-active.colorama-single [class*="_lyricsText"] > div > span:not([data-current="true"]), body.colorama-only-active.colorama-gradient [class*="_lyricsText"] > div > span:not([data-current="true"]) { - /* Reset non-active lines to default */ - color: inherit !important; + /* Match Radiant inactive styling */ + color: rgba(128, 128, 128, 0.4) !important; background: none !important; -webkit-background-clip: initial !important; background-clip: initial !important; -webkit-text-fill-color: initial !important; + text-shadow: initial !important; +} + +/* In only-active mode, keep TIDAL defaults even on hover for inactive lines */ +body.colorama-only-active.colorama-single [class*="_lyricsText"] > div > span:not([data-current="true"]):hover, +body.colorama-only-active.colorama-gradient [class*="_lyricsText"] > div > span:not([data-current="true"]):hover { + color: lightgray !important; + background: none !important; + -webkit-background-clip: initial !important; + background-clip: initial !important; + -webkit-text-fill-color: initial !important; + text-shadow: initial !important; }