mirror of
https://github.com/meowarex/TidaLuna-Plugins.git
synced 2026-06-18 03:43:10 +10:00
Add Audio Viz to Now Playing & Remove Lyrics Scrollbar
This commit is contained in:
@@ -8,53 +8,24 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
// Define a typed onChange signature for the switch
|
||||
type SwitchChangeHandler = (
|
||||
event: React.ChangeEvent<HTMLInputElement> | 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<ColoramaMode>(settings.mode);
|
||||
const [singleColor, setSingleColor] = React.useState(settings.singleColor);
|
||||
const [singleAlpha, setSingleAlpha] = React.useState<number>(
|
||||
settings.singleAlpha ?? 100,
|
||||
);
|
||||
const [gradientStart, setGradientStart] = React.useState(
|
||||
settings.gradientStart,
|
||||
);
|
||||
const [gradientStartAlpha, setGradientStartAlpha] = React.useState<number>(
|
||||
settings.gradientStartAlpha ?? 100,
|
||||
);
|
||||
const [gradientEnd, setGradientEnd] = React.useState(settings.gradientEnd);
|
||||
const [gradientEndAlpha, setGradientEndAlpha] = React.useState<number>(
|
||||
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);
|
||||
@@ -63,9 +34,6 @@ export const Settings = () => {
|
||||
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;
|
||||
@@ -73,28 +41,23 @@ export const Settings = () => {
|
||||
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;
|
||||
};
|
||||
@@ -121,8 +84,7 @@ export const Settings = () => {
|
||||
"#1976D2",
|
||||
];
|
||||
|
||||
const openPicker = (endpoint: "single" | "start" | "end" = "single") => {
|
||||
setActiveEndpoint(endpoint);
|
||||
const openPicker = () => {
|
||||
setShowPicker(true);
|
||||
setShouldRender(true);
|
||||
setTimeout(() => setIsAnimatingIn(true), 10);
|
||||
@@ -140,22 +102,10 @@ export const Settings = () => {
|
||||
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);
|
||||
}
|
||||
const next = normalizeToRGB(trimmed);
|
||||
settings.singleColor = next;
|
||||
setSingleColor(next);
|
||||
if (updateInput) setCustomInput(next);
|
||||
requestApply();
|
||||
};
|
||||
|
||||
@@ -172,12 +122,6 @@ export const Settings = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// const removeCustomColor = (color: string) => {
|
||||
// const updated = customColors.filter((c) => c !== color);
|
||||
// setCustomColors(updated);
|
||||
// settings.customColors = updated;
|
||||
// };
|
||||
|
||||
const allColors = [...colorPresets, ...customColors];
|
||||
|
||||
const requestApply = () => {
|
||||
@@ -186,66 +130,11 @@ export const Settings = () => {
|
||||
|
||||
return (
|
||||
<LunaSettings>
|
||||
{/* Mode selection via dropdown (aligned right) */}
|
||||
{/* Single color picker button */}
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 0",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<div style={{ fontWeight: "normal", fontSize: "1.075rem" }}>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;
|
||||
settings.mode = next;
|
||||
setMode(next);
|
||||
requestApply();
|
||||
}}
|
||||
style={{
|
||||
padding: "6px 10px",
|
||||
borderRadius: 6,
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
marginLeft: "auto",
|
||||
minWidth: 180,
|
||||
}}
|
||||
>
|
||||
<option value="single" style={{ color: "#000", background: "#fff" }}>
|
||||
Single
|
||||
</option>
|
||||
<option
|
||||
value="gradient-experimental"
|
||||
style={{ color: "#000", background: "#fff" }}
|
||||
>
|
||||
Gradient - Experimental
|
||||
</option>
|
||||
<option value="cover" style={{ color: "#000", background: "#fff" }}>
|
||||
Cover - Experimental
|
||||
</option>
|
||||
<option
|
||||
value="cover-gradient"
|
||||
style={{ color: "#000", background: "#fff" }}
|
||||
>
|
||||
Cover (Gradient) - Experimental
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Single color */}
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 0",
|
||||
display: mode === "single" ? "flex" : "none",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
@@ -272,7 +161,7 @@ export const Settings = () => {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (showPicker ? closePicker() : openPicker("single"))}
|
||||
onClick={() => (showPicker ? closePicker() : openPicker())}
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
@@ -285,84 +174,7 @@ export const Settings = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gradient controls (open picker) */}
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 0",
|
||||
display: mode === "gradient-experimental" ? "flex" : "none",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "normal",
|
||||
fontSize: "1.075rem",
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
Gradient (Experimental)
|
||||
</div>
|
||||
<div style={{ opacity: 0.7, fontSize: 14 }}>Set colors & angle</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCustomInput(gradientStart);
|
||||
openPicker("start");
|
||||
}}
|
||||
style={{
|
||||
padding: "8px 12px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Configure
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Cover gradient controls (open picker for angle) */}
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 0",
|
||||
display: mode === "cover-gradient" ? "flex" : "none",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "normal",
|
||||
fontSize: "1.075rem",
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
Cover (Gradient) - Experimental
|
||||
</div>
|
||||
<div style={{ opacity: 0.7, fontSize: 14 }}>Set angle</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openPicker("start")}
|
||||
style={{
|
||||
padding: "8px 12px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Configure
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Modal for picking and managing colors (reused) */}
|
||||
{/* Color picker modal */}
|
||||
{shouldRender && (
|
||||
<>
|
||||
<button
|
||||
@@ -415,369 +227,122 @@ export const Settings = () => {
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{mode === "single" ? "Single Color" : "Gradient Colors"}
|
||||
Lyrics Color
|
||||
</div>
|
||||
{mode === "gradient-experimental" && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 8,
|
||||
alignItems: "center",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ color: "rgba(255,255,255,0.7)", fontSize: 12 }}>
|
||||
Editing
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveEndpoint("start");
|
||||
setCustomInput(gradientStart);
|
||||
}}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "6px 10px",
|
||||
borderRadius: 8,
|
||||
border:
|
||||
activeEndpoint === "start"
|
||||
? "1px solid rgba(255,255,255,0.5)"
|
||||
: "1px solid rgba(255,255,255,0.2)",
|
||||
background: "rgba(255,255,255,0.05)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: 3,
|
||||
background: normalizeToRGB(gradientStart),
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: 12 }}>Start</span>
|
||||
</button>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(7, 1fr)",
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
{allColors.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveEndpoint("end");
|
||||
setCustomInput(gradientEnd);
|
||||
const next = normalizeToRGB(color);
|
||||
settings.singleColor = next;
|
||||
setSingleColor(next);
|
||||
setCustomInput(next);
|
||||
requestApply();
|
||||
}}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "6px 10px",
|
||||
borderRadius: 8,
|
||||
border:
|
||||
activeEndpoint === "end"
|
||||
? "1px solid rgba(255,255,255,0.5)"
|
||||
: "1px solid rgba(255,255,255,0.2)",
|
||||
background: "rgba(255,255,255,0.05)",
|
||||
color: "#fff",
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 6,
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
background: normalizeToRGB(color),
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: 3,
|
||||
background: normalizeToRGB(gradientEnd),
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: 12 }}>End</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{mode !== "cover-gradient" && (
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(7, 1fr)",
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
fontSize: 12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
{allColors.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const next = normalizeToRGB(color);
|
||||
if (mode === "single") {
|
||||
settings.singleColor = next;
|
||||
setSingleColor(next);
|
||||
} else if (mode === "gradient-experimental") {
|
||||
if (activeEndpoint === "end") {
|
||||
settings.gradientEnd = next;
|
||||
setGradientEnd(next);
|
||||
} else {
|
||||
settings.gradientStart = next;
|
||||
setGradientStart(next);
|
||||
}
|
||||
}
|
||||
setCustomInput(next);
|
||||
requestApply();
|
||||
}}
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 6,
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
background: normalizeToRGB(color),
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
Custom Hex (#RRGGBB)
|
||||
</div>
|
||||
)}
|
||||
{mode !== "cover-gradient" && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
fontSize: 12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
Custom Hex (#RRGGBB)
|
||||
</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") {
|
||||
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",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
applyCustomInputColor(customInput, false);
|
||||
<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") {
|
||||
applyCustomInputColor(customInput, true);
|
||||
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",
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Sliders inside picker based on mode */}
|
||||
{mode === "single" && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div
|
||||
}
|
||||
}}
|
||||
placeholder="#RRGGBB"
|
||||
style={{
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
fontSize: 12,
|
||||
marginBottom: 6,
|
||||
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",
|
||||
}}
|
||||
>
|
||||
Alpha
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={singleAlpha}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
settings.singleAlpha = value;
|
||||
setSingleAlpha(value);
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === "gradient-experimental" && (
|
||||
<div style={{ marginBottom: 16, display: "grid", gap: 16 }}>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 3,
|
||||
background: normalizeToRGB(gradientStart),
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}
|
||||
>
|
||||
Start Alpha
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={gradientStartAlpha}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
settings.gradientStartAlpha = value;
|
||||
setGradientStartAlpha(value);
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 3,
|
||||
background: normalizeToRGB(gradientEnd),
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}
|
||||
>
|
||||
End Alpha
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={gradientEndAlpha}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
settings.gradientEndAlpha = value;
|
||||
setGradientEndAlpha(value);
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}
|
||||
>
|
||||
Angle
|
||||
</div>
|
||||
<div
|
||||
style={{ color: "rgba(255,255,255,0.6)", fontSize: 12 }}
|
||||
>
|
||||
{gradientAngle}°
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={360}
|
||||
step={1}
|
||||
value={gradientAngle}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
settings.gradientAngle = value;
|
||||
setGradientAngle(value);
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === "cover-gradient" && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div
|
||||
<button
|
||||
onClick={() => {
|
||||
applyCustomInputColor(customInput, false);
|
||||
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: "space-between",
|
||||
marginBottom: 6,
|
||||
justifyContent: "center",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}>
|
||||
Angle
|
||||
</div>
|
||||
<div style={{ color: "rgba(255,255,255,0.6)", fontSize: 12 }}>
|
||||
{gradientAngle}°
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={360}
|
||||
step={1}
|
||||
value={gradientAngle}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
settings.gradientAngle = value;
|
||||
setGradientAngle(value);
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
fontSize: 12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
Alpha
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={singleAlpha}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
settings.singleAlpha = value;
|
||||
setSingleAlpha(value);
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={closePicker}
|
||||
@@ -800,7 +365,7 @@ export const Settings = () => {
|
||||
)}
|
||||
<AnySwitch
|
||||
title="Exclude Inactive"
|
||||
desc="Apply color/gradient only to the currently active lyric line"
|
||||
desc="Apply color only to the currently active lyric line"
|
||||
checked={excludeInactive}
|
||||
onChange={(_event: React.ChangeEvent<HTMLInputElement> | null, checked: boolean) => {
|
||||
settings.excludeInactive = checked;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LunaUnload, Tracer } from "@luna/core";
|
||||
import { StyleTag, PlayState } from "@luna/lib";
|
||||
import { StyleTag } from "@luna/lib";
|
||||
import { settings, Settings } from "./Settings";
|
||||
|
||||
import styles from "file://styles.css?minify";
|
||||
@@ -11,66 +11,6 @@ export const unloads = new Set<LunaUnload>();
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// build rgba() from hex + alpha percentage
|
||||
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
||||
let v = hex.trim();
|
||||
if (!v.startsWith("#")) v = `#${v}`;
|
||||
@@ -86,8 +26,6 @@ function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
||||
const b = parseInt(v.slice(5, 7), 16);
|
||||
return { r, g, b };
|
||||
}
|
||||
// 8-digit hex expects #AARRGGBB. Indices 1-3 are the alpha byte (ignored here),
|
||||
// so r/g/b are extracted from v.slice(3,5), v.slice(5,7), v.slice(7,9) respectively.
|
||||
if (/^#([0-9a-fA-F]{8})$/.test(v)) {
|
||||
const r = parseInt(v.slice(3, 5), 16);
|
||||
const g = parseInt(v.slice(5, 7), 16);
|
||||
@@ -113,102 +51,28 @@ function applySingleColor(color: string) {
|
||||
document.documentElement.style.setProperty("--cl-lyrics-color", rgba);
|
||||
document.documentElement.style.setProperty("--cl-glow1", rgba);
|
||||
document.documentElement.style.setProperty("--cl-glow2", rgba);
|
||||
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) {
|
||||
const startAlpha = (settings as any).gradientStartAlpha ?? 100;
|
||||
const endAlpha = (settings as any).gradientEndAlpha ?? 100;
|
||||
const startRgba = rgbaFromHexAndAlpha(start, startAlpha);
|
||||
const endRgba = rgbaFromHexAndAlpha(end, endAlpha);
|
||||
document.documentElement.style.setProperty("--cl-grad-start", startRgba);
|
||||
document.documentElement.style.setProperty("--cl-grad-end", endRgba);
|
||||
document.documentElement.style.setProperty("--cl-grad-angle", `${angle}deg`);
|
||||
document.documentElement.style.setProperty("--cl-glow1", startRgba);
|
||||
document.documentElement.style.setProperty("--cl-glow2", endRgba);
|
||||
document.body.classList.remove("colorama-single");
|
||||
document.body.classList.add("colorama-gradient");
|
||||
}
|
||||
|
||||
function resetModeClasses(): void {
|
||||
document.body.classList.remove("colorama-single", "colorama-gradient");
|
||||
}
|
||||
|
||||
async function applyCoverColors(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");
|
||||
document.body.classList.remove("colorama-single");
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle only-active-line mode class
|
||||
if (settings.excludeInactive) {
|
||||
document.body.classList.add("colorama-only-active");
|
||||
} else {
|
||||
document.body.classList.remove("colorama-only-active");
|
||||
}
|
||||
resetModeClasses();
|
||||
switch (settings.mode) {
|
||||
case "single":
|
||||
applySingleColor(settings.singleColor);
|
||||
break;
|
||||
case "gradient-experimental":
|
||||
applyGradient(
|
||||
settings.gradientStart,
|
||||
settings.gradientEnd,
|
||||
settings.gradientAngle,
|
||||
);
|
||||
break;
|
||||
case "cover":
|
||||
applyCoverColors(false);
|
||||
break;
|
||||
case "cover-gradient":
|
||||
applyCoverColors(true);
|
||||
break;
|
||||
}
|
||||
|
||||
applySingleColor(settings.singleColor);
|
||||
}
|
||||
|
||||
(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 === "cover" || settings.mode === "cover-gradient") {
|
||||
setTimeout(() => applyColoramaLyrics(), 200);
|
||||
}
|
||||
}
|
||||
};
|
||||
const interval = setInterval(check, 500);
|
||||
unloads.add(() => clearInterval(interval));
|
||||
check();
|
||||
}
|
||||
|
||||
// Initial apply and observers
|
||||
setTimeout(() => applyColoramaLyrics(), 200);
|
||||
observeTrackChanges();
|
||||
|
||||
// for some reason, re-apply after Radiant updates its styles/backgrounds
|
||||
function hookRadiantUpdates(): void {
|
||||
const w = window as any;
|
||||
const wrap = (name: string) => {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -24,54 +21,9 @@
|
||||
-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;
|
||||
}
|
||||
|
||||
/* 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"] {
|
||||
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
|
||||
@@ -90,20 +42,6 @@
|
||||
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 */
|
||||
}
|
||||
|
||||
/* MARKER: Radiant WBW Lyrics Support */
|
||||
|
||||
/* Single color: active wbw words & syllable finished */
|
||||
@@ -123,31 +61,6 @@
|
||||
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
|
||||
}
|
||||
|
||||
/* Gradient: active wbw words */
|
||||
.colorama-gradient .rl-wbw-word.rl-wbw-active {
|
||||
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;
|
||||
}
|
||||
|
||||
/* Gradient: syllable finished (solid color — gradient conflicts with sweep animation) */
|
||||
.colorama-gradient .rl-wbw-word.rl-syl-finished {
|
||||
color: var(--cl-glow1, #ffffff) !important;
|
||||
}
|
||||
|
||||
/* Gradient: active wbw word glow */
|
||||
.colorama-gradient .rl-wbw-word.rl-wbw-active {
|
||||
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: wbw words pick up Colorama colors */
|
||||
.colorama-single .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover,
|
||||
.colorama-single .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word.rl-wbw-word-hover {
|
||||
@@ -157,23 +70,8 @@
|
||||
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
|
||||
}
|
||||
|
||||
.colorama-gradient .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover,
|
||||
.colorama-gradient .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word.rl-wbw-word-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;
|
||||
}
|
||||
|
||||
/* Only-active: wbw words on inactive lines stay default */
|
||||
body.colorama-only-active.colorama-single
|
||||
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word,
|
||||
body.colorama-only-active.colorama-gradient
|
||||
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word {
|
||||
color: rgba(128, 128, 128, 0.4) !important;
|
||||
background: none !important;
|
||||
@@ -185,8 +83,6 @@ body.colorama-only-active.colorama-gradient
|
||||
|
||||
/* Only-active: hover on inactive wbw lines keeps default */
|
||||
body.colorama-only-active.colorama-single
|
||||
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover,
|
||||
body.colorama-only-active.colorama-gradient
|
||||
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover {
|
||||
color: lightgray !important;
|
||||
background: none !important;
|
||||
@@ -198,13 +94,8 @@ body.colorama-only-active.colorama-gradient
|
||||
|
||||
/* 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"]) {
|
||||
/* Match Radiant inactive styling */
|
||||
color: rgba(128, 128, 128, 0.4) !important;
|
||||
background: none !important;
|
||||
-webkit-background-clip: initial !important;
|
||||
@@ -215,10 +106,6 @@ body.colorama-only-active.colorama-gradient
|
||||
|
||||
/* 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;
|
||||
|
||||
Reference in New Issue
Block a user