mirror of
https://github.com/meowarex/TidaLuna-Plugins.git
synced 2026-06-18 03:43:10 +10:00
Added Colorama-Lyrics
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -45,6 +47,17 @@ export const Settings = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<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"
|
||||
desc="Enable hide/unhide UI functionality with toggle buttons"
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user