mirror of
https://github.com/meowarex/TidaLuna-Plugins.git
synced 2026-06-18 03:43:10 +10:00
@@ -1,16 +1,19 @@
|
|||||||
import { ReactiveStore } from "@luna/core";
|
import { ReactiveStore } from "@luna/core";
|
||||||
import { LunaSettings, LunaNumberSetting, LunaSwitchSetting, LunaTextSetting } from "@luna/ui";
|
import { LunaSettings, LunaSwitchSetting } from "@luna/ui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export type ColoramaMode = "single" | "gradient" | "auto-single" | "auto-gradient";
|
export type ColoramaMode = "single" | "gradient-experimental" | "cover" | "cover-gradient";
|
||||||
|
|
||||||
export const settings = await ReactiveStore.getPluginStorage("ColoramaLyrics", {
|
export const settings = await ReactiveStore.getPluginStorage("ColoramaLyrics", {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
mode: "single" as ColoramaMode,
|
mode: "single" as ColoramaMode,
|
||||||
// Store colors as ARGB hex (#AARRGGBB)
|
// Store colors as RGB hex (#RRGGBB) and opacity separately (0-100)
|
||||||
singleColor: "#FFFFFFFF",
|
singleColor: "#FFFFFF",
|
||||||
gradientStart: "#FFFFFFFF",
|
singleAlpha: 100,
|
||||||
gradientEnd: "#88AAFFFF",
|
gradientStart: "#FFFFFF",
|
||||||
|
gradientStartAlpha: 100,
|
||||||
|
gradientEnd: "#AAFFFF",
|
||||||
|
gradientEndAlpha: 100,
|
||||||
gradientAngle: 0,
|
gradientAngle: 0,
|
||||||
customColors: [] as string[],
|
customColors: [] as string[],
|
||||||
excludeInactive: false
|
excludeInactive: false
|
||||||
@@ -20,8 +23,11 @@ export const Settings = () => {
|
|||||||
const [enabled, setEnabled] = React.useState(settings.enabled);
|
const [enabled, setEnabled] = React.useState(settings.enabled);
|
||||||
const [mode, setMode] = React.useState<ColoramaMode>(settings.mode);
|
const [mode, setMode] = React.useState<ColoramaMode>(settings.mode);
|
||||||
const [singleColor, setSingleColor] = React.useState(settings.singleColor);
|
const [singleColor, setSingleColor] = React.useState(settings.singleColor);
|
||||||
|
const [singleAlpha, setSingleAlpha] = React.useState<number>(settings.singleAlpha ?? 100);
|
||||||
const [gradientStart, setGradientStart] = React.useState(settings.gradientStart);
|
const [gradientStart, setGradientStart] = React.useState(settings.gradientStart);
|
||||||
|
const [gradientStartAlpha, setGradientStartAlpha] = React.useState<number>(settings.gradientStartAlpha ?? 100);
|
||||||
const [gradientEnd, setGradientEnd] = React.useState(settings.gradientEnd);
|
const [gradientEnd, setGradientEnd] = React.useState(settings.gradientEnd);
|
||||||
|
const [gradientEndAlpha, setGradientEndAlpha] = React.useState<number>(settings.gradientEndAlpha ?? 100);
|
||||||
const [gradientAngle, setGradientAngle] = React.useState(settings.gradientAngle);
|
const [gradientAngle, setGradientAngle] = React.useState(settings.gradientAngle);
|
||||||
const [customInput, setCustomInput] = React.useState(settings.singleColor);
|
const [customInput, setCustomInput] = React.useState(settings.singleColor);
|
||||||
const [customColors, setCustomColors] = React.useState(settings.customColors);
|
const [customColors, setCustomColors] = React.useState(settings.customColors);
|
||||||
@@ -29,50 +35,40 @@ export const Settings = () => {
|
|||||||
const [isAnimatingIn, setIsAnimatingIn] = React.useState(false);
|
const [isAnimatingIn, setIsAnimatingIn] = React.useState(false);
|
||||||
const [shouldRender, setShouldRender] = React.useState(false);
|
const [shouldRender, setShouldRender] = React.useState(false);
|
||||||
const [excludeInactive, setExcludeInactive] = React.useState(settings.excludeInactive);
|
const [excludeInactive, setExcludeInactive] = React.useState(settings.excludeInactive);
|
||||||
|
const [activeEndpoint, setActiveEndpoint] = React.useState<'single' | 'start' | 'end'>('single');
|
||||||
|
const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType<any>;
|
||||||
|
|
||||||
// Helpers for ARGB <-> components
|
// Helper for HEX normalization
|
||||||
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n));
|
const normalizeToRGB = (hex: string, fallback: string = "#FFFFFF"): string => {
|
||||||
const normalizeToARGB = (hex: string, fallback: string = "#FFFFFFFF"): string => {
|
|
||||||
let v = hex.trim().toLowerCase();
|
let v = hex.trim().toLowerCase();
|
||||||
if (!v.startsWith('#')) v = `#${v}`;
|
if (!v.startsWith('#')) v = `#${v}`;
|
||||||
// #rgb or #rgba -> expand
|
// #rgb or #rgba -> expand
|
||||||
if (/^#([0-9a-f]{3,4})$/.test(v)) {
|
if (/^#([0-9a-f]{3,4})$/.test(v)) {
|
||||||
const m = v.slice(1);
|
const m = v.slice(1);
|
||||||
const a = m.length === 4 ? m[3] : 'f';
|
|
||||||
const r = m[0];
|
const r = m[0];
|
||||||
const g = m[1];
|
const g = m[1];
|
||||||
const b = m[2];
|
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
|
// #rrggbb
|
||||||
if (/^#([0-9a-f]{6})$/.test(v)) {
|
if (/^#([0-9a-f]{6})$/.test(v)) return v.toUpperCase();
|
||||||
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;
|
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 = [
|
const colorPresets = [
|
||||||
"#FFFFFFFF", "#FF0000FF", "#00FF00FF", "#0000FFFF", "#FFFF00FF", "#FF00FFFF", "#00FFFFFF",
|
"#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF",
|
||||||
"#FF8800FF", "#8800FFFF", "#0088FFFF", "#88FF00FF", "#FF0088FF", "#00FF88FF",
|
"#FF8800", "#8800FF", "#0088FF", "#88FF00", "#FF0088", "#00FF88",
|
||||||
"#444444FF", "#888888FF", "#CCCCCCFF", "#1DB954FF", "#E22134FF", "#1976D2FF"
|
"#444444", "#888888", "#CCCCCC", "#1DB954", "#E22134", "#1976D2"
|
||||||
];
|
];
|
||||||
|
|
||||||
const openPicker = () => {
|
const openPicker = (endpoint: 'single' | 'start' | 'end' = 'single') => {
|
||||||
|
setActiveEndpoint(endpoint);
|
||||||
setShowPicker(true);
|
setShowPicker(true);
|
||||||
setShouldRender(true);
|
setShouldRender(true);
|
||||||
setTimeout(() => setIsAnimatingIn(true), 10);
|
setTimeout(() => setIsAnimatingIn(true), 10);
|
||||||
@@ -85,16 +81,35 @@ export const Settings = () => {
|
|||||||
}, 200);
|
}, 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 applyCustomInputColor = (raw: string, updateInput: boolean): void => {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!hexColorRegex.test(trimmed)) return;
|
||||||
|
if (mode === "single") {
|
||||||
|
const next = normalizeToRGB(trimmed);
|
||||||
|
setSingleColor((settings.singleColor = next));
|
||||||
|
if (updateInput) setCustomInput(next);
|
||||||
|
} else if (mode === "gradient-experimental") {
|
||||||
|
const norm = normalizeToRGB(trimmed);
|
||||||
|
if (activeEndpoint === 'end') {
|
||||||
|
setGradientEnd((settings.gradientEnd = norm));
|
||||||
|
} else {
|
||||||
|
setGradientStart((settings.gradientStart = norm));
|
||||||
|
}
|
||||||
|
if (updateInput) setCustomInput(norm);
|
||||||
|
}
|
||||||
|
requestApply();
|
||||||
|
};
|
||||||
|
|
||||||
const addCustomColor = () => {
|
const addCustomColor = () => {
|
||||||
const trimmed = customInput.trim();
|
const trimmed = customInput.trim();
|
||||||
if (
|
if (
|
||||||
argbColorRegex.test(trimmed) &&
|
hexColorRegex.test(trimmed) &&
|
||||||
!colorPresets.includes(trimmed) &&
|
!colorPresets.includes(trimmed) &&
|
||||||
!customColors.includes(normalizeToARGB(trimmed))
|
!customColors.includes(normalizeToRGB(trimmed))
|
||||||
) {
|
) {
|
||||||
const updated = [...customColors, normalizeToARGB(trimmed)];
|
const updated = [...customColors, normalizeToRGB(trimmed)];
|
||||||
setCustomColors(updated);
|
setCustomColors(updated);
|
||||||
settings.customColors = updated;
|
settings.customColors = updated;
|
||||||
}
|
}
|
||||||
@@ -115,10 +130,10 @@ export const Settings = () => {
|
|||||||
return (
|
return (
|
||||||
<LunaSettings>
|
<LunaSettings>
|
||||||
|
|
||||||
{/* Mode selection via dropdown */}
|
{/* Mode selection via dropdown (aligned right) */}
|
||||||
<div style={{ padding: "8px 0", display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
|
<div style={{ padding: "8px 0", display: "flex", alignItems: "center", gap: 12 }}>
|
||||||
<div style={{ minWidth: 160 }}>
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<div style={{ fontWeight: "normal", fontSize: "1.075rem", marginBottom: 4 }}>Mode</div>
|
<div style={{ fontWeight: "normal", fontSize: "1.075rem" }}>Mode</div>
|
||||||
<div style={{ opacity: 0.7, fontSize: 14 }}>Choose how lyrics are colored</div>
|
<div style={{ opacity: 0.7, fontSize: 14 }}>Choose how lyrics are colored</div>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
@@ -132,15 +147,17 @@ export const Settings = () => {
|
|||||||
padding: "6px 10px",
|
padding: "6px 10px",
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
border: "1px solid rgba(255,255,255,0.2)",
|
border: "1px solid rgba(255,255,255,0.2)",
|
||||||
background: "rgba(255,255,255,0.05)",
|
background: "rgba(255,255,255,0.08)",
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
cursor: "pointer"
|
cursor: "pointer",
|
||||||
|
marginLeft: "auto",
|
||||||
|
minWidth: 180
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="single">Single</option>
|
<option value="single" style={{ color: '#000', background: '#fff' }}>Single</option>
|
||||||
<option value="gradient">Gradient</option>
|
<option value="gradient-experimental" style={{ color: '#000', background: '#fff' }}>Gradient - Experimental</option>
|
||||||
<option value="auto-single">Auto (Cover)</option>
|
<option value="cover" style={{ color: '#000', background: '#fff' }}>Cover - Experimental</option>
|
||||||
<option value="auto-gradient">Auto Gradient</option>
|
<option value="cover-gradient" style={{ color: '#000', background: '#fff' }}>Cover (Gradient) - Experimental</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -148,99 +165,64 @@ export const Settings = () => {
|
|||||||
<div style={{ padding: "8px 0", display: mode === "single" ? "flex" : "none", justifyContent: "space-between", alignItems: "center" }}>
|
<div style={{ padding: "8px 0", display: mode === "single" ? "flex" : "none", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: "normal", fontSize: "1.075rem", marginBottom: 4 }}>Lyrics Color</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 style={{ opacity: 0.7, fontSize: 14 }}>Set lyrics color</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: 8, alignItems: "center", position: "relative" }}>
|
<div style={{ display: "flex", gap: 8, alignItems: "center", position: "relative" }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => (showPicker ? closePicker() : openPicker())}
|
onClick={() => (showPicker ? closePicker() : openPicker('single'))}
|
||||||
style={{
|
style={{
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
border: "1px solid rgba(255,255,255,0.15)",
|
border: "1px solid rgba(255,255,255,0.15)",
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
background: normalizeToARGB(singleColor)
|
background: normalizeToRGB(singleColor)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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" }}>
|
{/* Gradient controls (open picker) */}
|
||||||
<div style={{ fontWeight: "normal", fontSize: "1.075rem", marginBottom: 4 }}>Gradient</div>
|
<div style={{ padding: "8px 0", display: mode === "gradient-experimental" ? "flex" : "none", justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<div style={{ opacity: 0.7, fontSize: 14, marginBottom: 8 }}>Pick start/end and angle</div>
|
<div>
|
||||||
<div style={{ display: "flex", gap: 12, alignItems: "center" }}>
|
<div style={{ fontWeight: "normal", fontSize: "1.075rem", marginBottom: 4 }}>Gradient (Experimental)</div>
|
||||||
<button
|
<div style={{ opacity: 0.7, fontSize: 14 }}>Set colors & angle</div>
|
||||||
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>
|
</div>
|
||||||
<LunaNumberSetting
|
<button
|
||||||
title="Start Alpha"
|
onClick={() => { setCustomInput(gradientStart); openPicker('start'); }}
|
||||||
desc="Opacity of the gradient start (0-100%)"
|
style={{
|
||||||
min={0}
|
padding: '8px 12px',
|
||||||
max={100}
|
borderRadius: 8,
|
||||||
step={1}
|
border: '1px solid rgba(255,255,255,0.2)',
|
||||||
value={Math.round(getAlpha01(gradientStart) * 100)}
|
background: 'rgba(255,255,255,0.08)',
|
||||||
onNumber={(value: number) => {
|
color: '#fff',
|
||||||
const next = setAlphaOnARGB(normalizeToARGB(gradientStart), value / 100);
|
cursor: 'pointer'
|
||||||
setGradientStart((settings.gradientStart = next));
|
|
||||||
requestApply();
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
<LunaNumberSetting
|
Configure
|
||||||
title="End Alpha"
|
</button>
|
||||||
desc="Opacity of the gradient end (0-100%)"
|
</div>
|
||||||
min={0}
|
|
||||||
max={100}
|
{/* Cover gradient controls (open picker for angle) */}
|
||||||
step={1}
|
<div style={{ padding: "8px 0", display: mode === "cover-gradient" ? "flex" : "none", justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
value={Math.round(getAlpha01(gradientEnd) * 100)}
|
<div>
|
||||||
onNumber={(value: number) => {
|
<div style={{ fontWeight: "normal", fontSize: "1.075rem", marginBottom: 4 }}>Cover (Gradient) - Experimental</div>
|
||||||
const next = setAlphaOnARGB(normalizeToARGB(gradientEnd), value / 100);
|
<div style={{ opacity: 0.7, fontSize: 14 }}>Set angle</div>
|
||||||
setGradientEnd((settings.gradientEnd = next));
|
</div>
|
||||||
requestApply();
|
<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'
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
<LunaNumberSetting
|
Configure
|
||||||
title="Gradient Angle"
|
</button>
|
||||||
desc="Angle in degrees (0-360)"
|
|
||||||
min={0}
|
|
||||||
max={360}
|
|
||||||
step={1}
|
|
||||||
value={gradientAngle}
|
|
||||||
onNumber={(value: number) => {
|
|
||||||
setGradientAngle((settings.gradientAngle = value));
|
|
||||||
requestApply();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal for picking and managing colors (reused) */}
|
{/* Modal for picking and managing colors (reused) */}
|
||||||
@@ -281,40 +263,72 @@ export const Settings = () => {
|
|||||||
transition: "all 0.2s ease"
|
transition: "all 0.2s ease"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ marginBottom: 12, color: "#fff", fontWeight: "bold", fontSize: 14 }}>Choose Color (ARGB HEX)</div>
|
<div style={{ marginBottom: 12, color: "#fff", fontWeight: "bold", fontSize: 14 }}>
|
||||||
|
{mode === 'single' ? 'Single Color' : 'Gradient Colors'}
|
||||||
|
</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'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<button
|
||||||
|
onClick={() => { setActiveEndpoint('end'); setCustomInput(gradientEnd); }}
|
||||||
|
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', 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 style={{ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: 8, marginBottom: 16 }}>
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: 8, marginBottom: 16 }}>
|
||||||
{allColors.map((color, index) => (
|
{allColors.map((color, index) => (
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (mode === "single") {
|
if (mode === "single") {
|
||||||
const next = normalizeToARGB(color);
|
const next = normalizeToRGB(color);
|
||||||
setSingleColor((settings.singleColor = next));
|
setSingleColor((settings.singleColor = next));
|
||||||
} else if (mode === "gradient") {
|
} else if (mode === "gradient-experimental") {
|
||||||
// Toggle which endpoint to update based on last edited input
|
if (activeEndpoint === 'end') {
|
||||||
if (customInput.toLowerCase() === gradientEnd.toLowerCase()) {
|
setGradientEnd((settings.gradientEnd = normalizeToRGB(color)));
|
||||||
setGradientEnd((settings.gradientEnd = normalizeToARGB(color)));
|
|
||||||
} else {
|
} else {
|
||||||
setGradientStart((settings.gradientStart = normalizeToARGB(color)));
|
setGradientStart((settings.gradientStart = normalizeToRGB(color)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setCustomInput(normalizeToARGB(color));
|
setCustomInput(normalizeToRGB(color));
|
||||||
requestApply();
|
requestApply();
|
||||||
closePicker();
|
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
border: "1px solid rgba(255,255,255,0.2)",
|
border: "1px solid rgba(255,255,255,0.2)",
|
||||||
background: normalizeToARGB(color),
|
background: normalizeToRGB(color),
|
||||||
cursor: "pointer"
|
cursor: "pointer"
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{mode !== 'cover-gradient' && (
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<div style={{ color: "rgba(255,255,255,0.7)", fontSize: 12, marginBottom: 6 }}>Custom ARGB Hex (#AARRGGBB)</div>
|
<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" }}>
|
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -322,23 +336,11 @@ export const Settings = () => {
|
|||||||
onChange={(e) => setCustomInput(e.target.value)}
|
onChange={(e) => setCustomInput(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
const trimmed = customInput.trim();
|
applyCustomInputColor(customInput, true);
|
||||||
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();
|
addCustomColor();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="#AARRGGBB"
|
placeholder="#RRGGBB"
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: "8px 12px",
|
padding: "8px 12px",
|
||||||
@@ -353,19 +355,7 @@ export const Settings = () => {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const trimmed = customInput.trim();
|
applyCustomInputColor(customInput, false);
|
||||||
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();
|
addCustomColor();
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
@@ -387,6 +377,111 @@ export const Settings = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Sliders inside picker based on mode */}
|
||||||
|
{mode === 'single' && (
|
||||||
|
<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);
|
||||||
|
setSingleAlpha((settings.singleAlpha = 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);
|
||||||
|
setGradientStartAlpha((settings.gradientStartAlpha = 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);
|
||||||
|
setGradientEndAlpha((settings.gradientEndAlpha = 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);
|
||||||
|
setGradientAngle((settings.gradientAngle = value));
|
||||||
|
requestApply();
|
||||||
|
}}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mode === 'cover-gradient' && (
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<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);
|
||||||
|
setGradientAngle((settings.gradientAngle = value));
|
||||||
|
requestApply();
|
||||||
|
}}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={closePicker}
|
onClick={closePicker}
|
||||||
style={{
|
style={{
|
||||||
@@ -405,8 +500,8 @@ export const Settings = () => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<LunaSwitchSetting
|
<AnySwitch
|
||||||
title="Exclude Inactive | Experimental"
|
title="Exclude Inactive"
|
||||||
desc="Apply color/gradient only to the currently active lyric line"
|
desc="Apply color/gradient only to the currently active lyric line"
|
||||||
checked={excludeInactive}
|
checked={excludeInactive}
|
||||||
onChange={(_: unknown, checked: boolean) => {
|
onChange={(_: unknown, checked: boolean) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LunaUnload, Tracer } from "@luna/core";
|
import { LunaUnload, Tracer } from "@luna/core";
|
||||||
import { StyleTag, observe, observePromise, PlayState } from "@luna/lib";
|
import { StyleTag, PlayState } from "@luna/lib";
|
||||||
import { settings, Settings } from "./Settings";
|
import { settings, Settings } from "./Settings";
|
||||||
|
|
||||||
import styles from "file://styles.css?minify";
|
import styles from "file://styles.css?minify";
|
||||||
@@ -9,7 +9,7 @@ export { Settings };
|
|||||||
|
|
||||||
export const unloads = new Set<LunaUnload>();
|
export const unloads = new Set<LunaUnload>();
|
||||||
|
|
||||||
const styleTag = new StyleTag("ColoramaLyrics", unloads, styles);
|
new StyleTag("ColoramaLyrics", unloads, styles);
|
||||||
|
|
||||||
// Simple dominant color extraction from current cover art
|
// Simple dominant color extraction from current cover art
|
||||||
async function getCoverArtElement(): Promise<HTMLImageElement | null> {
|
async function getCoverArtElement(): Promise<HTMLImageElement | null> {
|
||||||
@@ -63,10 +63,46 @@ function getDominantColorsFromImage(img: HTMLImageElement, count: number = 2): s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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}`;
|
||||||
|
if (/^#([0-9a-fA-F]{3})$/.test(v)) {
|
||||||
|
const r = parseInt(v[1] + v[1], 16);
|
||||||
|
const g = parseInt(v[2] + v[2], 16);
|
||||||
|
const b = parseInt(v[3] + v[3], 16);
|
||||||
|
return { r, g, b };
|
||||||
|
}
|
||||||
|
if (/^#([0-9a-fA-F]{6})$/.test(v)) {
|
||||||
|
const r = parseInt(v.slice(1, 3), 16);
|
||||||
|
const g = parseInt(v.slice(3, 5), 16);
|
||||||
|
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);
|
||||||
|
const b = parseInt(v.slice(7, 9), 16);
|
||||||
|
return { r, g, b };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbaFromHexAndAlpha(hex: string, alphaPercent: number | undefined): string {
|
||||||
|
const rgb = hexToRgb(hex);
|
||||||
|
const a = Math.max(0.05, Math.min(100, alphaPercent ?? 100)) / 100;
|
||||||
|
if (!rgb) return `rgba(255,255,255,${a})`;
|
||||||
|
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`;
|
||||||
|
}
|
||||||
|
|
||||||
function applySingleColor(color: string) {
|
function applySingleColor(color: string) {
|
||||||
document.documentElement.style.setProperty('--cl-lyrics-color', color);
|
const alpha = (settings as any).singleAlpha ?? 100;
|
||||||
document.documentElement.style.setProperty('--cl-glow1', color);
|
const rgba = rgbaFromHexAndAlpha(color, alpha);
|
||||||
document.documentElement.style.setProperty('--cl-glow2', color);
|
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-start');
|
||||||
document.documentElement.style.removeProperty('--cl-grad-end');
|
document.documentElement.style.removeProperty('--cl-grad-end');
|
||||||
document.documentElement.style.removeProperty('--cl-grad-angle');
|
document.documentElement.style.removeProperty('--cl-grad-angle');
|
||||||
@@ -75,16 +111,24 @@ function applySingleColor(color: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyGradient(start: string, end: string, angle: number) {
|
function applyGradient(start: string, end: string, angle: number) {
|
||||||
document.documentElement.style.setProperty('--cl-grad-start', start);
|
const startAlpha = (settings as any).gradientStartAlpha ?? 100;
|
||||||
document.documentElement.style.setProperty('--cl-grad-end', end);
|
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-grad-angle', `${angle}deg`);
|
||||||
document.documentElement.style.setProperty('--cl-glow1', start);
|
document.documentElement.style.setProperty('--cl-glow1', startRgba);
|
||||||
document.documentElement.style.setProperty('--cl-glow2', end);
|
document.documentElement.style.setProperty('--cl-glow2', endRgba);
|
||||||
document.body.classList.remove('colorama-single');
|
document.body.classList.remove('colorama-single');
|
||||||
document.body.classList.add('colorama-gradient');
|
document.body.classList.add('colorama-gradient');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyAutoColors(gradient: boolean) {
|
function resetModeClasses(): void {
|
||||||
|
document.body.classList.remove('colorama-single', 'colorama-gradient');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyCoverColors(gradient: boolean) {
|
||||||
const img = await getCoverArtElement();
|
const img = await getCoverArtElement();
|
||||||
if (!img) return;
|
if (!img) return;
|
||||||
const colors = getDominantColorsFromImage(img, gradient ? 2 : 1);
|
const colors = getDominantColorsFromImage(img, gradient ? 2 : 1);
|
||||||
@@ -105,23 +149,24 @@ function applyColoramaLyrics(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Toggle only-active-line mode class
|
// Toggle only-active-line mode class
|
||||||
if (settings.onlyActiveLine) {
|
if (settings.excludeInactive) {
|
||||||
document.body.classList.add('colorama-only-active');
|
document.body.classList.add('colorama-only-active');
|
||||||
} else {
|
} else {
|
||||||
document.body.classList.remove('colorama-only-active');
|
document.body.classList.remove('colorama-only-active');
|
||||||
}
|
}
|
||||||
|
resetModeClasses();
|
||||||
switch (settings.mode) {
|
switch (settings.mode) {
|
||||||
case "single":
|
case "single":
|
||||||
applySingleColor(settings.singleColor);
|
applySingleColor(settings.singleColor);
|
||||||
break;
|
break;
|
||||||
case "gradient":
|
case "gradient-experimental":
|
||||||
applyGradient(settings.gradientStart, settings.gradientEnd, settings.gradientAngle);
|
applyGradient(settings.gradientStart, settings.gradientEnd, settings.gradientAngle);
|
||||||
break;
|
break;
|
||||||
case "auto-single":
|
case "cover":
|
||||||
applyAutoColors(false);
|
applyCoverColors(false);
|
||||||
break;
|
break;
|
||||||
case "auto-gradient":
|
case "cover-gradient":
|
||||||
applyAutoColors(true);
|
applyCoverColors(true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,7 +180,7 @@ function observeTrackChanges(): void {
|
|||||||
const currentTrackId = PlayState.playbackContext?.actualProductId;
|
const currentTrackId = PlayState.playbackContext?.actualProductId;
|
||||||
if (currentTrackId && currentTrackId !== lastTrackId) {
|
if (currentTrackId && currentTrackId !== lastTrackId) {
|
||||||
lastTrackId = currentTrackId;
|
lastTrackId = currentTrackId;
|
||||||
if (settings.mode.startsWith("auto")) {
|
if (settings.mode === 'cover' || settings.mode === 'cover-gradient') {
|
||||||
setTimeout(() => applyColoramaLyrics(), 200);
|
setTimeout(() => applyColoramaLyrics(), 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,7 +194,7 @@ function observeTrackChanges(): void {
|
|||||||
setTimeout(() => applyColoramaLyrics(), 200);
|
setTimeout(() => applyColoramaLyrics(), 200);
|
||||||
observeTrackChanges();
|
observeTrackChanges();
|
||||||
|
|
||||||
// Ensure compatibility: re-apply after Radiant updates its styles/backgrounds
|
// for some reason, re-apply after Radiant updates its styles/backgrounds
|
||||||
function hookRadiantUpdates(): void {
|
function hookRadiantUpdates(): void {
|
||||||
const w = window as any;
|
const w = window as any;
|
||||||
const wrap = (name: string) => {
|
const wrap = (name: string) => {
|
||||||
|
|||||||
@@ -32,6 +32,8 @@
|
|||||||
-webkit-text-fill-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) */
|
/* Slight emphasis on current line (uniform to single mode) */
|
||||||
.colorama-gradient [class*="_lyricsText"] > 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"] {
|
||||||
@@ -69,12 +71,24 @@
|
|||||||
/* Only color active line mode */
|
/* Only color active line mode */
|
||||||
body.colorama-only-active.colorama-single [class*="_lyricsText"] > div > span:not([data-current="true"]),
|
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"]) {
|
body.colorama-only-active.colorama-gradient [class*="_lyricsText"] > div > span:not([data-current="true"]) {
|
||||||
/* Reset non-active lines to default */
|
/* Match Radiant inactive styling */
|
||||||
color: inherit !important;
|
color: rgba(128, 128, 128, 0.4) !important;
|
||||||
background: none !important;
|
background: none !important;
|
||||||
-webkit-background-clip: initial !important;
|
-webkit-background-clip: initial !important;
|
||||||
background-clip: initial !important;
|
background-clip: initial !important;
|
||||||
-webkit-text-fill-color: 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user