Added Colorama-Lyrics

This commit is contained in:
2025-08-12 23:43:51 +10:00
parent cf9bbb62e6
commit 1fda054d2a
7 changed files with 725 additions and 3 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@meowarex/colorama-lyrics",
"description": "Customize lyrics colors: single, gradient & auto from cover art",
"author": {
"name": "meowarex",
"url": "https://github.com/meowarex",
"avatarUrl": "https://avatars.githubusercontent.com/u/90243579"
},
"main": "./src/index.ts",
"type": "module"
}
@@ -0,0 +1,421 @@
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 const settings = await ReactiveStore.getPluginStorage("ColoramaLyrics", {
enabled: true,
mode: "single" as ColoramaMode,
// Store colors as ARGB hex (#AARRGGBB)
singleColor: "#FFFFFFFF",
gradientStart: "#FFFFFFFF",
gradientEnd: "#88AAFFFF",
gradientAngle: 0,
customColors: [] as string[],
excludeInactive: false
});
export const Settings = () => {
const [enabled, setEnabled] = React.useState(settings.enabled);
const [mode, setMode] = React.useState<ColoramaMode>(settings.mode);
const [singleColor, setSingleColor] = React.useState(settings.singleColor);
const [gradientStart, setGradientStart] = React.useState(settings.gradientStart);
const [gradientEnd, setGradientEnd] = React.useState(settings.gradientEnd);
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);
// Helpers for ARGB <-> components
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n));
const normalizeToARGB = (hex: string, fallback: string = "#FFFFFFFF"): 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
}
// #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();
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 colorPresets = [
"#FFFFFFFF", "#FF0000FF", "#00FF00FF", "#0000FFFF", "#FFFF00FF", "#FF00FFFF", "#00FFFFFF",
"#FF8800FF", "#8800FFFF", "#0088FFFF", "#88FF00FF", "#FF0088FF", "#00FF88FF",
"#444444FF", "#888888FF", "#CCCCCCFF", "#1DB954FF", "#E22134FF", "#1976D2FF"
];
const openPicker = () => {
setShowPicker(true);
setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10);
};
const closePicker = () => {
setIsAnimatingIn(false);
setTimeout(() => {
setShowPicker(false);
setShouldRender(false);
}, 200);
};
const argbColorRegex = /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3,4})$/i;
const addCustomColor = () => {
const trimmed = customInput.trim();
if (
argbColorRegex.test(trimmed) &&
!colorPresets.includes(trimmed) &&
!customColors.includes(normalizeToARGB(trimmed))
) {
const updated = [...customColors, normalizeToARGB(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 as any).applyColoramaLyrics?.();
};
return (
<LunaSettings>
{/* Mode selection via dropdown */}
<div style={{ padding: "8px 0", display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
<div style={{ minWidth: 160 }}>
<div style={{ fontWeight: "normal", fontSize: "1.075rem", marginBottom: 4 }}>Mode</div>
<div style={{ opacity: 0.7, fontSize: 14 }}>Choose how lyrics are colored</div>
</div>
<select
value={mode}
onChange={(e) => {
const next = e.target.value as ColoramaMode;
setMode((settings.mode = next));
requestApply();
}}
style={{
padding: "6px 10px",
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.05)",
color: "#fff",
cursor: "pointer"
}}
>
<option value="single">Single</option>
<option value="gradient">Gradient</option>
<option value="auto-single">Auto (Cover)</option>
<option value="auto-gradient">Auto Gradient</option>
</select>
</div>
{/* Single color */}
<div style={{ padding: "8px 0", display: mode === "single" ? "flex" : "none", justifyContent: "space-between", alignItems: "center" }}>
<div>
<div style={{ fontWeight: "normal", fontSize: "1.075rem", marginBottom: 4 }}>Lyrics Color</div>
<div style={{ opacity: 0.7, fontSize: 14 }}>Solid color (HEX/ARGB HEX)</div>
</div>
<div style={{ display: "flex", gap: 8, alignItems: "center", position: "relative" }}>
<button
onClick={() => (showPicker ? closePicker() : openPicker())}
style={{
width: 32,
height: 32,
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: 6,
cursor: "pointer",
background: normalizeToARGB(singleColor)
}}
/>
</div>
</div>
<div style={{ display: mode === "single" ? 'block' : 'none' }}>
<LunaNumberSetting
title="Single Alpha"
desc="Opacity of the single color (0-100%)"
min={0}
max={100}
step={1}
value={Math.round(getAlpha01(singleColor) * 100)}
onNumber={(value: number) => {
const next = setAlphaOnARGB(normalizeToARGB(singleColor), value / 100);
setSingleColor((settings.singleColor = next));
if (customInput) setCustomInput(next);
requestApply();
}}
/>
</div>
{/* Gradient controls */}
<div style={{ padding: "8px 0", display: mode === "gradient" ? "block" : "none" }}>
<div style={{ fontWeight: "normal", fontSize: "1.075rem", marginBottom: 4 }}>Gradient</div>
<div style={{ opacity: 0.7, fontSize: 14, marginBottom: 8 }}>Pick start/end and angle</div>
<div style={{ display: "flex", gap: 12, alignItems: "center" }}>
<button
onClick={() => {
setCustomInput(gradientStart);
openPicker();
}}
title="Start Color"
style={{ width: 32, height: 32, border: "1px solid rgba(255,255,255,0.15)", borderRadius: 6, background: normalizeToARGB(gradientStart) }}
/>
<button
onClick={() => {
setCustomInput(gradientEnd);
openPicker();
}}
title="End Color"
style={{ width: 32, height: 32, border: "1px solid rgba(255,255,255,0.15)", borderRadius: 6, background: normalizeToARGB(gradientEnd) }}
/>
</div>
<LunaNumberSetting
title="Start Alpha"
desc="Opacity of the gradient start (0-100%)"
min={0}
max={100}
step={1}
value={Math.round(getAlpha01(gradientStart) * 100)}
onNumber={(value: number) => {
const next = setAlphaOnARGB(normalizeToARGB(gradientStart), value / 100);
setGradientStart((settings.gradientStart = next));
requestApply();
}}
/>
<LunaNumberSetting
title="End Alpha"
desc="Opacity of the gradient end (0-100%)"
min={0}
max={100}
step={1}
value={Math.round(getAlpha01(gradientEnd) * 100)}
onNumber={(value: number) => {
const next = setAlphaOnARGB(normalizeToARGB(gradientEnd), value / 100);
setGradientEnd((settings.gradientEnd = next));
requestApply();
}}
/>
<LunaNumberSetting
title="Gradient Angle"
desc="Angle in degrees (0-360)"
min={0}
max={360}
step={1}
value={gradientAngle}
onNumber={(value: number) => {
setGradientAngle((settings.gradientAngle = value));
requestApply();
}}
/>
</div>
{/* Modal for picking and managing colors (reused) */}
{shouldRender && (
<>
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0,0,0,0.6)",
zIndex: 1000,
opacity: isAnimatingIn ? 1 : 0,
transition: "opacity 0.2s ease"
}}
onClick={closePicker}
/>
<div
style={{
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: 16,
padding: 20,
minWidth: 320,
maxWidth: "90vw",
maxHeight: "90vh",
zIndex: 1001,
boxShadow: "0 20px 40px rgba(0,0,0,0.7)",
opacity: isAnimatingIn ? 1 : 0,
transform: isAnimatingIn ? "translate(-50%, -50%) scale(1)" : "translate(-50%, -50%) scale(0.9)",
transition: "all 0.2s ease"
}}
>
<div style={{ marginBottom: 12, color: "#fff", fontWeight: "bold", fontSize: 14 }}>Choose Color (ARGB HEX)</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: 8, marginBottom: 16 }}>
{allColors.map((color, index) => (
<button
key={index}
onClick={() => {
if (mode === "single") {
const next = normalizeToARGB(color);
setSingleColor((settings.singleColor = next));
} else if (mode === "gradient") {
// Toggle which endpoint to update based on last edited input
if (customInput.toLowerCase() === gradientEnd.toLowerCase()) {
setGradientEnd((settings.gradientEnd = normalizeToARGB(color)));
} else {
setGradientStart((settings.gradientStart = normalizeToARGB(color)));
}
}
setCustomInput(normalizeToARGB(color));
requestApply();
closePicker();
}}
style={{
width: 32,
height: 32,
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.2)",
background: normalizeToARGB(color),
cursor: "pointer"
}}
/>
))}
</div>
<div style={{ marginBottom: 12 }}>
<div style={{ color: "rgba(255,255,255,0.7)", fontSize: 12, marginBottom: 6 }}>Custom ARGB Hex (#AARRGGBB)</div>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<input
type="text"
value={customInput}
onChange={(e) => setCustomInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
const trimmed = customInput.trim();
if (argbColorRegex.test(trimmed)) {
if (mode === "single") {
setSingleColor((settings.singleColor = normalizeToARGB(trimmed)));
} else if (mode === "gradient") {
if (customInput.toLowerCase() === gradientEnd.toLowerCase()) {
setGradientEnd((settings.gradientEnd = normalizeToARGB(trimmed)));
} else {
setGradientStart((settings.gradientStart = normalizeToARGB(trimmed)));
}
}
requestApply();
}
addCustomColor();
}
}}
placeholder="#AARRGGBB"
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"
}}
/>
<button
onClick={() => {
const trimmed = customInput.trim();
if (argbColorRegex.test(trimmed)) {
if (mode === "single") {
setSingleColor((settings.singleColor = normalizeToARGB(trimmed)));
} else if (mode === "gradient") {
if (customInput.toLowerCase() === gradientEnd.toLowerCase()) {
setGradientEnd((settings.gradientEnd = normalizeToARGB(trimmed)));
} else {
setGradientStart((settings.gradientStart = normalizeToARGB(trimmed)));
}
}
requestApply();
}
addCustomColor();
}}
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"
}}
>
+
</button>
</div>
</div>
<button
onClick={closePicker}
style={{
width: "100%",
padding: 8,
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.1)",
color: "#fff",
cursor: "pointer",
fontSize: 12
}}
>
Done
</button>
</div>
</>
)}
<LunaSwitchSetting
title="Exclude Inactive | Experimental"
desc="Apply color/gradient only to the currently active lyric line"
checked={excludeInactive}
onChange={(_: unknown, checked: boolean) => {
setExcludeInactive((settings.excludeInactive = checked));
requestApply();
}}
/>
</LunaSettings>
);
};
+176
View File
@@ -0,0 +1,176 @@
import { LunaUnload, Tracer } from "@luna/core";
import { StyleTag, observe, observePromise, PlayState } from "@luna/lib";
import { settings, Settings } from "./Settings";
import styles from "file://styles.css?minify";
export const { trace } = Tracer("[Colorama Lyrics]");
export { Settings };
export const unloads = new Set<LunaUnload>();
const styleTag = new StyleTag("ColoramaLyrics", unloads, styles);
// Simple dominant color extraction from current cover art
async function getCoverArtElement(): Promise<HTMLImageElement | null> {
const img = document.querySelector('figure[class*="_albumImage"] > div > div > div > img') as HTMLImageElement | null;
if (img) return img;
const video = document.querySelector('figure[class*="_albumImage"] > div > div > div > video') as HTMLVideoElement | null;
if (video) {
const poster = video.getAttribute("poster");
if (!poster) return null;
const tempImg = new Image();
tempImg.crossOrigin = "anonymous";
tempImg.src = poster;
await new Promise<void>((resolve) => {
tempImg.onload = () => resolve();
tempImg.onerror = () => resolve();
});
return tempImg as unknown as HTMLImageElement;
}
return null;
}
function getDominantColorsFromImage(img: HTMLImageElement, count: number = 2): string[] {
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return ["#ffffff", "#88aaff"]; // fallback
const w = 64;
const h = 64;
canvas.width = w;
canvas.height = h;
ctx.drawImage(img, 0, 0, w, h);
const data = ctx.getImageData(0, 0, w, h).data;
// Simple k-means-ish binning into 16 buckets per channel
const buckets = new Map<string, number>();
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const key = `${Math.round(r/16)},${Math.round(g/16)},${Math.round(b/16)}`;
buckets.set(key, (buckets.get(key) ?? 0) + 1);
}
const sorted = [...buckets.entries()].sort((a, b) => b[1] - a[1]);
const picked = sorted.slice(0, Math.max(1, count)).map(([key]) => {
const [r, g, b] = key.split(',').map(v => parseInt(v, 10) * 16);
return `#${[r, g, b].map(v => Math.max(0, Math.min(255, v)).toString(16).padStart(2, '0')).join('')}`;
});
return picked;
} catch {
return ["#ffffff", "#88aaff"]; // fallback
}
}
function applySingleColor(color: string) {
document.documentElement.style.setProperty('--cl-lyrics-color', color);
document.documentElement.style.setProperty('--cl-glow1', color);
document.documentElement.style.setProperty('--cl-glow2', color);
document.documentElement.style.removeProperty('--cl-grad-start');
document.documentElement.style.removeProperty('--cl-grad-end');
document.documentElement.style.removeProperty('--cl-grad-angle');
document.body.classList.remove('colorama-gradient');
document.body.classList.add('colorama-single');
}
function applyGradient(start: string, end: string, angle: number) {
document.documentElement.style.setProperty('--cl-grad-start', start);
document.documentElement.style.setProperty('--cl-grad-end', end);
document.documentElement.style.setProperty('--cl-grad-angle', `${angle}deg`);
document.documentElement.style.setProperty('--cl-glow1', start);
document.documentElement.style.setProperty('--cl-glow2', end);
document.body.classList.remove('colorama-single');
document.body.classList.add('colorama-gradient');
}
async function applyAutoColors(gradient: boolean) {
const img = await getCoverArtElement();
if (!img) return;
const colors = getDominantColorsFromImage(img, gradient ? 2 : 1);
if (gradient) {
const start = colors[0] ?? settings.gradientStart;
const end = colors[1] ?? settings.gradientEnd;
applyGradient(start, end, settings.gradientAngle);
} else {
const color = colors[0] ?? settings.singleColor;
applySingleColor(color);
}
}
function applyColoramaLyrics(): void {
if (!settings.enabled) {
document.body.classList.remove('colorama-single', 'colorama-gradient');
return;
}
// Toggle only-active-line mode class
if (settings.onlyActiveLine) {
document.body.classList.add('colorama-only-active');
} else {
document.body.classList.remove('colorama-only-active');
}
switch (settings.mode) {
case "single":
applySingleColor(settings.singleColor);
break;
case "gradient":
applyGradient(settings.gradientStart, settings.gradientEnd, settings.gradientAngle);
break;
case "auto-single":
applyAutoColors(false);
break;
case "auto-gradient":
applyAutoColors(true);
break;
}
}
(window as any).applyColoramaLyrics = applyColoramaLyrics;
// Re-apply on track changes (for auto modes)
function observeTrackChanges(): void {
let lastTrackId: string | null = null;
const check = () => {
const currentTrackId = PlayState.playbackContext?.actualProductId;
if (currentTrackId && currentTrackId !== lastTrackId) {
lastTrackId = currentTrackId;
if (settings.mode.startsWith("auto")) {
setTimeout(() => applyColoramaLyrics(), 200);
}
}
};
const interval = setInterval(check, 500);
unloads.add(() => clearInterval(interval));
check();
}
// Initial apply and observers
setTimeout(() => applyColoramaLyrics(), 200);
observeTrackChanges();
// Ensure compatibility: re-apply after Radiant updates its styles/backgrounds
function hookRadiantUpdates(): void {
const w = window as any;
const wrap = (name: string) => {
const fn = w[name];
if (typeof fn === 'function' && !fn.__coloramaPatched) {
const orig = fn.bind(w);
const patched = (...args: unknown[]) => {
const result = orig(...args);
try { applyColoramaLyrics(); } catch {}
return result;
};
(patched as any).__coloramaPatched = true;
w[name] = patched;
}
};
wrap('updateRadiantLyricsStyles');
wrap('updateRadiantLyricsNowPlayingBackground');
wrap('updateRadiantLyricsGlobalBackground');
wrap('updateRadiantLyricsTextGlow');
}
setTimeout(() => hookRadiantUpdates(), 0);
@@ -0,0 +1,80 @@
/* Variables used by Colorama Lyrics */
:root {
--cl-lyrics-color: #ffffff;
--cl-grad-start: #ffffff;
--cl-grad-end: #88aaff;
--cl-grad-angle: 0deg;
--cl-glow1: #ffffff;
--cl-glow2: #ffffff;
}
/* Apply solid color to lyrics text */
.colorama-single [class*="_lyricsText"] > div > span,
.colorama-single [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-single [class^="_lyricsContainer"] > div > div > span,
.colorama-single [class^="_lyricsContainer"] > div > div > span[data-current="true"] {
color: var(--cl-lyrics-color) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
}
/* Apply gradient to lyrics text */
.colorama-gradient [class*="_lyricsText"] > div > span,
.colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-gradient [class^="_lyricsContainer"] > div > div > span,
.colorama-gradient [class^="_lyricsContainer"] > div > div > span[data-current="true"] {
background: linear-gradient(var(--cl-grad-angle), var(--cl-grad-start), var(--cl-grad-end)) !important;
-webkit-background-clip: text !important;
background-clip: text !important;
color: transparent !important;
-webkit-text-fill-color: transparent !important;
}
/* 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"] {
filter: brightness(1.1) !important;
}
/* Keep song title color unchanged; its glow is controlled in Radiant CSS */
/* Color Radiant glow shadows using Colorama colors (respect RL sizes) */
.colorama-single [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-single [class^="_lyricsContainer"] > div > div > span[data-current="true"],
.colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-gradient [class^="_lyricsContainer"] > div > div > span[data-current="true"],
.colorama-gradient [class^="_lyricsContainer"] > div > div > span[data-current="true"] {
text-shadow: 0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff), 0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
}
/* Hover: force glow color to match Colorama settings for inactive lines */
.colorama-single [class*="_lyricsText"] > div > span:hover,
.colorama-single [class^="_lyricsContainer"] > div > div > span:hover {
color: var(--cl-lyrics-color) !important;
text-shadow: 0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff), 0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
}
.colorama-gradient [class*="_lyricsText"] > div > span:hover,
.colorama-gradient [class^="_lyricsContainer"] > div > div > span:hover {
background: linear-gradient(var(--cl-grad-angle), var(--cl-grad-start), var(--cl-grad-end)) !important;
-webkit-background-clip: text !important;
background-clip: text !important;
color: transparent !important;
-webkit-text-fill-color: transparent !important;
/* Do not increase glow strength on hover for gradients */
}
/* 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;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
}
+14 -1
View File
@@ -4,6 +4,7 @@ import React from "react";
export const settings = await ReactiveStore.getPluginStorage("RadiantLyrics", {
hideUIEnabled: true,
trackTitleGlow: false,
playerBarVisible: false,
lyricsGlowEnabled: true,
textGlow: 20,
@@ -14,7 +15,7 @@ export const settings = await ReactiveStore.getPluginStorage("RadiantLyrics", {
backgroundBlur: 80,
backgroundBrightness: 40,
spinSpeed: 45,
settingsAffectNowPlaying: true,
settingsAffectNowPlaying: true
});
export const Settings = () => {
@@ -30,6 +31,7 @@ export const Settings = () => {
const [backgroundBrightness, setBackgroundBrightness] = React.useState(settings.backgroundBrightness);
const [spinSpeed, setSpinSpeed] = React.useState(settings.spinSpeed);
const [settingsAffectNowPlaying, setSettingsAffectNowPlaying] = React.useState(settings.settingsAffectNowPlaying);
const [trackTitleGlow, setTrackTitleGlow] = React.useState(settings.trackTitleGlow);
return (
<LunaSettings>
@@ -44,6 +46,17 @@ export const Settings = () => {
(window as any).updateRadiantLyricsStyles();
}
}}
/>
<LunaSwitchSetting
title="Track Title Glow"
desc="Apply glow to the track title"
checked={trackTitleGlow}
onChange={(_: unknown, checked: boolean) => {
setTrackTitleGlow((settings.trackTitleGlow = checked));
if ((window as any).updateRadiantLyricsStyles) {
(window as any).updateRadiantLyricsStyles();
}
}}
/>
<LunaSwitchSetting
title="Hide UI Feature"
+10
View File
@@ -78,6 +78,16 @@ const updateRadiantLyricsStyles = function(): void {
}
}).catch(() => {});
}
// Track title glow toggle
const trackTitleEl = document.querySelector('[data-test="now-playing-track-title"]') as HTMLElement | null;
if (trackTitleEl) {
if (settings.trackTitleGlow && settings.lyricsGlowEnabled) {
trackTitleEl.classList.remove('rl-title-glow-disabled');
} else {
trackTitleEl.classList.add('rl-title-glow-disabled');
}
}
};
@@ -25,7 +25,7 @@
/* Enhanced lyrics styling with glow effects */
[class*="_lyricsText"] > div > span[data-current="true"] {
text-shadow: 0 0 var(--rl-glow-inner, 2px) #fff, 0 0 var(--rl-glow-outer, 20px) #fff !important;
text-shadow: 0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #fff), 0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #fff) !important;
padding-left: 20px;
transition-duration: 0.7s;
font-size: 55px;
@@ -52,7 +52,17 @@
/* Track title glow */
[data-test="now-playing-track-title"] {
text-shadow: 0 0 1px #fff, 0 0 var(--rl-glow-outer, 30px) #fff !important;
/* Title text color/gradient is left to default app styling; only glow is customized. */
text-shadow: 0 0 var(--rl-glow-inner, 1px) var(--cl-glow1, #fff), 0 0 var(--rl-glow-outer, 30px) #fff !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
color: inherit !important;
}
/* When track title glow setting is disabled, remove glow regardless of Colorama */
.rl-title-glow-disabled[data-test="now-playing-track-title"] {
text-shadow: none !important;
}
/* Current line transitions */