mirror of
https://github.com/meowarex/TidaLuna-Plugins.git
synced 2026-06-18 03:43:10 +10:00
BIOME Formating
This commit is contained in:
@@ -1,13 +1,21 @@
|
||||
import { ReactiveStore } from "@luna/core";
|
||||
import { LunaSettings, LunaNumberSetting, LunaSwitchSetting, LunaTextSetting } from "@luna/ui";
|
||||
import {
|
||||
LunaSettings,
|
||||
LunaNumberSetting,
|
||||
LunaSwitchSetting,
|
||||
LunaTextSetting,
|
||||
} from "@luna/ui";
|
||||
import React from "react";
|
||||
|
||||
export const settings = await ReactiveStore.getPluginStorage("AudioVisualizer", {
|
||||
export const settings = await ReactiveStore.getPluginStorage(
|
||||
"AudioVisualizer",
|
||||
{
|
||||
barCount: 32,
|
||||
barColor: "#ffffff",
|
||||
barRounding: true,
|
||||
customColors: [] as string[]
|
||||
});
|
||||
customColors: [] as string[],
|
||||
},
|
||||
);
|
||||
|
||||
export const Settings = () => {
|
||||
const [barCount, setBarCount] = React.useState(settings.barCount);
|
||||
@@ -18,7 +26,9 @@ export const Settings = () => {
|
||||
const [shouldRender, setShouldRender] = React.useState(false);
|
||||
const [customInput, setCustomInput] = React.useState(settings.barColor);
|
||||
const [customColors, setCustomColors] = React.useState(settings.customColors);
|
||||
const [hoveredColorIndex, setHoveredColorIndex] = React.useState<number | null>(null);
|
||||
const [hoveredColorIndex, setHoveredColorIndex] = React.useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const closeColorPicker = () => {
|
||||
setIsAnimatingIn(false);
|
||||
@@ -43,9 +53,25 @@ export const Settings = () => {
|
||||
|
||||
// Common color presets for cool points :D
|
||||
const colorPresets = [
|
||||
"#ffffff", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff",
|
||||
"#ff8800", "#8800ff", "#0088ff", "#88ff00", "#ff0088", "#00ff88",
|
||||
"#444444", "#888888", "#cccccc", "#1db954", "#e22134", "#1976d2"
|
||||
"#ffffff",
|
||||
"#ff0000",
|
||||
"#00ff00",
|
||||
"#0000ff",
|
||||
"#ffff00",
|
||||
"#ff00ff",
|
||||
"#00ffff",
|
||||
"#ff8800",
|
||||
"#8800ff",
|
||||
"#0088ff",
|
||||
"#88ff00",
|
||||
"#ff0088",
|
||||
"#00ff88",
|
||||
"#444444",
|
||||
"#888888",
|
||||
"#cccccc",
|
||||
"#1db954",
|
||||
"#e22134",
|
||||
"#1976d2",
|
||||
];
|
||||
|
||||
const updateColor = (color: string) => {
|
||||
@@ -63,9 +89,11 @@ export const Settings = () => {
|
||||
// Validate hex color format
|
||||
const hexColorRegex = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
|
||||
|
||||
if (hexColorRegex.test(trimmedInput) &&
|
||||
if (
|
||||
hexColorRegex.test(trimmedInput) &&
|
||||
!colorPresets.includes(trimmedInput) &&
|
||||
!customColors.includes(trimmedInput)) {
|
||||
!customColors.includes(trimmedInput)
|
||||
) {
|
||||
const newCustomColors = [...customColors, trimmedInput];
|
||||
setCustomColors(newCustomColors);
|
||||
settings.customColors = newCustomColors;
|
||||
@@ -74,7 +102,9 @@ export const Settings = () => {
|
||||
};
|
||||
|
||||
const removeCustomColor = (colorToRemove: string) => {
|
||||
const newCustomColors = customColors.filter(color => color !== colorToRemove);
|
||||
const newCustomColors = customColors.filter(
|
||||
(color) => color !== colorToRemove,
|
||||
);
|
||||
setCustomColors(newCustomColors);
|
||||
settings.customColors = newCustomColors;
|
||||
|
||||
@@ -117,19 +147,40 @@ export const Settings = () => {
|
||||
{/* I'm not sure if this is a good idea, but it works & looks amazing */}
|
||||
{/* Sorry @Inrixia <3 */}
|
||||
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
padding: "16px 0",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontWeight: "normal", fontSize: "1.075rem", marginBottom: "4px" }}>Bar Color</div>
|
||||
<div style={{ opacity: 0.7, fontSize: "14px" }}>Color of the visualizer bars</div>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "normal",
|
||||
fontSize: "1.075rem",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
Bar Color
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "8px", alignItems: "center", position: "relative" }}>
|
||||
<div style={{ opacity: 0.7, fontSize: "14px" }}>
|
||||
Color of the visualizer bars
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => showColorPicker ? closeColorPicker() : openColorPicker()}
|
||||
onClick={() =>
|
||||
showColorPicker ? closeColorPicker() : openColorPicker()
|
||||
}
|
||||
style={{
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
@@ -140,15 +191,17 @@ export const Settings = () => {
|
||||
backdropFilter: "blur(10px)",
|
||||
WebkitBackdropFilter: "blur(10px)",
|
||||
position: "relative",
|
||||
overflow: "hidden"
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background: "rgba(0,0,0,0.1)",
|
||||
backdropFilter: "blur(2px)"
|
||||
}} />
|
||||
backdropFilter: "blur(2px)",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Custom Color Picker Modal */}
|
||||
@@ -165,13 +218,14 @@ export const Settings = () => {
|
||||
background: "rgba(0,0,0,0.6)",
|
||||
zIndex: 1000,
|
||||
opacity: isAnimatingIn ? 1 : 0,
|
||||
transition: "opacity 0.2s ease"
|
||||
transition: "opacity 0.2s ease",
|
||||
}}
|
||||
onClick={closeColorPicker}
|
||||
/>
|
||||
|
||||
{/* Color Picker Panel */}
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
@@ -187,20 +241,32 @@ export const Settings = () => {
|
||||
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: "12px", color: "#fff", fontWeight: "bold", fontSize: "14px" }}>
|
||||
transform: isAnimatingIn
|
||||
? "translate(-50%, -50%) scale(1)"
|
||||
: "translate(-50%, -50%) scale(0.9)",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "12px",
|
||||
color: "#fff",
|
||||
fontWeight: "bold",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
Choose Color
|
||||
</div>
|
||||
|
||||
{/* Color Grid */}
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(7, 1fr)",
|
||||
gap: "8px",
|
||||
marginBottom: "16px"
|
||||
}}>
|
||||
marginBottom: "16px",
|
||||
}}
|
||||
>
|
||||
{allColors.map((color, index) => {
|
||||
const isCustomColor = customColors.includes(color);
|
||||
const isHovered = hoveredColorIndex === index;
|
||||
@@ -211,7 +277,7 @@ export const Settings = () => {
|
||||
position: "relative",
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
cursor: "pointer"
|
||||
cursor: "pointer",
|
||||
}}
|
||||
className="color-item"
|
||||
onMouseEnter={() => setHoveredColorIndex(index)}
|
||||
@@ -226,10 +292,13 @@ export const Settings = () => {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
borderRadius: "6px",
|
||||
border: barColor === color ? "2px solid #fff" : "1px solid rgba(255,255,255,0.2)",
|
||||
border:
|
||||
barColor === color
|
||||
? "2px solid #fff"
|
||||
: "1px solid rgba(255,255,255,0.2)",
|
||||
background: color,
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease"
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
/>
|
||||
{isCustomColor && (
|
||||
@@ -255,7 +324,7 @@ export const Settings = () => {
|
||||
justifyContent: "center",
|
||||
opacity: isHovered ? 1 : 0,
|
||||
transition: "opacity 0.2s ease",
|
||||
zIndex: 10
|
||||
zIndex: 10,
|
||||
}}
|
||||
className="remove-button"
|
||||
>
|
||||
@@ -269,16 +338,28 @@ export const Settings = () => {
|
||||
|
||||
{/* Custom Hex Input */}
|
||||
<div style={{ marginBottom: "12px" }}>
|
||||
<div style={{ color: "rgba(255,255,255,0.7)", fontSize: "12px", marginBottom: "6px" }}>
|
||||
<div
|
||||
style={{
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
fontSize: "12px",
|
||||
marginBottom: "6px",
|
||||
}}
|
||||
>
|
||||
Add Custom Color
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={customInput}
|
||||
onChange={(e) => setCustomInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === "Enter") {
|
||||
updateColor(customInput);
|
||||
addCustomColor();
|
||||
}
|
||||
@@ -293,7 +374,7 @@ export const Settings = () => {
|
||||
color: "#fff",
|
||||
fontSize: "14px",
|
||||
fontFamily: "monospace",
|
||||
boxSizing: "border-box"
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
@@ -313,13 +394,15 @@ export const Settings = () => {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
transition: "all 0.2s ease"
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = "rgba(255,255,255,0.25)";
|
||||
e.currentTarget.style.background =
|
||||
"rgba(255,255,255,0.25)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = "rgba(255,255,255,0.15)";
|
||||
e.currentTarget.style.background =
|
||||
"rgba(255,255,255,0.15)";
|
||||
}}
|
||||
>
|
||||
+
|
||||
@@ -338,7 +421,7 @@ export const Settings = () => {
|
||||
background: "rgba(255,255,255,0.1)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
fontSize: "12px"
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
Done
|
||||
@@ -348,8 +431,6 @@ export const Settings = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</LunaSettings>
|
||||
);
|
||||
};
|
||||
@@ -10,21 +10,28 @@ export const { trace } = Tracer("[Audio Visualizer]");
|
||||
// Helper function for consistent logging
|
||||
const log = (message: string) => console.log(`[Audio Visualizer] ${message}`);
|
||||
const warn = (message: string) => console.warn(`[Audio Visualizer] ${message}`);
|
||||
const error = (message: string) => console.error(`[Audio Visualizer] ${message}`);
|
||||
const error = (message: string) =>
|
||||
console.error(`[Audio Visualizer] ${message}`);
|
||||
export { Settings };
|
||||
|
||||
// Basic config with settings
|
||||
const config = {
|
||||
enabled: true,
|
||||
position: 'left' as 'left' | 'right',
|
||||
position: "left" as "left" | "right",
|
||||
width: 200,
|
||||
height: 40,
|
||||
get barCount() { return settings.barCount; },
|
||||
get color() { return settings.barColor; },
|
||||
get barRounding() { return settings.barRounding; },
|
||||
get barCount() {
|
||||
return settings.barCount;
|
||||
},
|
||||
get color() {
|
||||
return settings.barColor;
|
||||
},
|
||||
get barRounding() {
|
||||
return settings.barRounding;
|
||||
},
|
||||
sensitivity: 1.5,
|
||||
smoothing: 0.8,
|
||||
visualizerType: 'bars' as 'bars' | 'waveform' | 'circular'
|
||||
visualizerType: "bars" as "bars" | "waveform" | "circular",
|
||||
};
|
||||
|
||||
// Clean up resources
|
||||
@@ -51,21 +58,24 @@ let canvasContext: CanvasRenderingContext2D | null = null;
|
||||
const findAudioElement = (): HTMLAudioElement | null => {
|
||||
// Try main selectors first
|
||||
const selectors = [
|
||||
'audio',
|
||||
'video',
|
||||
'audio[data-test]',
|
||||
'[data-test="audio-player"] audio'
|
||||
"audio",
|
||||
"video",
|
||||
"audio[data-test]",
|
||||
'[data-test="audio-player"] audio',
|
||||
];
|
||||
|
||||
for (const selector of selectors) {
|
||||
const element = document.querySelector(selector) as HTMLAudioElement;
|
||||
if (element && (element.tagName === 'AUDIO' || element.tagName === 'VIDEO')) {
|
||||
if (
|
||||
element &&
|
||||
(element.tagName === "AUDIO" || element.tagName === "VIDEO")
|
||||
) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
// Quick scan for any audio elements
|
||||
const audioElements = document.querySelectorAll('audio, video');
|
||||
const audioElements = document.querySelectorAll("audio, video");
|
||||
for (const element of audioElements) {
|
||||
const audioEl = element as HTMLAudioElement;
|
||||
if (audioEl.src || audioEl.currentSrc) {
|
||||
@@ -114,7 +124,10 @@ const initializeAudioVisualizer = async (): Promise<void> => {
|
||||
log("Connected to audio stream with output");
|
||||
} catch (error) {
|
||||
// Audio is connected elsewhere - that's fine, we just can't visualize
|
||||
if (error instanceof Error && error.message.includes('already connected')) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("already connected")
|
||||
) {
|
||||
log("Audio already connected elsewhere - skipping visualization");
|
||||
}
|
||||
return;
|
||||
@@ -123,7 +136,7 @@ const initializeAudioVisualizer = async (): Promise<void> => {
|
||||
|
||||
// Resume context only if needed and don't wait for it
|
||||
// (otherwise it will wait for the audio to start playing)
|
||||
if (audioContext.state === 'suspended') {
|
||||
if (audioContext.state === "suspended") {
|
||||
audioContext.resume().catch(() => {}); // Fire and forget
|
||||
}
|
||||
|
||||
@@ -136,7 +149,6 @@ const initializeAudioVisualizer = async (): Promise<void> => {
|
||||
if (!animationId) {
|
||||
animate();
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
// log errors
|
||||
console.error(err);
|
||||
@@ -151,7 +163,9 @@ const createVisualizerUI = (): void => {
|
||||
if (!config.enabled) return;
|
||||
|
||||
// Find the search bar
|
||||
const searchField = document.querySelector('input[class*="_searchField"]') as HTMLInputElement;
|
||||
const searchField = document.querySelector(
|
||||
'input[class*="_searchField"]',
|
||||
) as HTMLInputElement;
|
||||
if (!searchField) {
|
||||
warn("Search field not found");
|
||||
return;
|
||||
@@ -164,13 +178,13 @@ const createVisualizerUI = (): void => {
|
||||
}
|
||||
|
||||
// Create visualizer container
|
||||
visualizerContainer = document.createElement('div');
|
||||
visualizerContainer.id = 'audio-visualizer-container';
|
||||
visualizerContainer = document.createElement("div");
|
||||
visualizerContainer.id = "audio-visualizer-container";
|
||||
visualizerContainer.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-${config.position === 'left' ? 'right' : 'left'}: 12px;
|
||||
margin-${config.position === "left" ? "right" : "left"}: 12px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
@@ -179,7 +193,7 @@ const createVisualizerUI = (): void => {
|
||||
`;
|
||||
|
||||
// Create canvas
|
||||
canvas = document.createElement('canvas');
|
||||
canvas = document.createElement("canvas");
|
||||
canvas.width = config.width;
|
||||
canvas.height = config.height;
|
||||
canvas.style.cssText = `
|
||||
@@ -189,13 +203,19 @@ const createVisualizerUI = (): void => {
|
||||
`;
|
||||
|
||||
visualizerContainer.appendChild(canvas);
|
||||
canvasContext = canvas.getContext('2d');
|
||||
canvasContext = canvas.getContext("2d");
|
||||
|
||||
// Insert visualizer next to search bar
|
||||
if (config.position === 'left') {
|
||||
searchContainer.parentElement?.insertBefore(visualizerContainer, searchContainer);
|
||||
if (config.position === "left") {
|
||||
searchContainer.parentElement?.insertBefore(
|
||||
visualizerContainer,
|
||||
searchContainer,
|
||||
);
|
||||
} else {
|
||||
searchContainer.parentElement?.insertBefore(visualizerContainer, searchContainer.nextSibling);
|
||||
searchContainer.parentElement?.insertBefore(
|
||||
visualizerContainer,
|
||||
searchContainer.nextSibling,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -225,7 +245,8 @@ const animate = (): void => {
|
||||
if (analyser && dataArray) {
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
// Check if there's actual audio signal (not just silence)
|
||||
const avgVolume = dataArray.reduce((sum, val) => sum + val, 0) / dataArray.length;
|
||||
const avgVolume =
|
||||
dataArray.reduce((sum, val) => sum + val, 0) / dataArray.length;
|
||||
hasRealAudio = avgVolume > 5; // Threshold for detecting actual audio
|
||||
}
|
||||
|
||||
@@ -235,13 +256,13 @@ const animate = (): void => {
|
||||
if (hasRealAudio && analyser && dataArray) {
|
||||
// Draw real audio visualization
|
||||
switch (config.visualizerType) {
|
||||
case 'bars': // Is implemented YAYYY (default)
|
||||
case "bars": // Is implemented YAYYY (default)
|
||||
drawBars();
|
||||
break;
|
||||
case 'waveform': // Not implemented yet
|
||||
case "waveform": // Not implemented yet
|
||||
drawWaveform();
|
||||
break;
|
||||
case 'circular': // Not implemented yet
|
||||
case "circular": // Not implemented yet
|
||||
drawCircular();
|
||||
break;
|
||||
}
|
||||
@@ -257,7 +278,14 @@ const animate = (): void => {
|
||||
let waveTime = 0;
|
||||
|
||||
// Helper function to draw rounded rectangles
|
||||
const drawRoundedRect = (ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number): void => {
|
||||
const drawRoundedRect = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
radius: number,
|
||||
): void => {
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y, width, height, radius);
|
||||
ctx.fill();
|
||||
@@ -314,7 +342,7 @@ const drawBars = (): void => {
|
||||
|
||||
for (let i = 0; i < config.barCount; i++) {
|
||||
const dataIndex = Math.floor(i * (dataArray.length / config.barCount));
|
||||
const barHeight = (dataArray[dataIndex] * config.sensitivity * heightScale);
|
||||
const barHeight = dataArray[dataIndex] * config.sensitivity * heightScale;
|
||||
|
||||
const x = i * barWidth;
|
||||
const y = canvas.height - barHeight;
|
||||
@@ -456,7 +484,8 @@ const observePlayState = (): void => {
|
||||
// Start with fast checking, then slow down
|
||||
const fastInterval = setInterval(() => {
|
||||
checkAndInitialize();
|
||||
if (checkCount > 10) { // After 10 quick checks, switch to slower
|
||||
if (checkCount > 10) {
|
||||
// After 10 quick checks, switch to slower
|
||||
clearInterval(fastInterval);
|
||||
const slowInterval = setInterval(checkAndInitialize, 2000);
|
||||
unloads.add(() => clearInterval(slowInterval));
|
||||
@@ -507,7 +536,7 @@ const completeCleanup = (): void => {
|
||||
}
|
||||
|
||||
// Close audio context completely on plugin unload
|
||||
if (audioContext && audioContext.state !== 'closed') {
|
||||
if (audioContext && audioContext.state !== "closed") {
|
||||
audioContext.close();
|
||||
log("Closed AudioContext");
|
||||
}
|
||||
|
||||
@@ -9,4 +9,3 @@
|
||||
"main": "./src/index.ts",
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@ import { ReactiveStore } from "@luna/core";
|
||||
import { LunaSettings, LunaSwitchSetting } from "@luna/ui";
|
||||
import React from "react";
|
||||
|
||||
export type ColoramaMode = "single" | "gradient-experimental" | "cover" | "cover-gradient";
|
||||
export type ColoramaMode =
|
||||
| "single"
|
||||
| "gradient-experimental"
|
||||
| "cover"
|
||||
| "cover-gradient";
|
||||
|
||||
export const settings = await ReactiveStore.getPluginStorage("ColoramaLyrics", {
|
||||
enabled: true,
|
||||
@@ -16,32 +20,49 @@ export const settings = await ReactiveStore.getPluginStorage("ColoramaLyrics", {
|
||||
gradientEndAlpha: 100,
|
||||
gradientAngle: 0,
|
||||
customColors: [] as string[],
|
||||
excludeInactive: false
|
||||
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 [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 [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);
|
||||
const [isAnimatingIn, setIsAnimatingIn] = React.useState(false);
|
||||
const [shouldRender, setShouldRender] = React.useState(false);
|
||||
const [excludeInactive, setExcludeInactive] = React.useState(settings.excludeInactive);
|
||||
const [activeEndpoint, setActiveEndpoint] = React.useState<'single' | 'start' | 'end'>('single');
|
||||
const [excludeInactive, setExcludeInactive] = React.useState(
|
||||
settings.excludeInactive,
|
||||
);
|
||||
const [activeEndpoint, setActiveEndpoint] = React.useState<
|
||||
"single" | "start" | "end"
|
||||
>("single");
|
||||
const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType<any>;
|
||||
|
||||
// Helper for HEX normalization
|
||||
const normalizeToRGB = (hex: string, fallback: string = "#FFFFFF"): string => {
|
||||
const normalizeToRGB = (
|
||||
hex: string,
|
||||
fallback: string = "#FFFFFF",
|
||||
): string => {
|
||||
let v = hex.trim().toLowerCase();
|
||||
if (!v.startsWith('#')) v = `#${v}`;
|
||||
if (!v.startsWith("#")) v = `#${v}`;
|
||||
// #rgb or #rgba -> expand
|
||||
if (/^#([0-9a-f]{3,4})$/.test(v)) {
|
||||
const m = v.slice(1);
|
||||
@@ -62,12 +83,28 @@ export const Settings = () => {
|
||||
};
|
||||
|
||||
const colorPresets = [
|
||||
"#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF",
|
||||
"#FF8800", "#8800FF", "#0088FF", "#88FF00", "#FF0088", "#00FF88",
|
||||
"#444444", "#888888", "#CCCCCC", "#1DB954", "#E22134", "#1976D2"
|
||||
"#FFFFFF",
|
||||
"#FF0000",
|
||||
"#00FF00",
|
||||
"#0000FF",
|
||||
"#FFFF00",
|
||||
"#FF00FF",
|
||||
"#00FFFF",
|
||||
"#FF8800",
|
||||
"#8800FF",
|
||||
"#0088FF",
|
||||
"#88FF00",
|
||||
"#FF0088",
|
||||
"#00FF88",
|
||||
"#444444",
|
||||
"#888888",
|
||||
"#CCCCCC",
|
||||
"#1DB954",
|
||||
"#E22134",
|
||||
"#1976D2",
|
||||
];
|
||||
|
||||
const openPicker = (endpoint: 'single' | 'start' | 'end' = 'single') => {
|
||||
const openPicker = (endpoint: "single" | "start" | "end" = "single") => {
|
||||
setActiveEndpoint(endpoint);
|
||||
setShowPicker(true);
|
||||
setShouldRender(true);
|
||||
@@ -92,7 +129,7 @@ export const Settings = () => {
|
||||
if (updateInput) setCustomInput(next);
|
||||
} else if (mode === "gradient-experimental") {
|
||||
const norm = normalizeToRGB(trimmed);
|
||||
if (activeEndpoint === 'end') {
|
||||
if (activeEndpoint === "end") {
|
||||
setGradientEnd((settings.gradientEnd = norm));
|
||||
} else {
|
||||
setGradientStart((settings.gradientStart = norm));
|
||||
@@ -116,7 +153,7 @@ export const Settings = () => {
|
||||
};
|
||||
|
||||
const removeCustomColor = (color: string) => {
|
||||
const updated = customColors.filter(c => c !== color);
|
||||
const updated = customColors.filter((c) => c !== color);
|
||||
setCustomColors(updated);
|
||||
settings.customColors = updated;
|
||||
};
|
||||
@@ -129,12 +166,20 @@ export const Settings = () => {
|
||||
|
||||
return (
|
||||
<LunaSettings>
|
||||
|
||||
{/* Mode selection via dropdown (aligned right) */}
|
||||
<div style={{ padding: "8px 0", display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<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 style={{ opacity: 0.7, fontSize: 14 }}>
|
||||
Choose how lyrics are colored
|
||||
</div>
|
||||
</div>
|
||||
<select
|
||||
value={mode}
|
||||
@@ -151,53 +196,106 @@ export const Settings = () => {
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
marginLeft: "auto",
|
||||
minWidth: 180
|
||||
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>
|
||||
<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" }}>
|
||||
<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={{
|
||||
fontWeight: "normal",
|
||||
fontSize: "1.075rem",
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
Lyrics Color
|
||||
</div>
|
||||
<div style={{ opacity: 0.7, fontSize: 14 }}>Set lyrics color</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center", position: "relative" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 8,
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => (showPicker ? closePicker() : openPicker('single'))}
|
||||
onClick={() => (showPicker ? closePicker() : openPicker("single"))}
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
borderRadius: 6,
|
||||
cursor: "pointer",
|
||||
background: normalizeToRGB(singleColor)
|
||||
background: normalizeToRGB(singleColor),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Gradient controls (open picker) */}
|
||||
<div style={{ padding: "8px 0", display: mode === "gradient-experimental" ? "flex" : "none", justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<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={{
|
||||
fontWeight: "normal",
|
||||
fontSize: "1.075rem",
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
Gradient (Experimental)
|
||||
</div>
|
||||
<div style={{ opacity: 0.7, fontSize: 14 }}>Set colors & angle</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { setCustomInput(gradientStart); openPicker('start'); }}
|
||||
onClick={() => {
|
||||
setCustomInput(gradientStart);
|
||||
openPicker("start");
|
||||
}}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
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'
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Configure
|
||||
@@ -205,20 +303,35 @@ export const Settings = () => {
|
||||
</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
|
||||
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={{
|
||||
fontWeight: "normal",
|
||||
fontSize: "1.075rem",
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
Cover (Gradient) - Experimental
|
||||
</div>
|
||||
<div style={{ opacity: 0.7, fontSize: 14 }}>Set angle</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => openPicker('start')}
|
||||
onClick={() => openPicker("start")}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
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'
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Configure
|
||||
@@ -238,7 +351,7 @@ export const Settings = () => {
|
||||
background: "rgba(0,0,0,0.6)",
|
||||
zIndex: 1000,
|
||||
opacity: isAnimatingIn ? 1 : 0,
|
||||
transition: "opacity 0.2s ease"
|
||||
transition: "opacity 0.2s ease",
|
||||
}}
|
||||
onClick={closePicker}
|
||||
/>
|
||||
@@ -259,44 +372,107 @@ export const Settings = () => {
|
||||
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"
|
||||
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 }}>
|
||||
{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); }}
|
||||
<div
|
||||
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'
|
||||
marginBottom: 12,
|
||||
color: "#fff",
|
||||
fontWeight: "bold",
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
<span style={{ width: 14, height: 14, borderRadius: 3, background: normalizeToRGB(gradientStart), border: '1px solid rgba(255,255,255,0.3)' }} />
|
||||
{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); }}
|
||||
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'
|
||||
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={{
|
||||
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 }}>
|
||||
{mode !== "cover-gradient" && (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(7, 1fr)",
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
{allColors.map((color, index) => (
|
||||
<button
|
||||
key={index}
|
||||
@@ -305,10 +481,14 @@ export const Settings = () => {
|
||||
const next = normalizeToRGB(color);
|
||||
setSingleColor((settings.singleColor = next));
|
||||
} else if (mode === "gradient-experimental") {
|
||||
if (activeEndpoint === 'end') {
|
||||
setGradientEnd((settings.gradientEnd = normalizeToRGB(color)));
|
||||
if (activeEndpoint === "end") {
|
||||
setGradientEnd(
|
||||
(settings.gradientEnd = normalizeToRGB(color)),
|
||||
);
|
||||
} else {
|
||||
setGradientStart((settings.gradientStart = normalizeToRGB(color)));
|
||||
setGradientStart(
|
||||
(settings.gradientStart = normalizeToRGB(color)),
|
||||
);
|
||||
}
|
||||
}
|
||||
setCustomInput(normalizeToRGB(color));
|
||||
@@ -320,22 +500,30 @@ export const Settings = () => {
|
||||
borderRadius: 6,
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
background: normalizeToRGB(color),
|
||||
cursor: "pointer"
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{mode !== 'cover-gradient' && (
|
||||
{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={{
|
||||
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') {
|
||||
if (e.key === "Enter") {
|
||||
applyCustomInputColor(customInput, true);
|
||||
addCustomColor();
|
||||
}
|
||||
@@ -350,7 +538,7 @@ export const Settings = () => {
|
||||
color: "#fff",
|
||||
fontSize: 14,
|
||||
fontFamily: "monospace",
|
||||
boxSizing: "border-box"
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
@@ -370,7 +558,7 @@ export const Settings = () => {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
transition: "all 0.2s ease"
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
+
|
||||
@@ -379,9 +567,17 @@ export const Settings = () => {
|
||||
</div>
|
||||
)}
|
||||
{/* Sliders inside picker based on mode */}
|
||||
{mode === 'single' && (
|
||||
{mode === "single" && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ color: "rgba(255,255,255,0.8)", fontSize: 12, marginBottom: 6 }}>Alpha</div>
|
||||
<div
|
||||
style={{
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
fontSize: 12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
Alpha
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
@@ -393,17 +589,36 @@ export const Settings = () => {
|
||||
setSingleAlpha((settings.singleAlpha = value));
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'gradient-experimental' && (
|
||||
<div style={{ marginBottom: 16, display: 'grid', gap: 16 }}>
|
||||
{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
|
||||
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"
|
||||
@@ -413,16 +628,37 @@ export const Settings = () => {
|
||||
value={gradientStartAlpha}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
setGradientStartAlpha((settings.gradientStartAlpha = value));
|
||||
setGradientStartAlpha(
|
||||
(settings.gradientStartAlpha = value),
|
||||
);
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
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
|
||||
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"
|
||||
@@ -435,13 +671,28 @@ export const Settings = () => {
|
||||
setGradientEndAlpha((settings.gradientEndAlpha = value));
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
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
|
||||
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"
|
||||
@@ -454,17 +705,28 @@ export const Settings = () => {
|
||||
setGradientAngle((settings.gradientAngle = value));
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'cover-gradient' && (
|
||||
{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
|
||||
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"
|
||||
@@ -477,7 +739,7 @@ export const Settings = () => {
|
||||
setGradientAngle((settings.gradientAngle = value));
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -492,7 +754,7 @@ export const Settings = () => {
|
||||
background: "rgba(255,255,255,0.1)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
fontSize: 12
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
Done
|
||||
@@ -512,5 +774,3 @@ export const Settings = () => {
|
||||
</LunaSettings>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -13,9 +13,13 @@ 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;
|
||||
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;
|
||||
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;
|
||||
@@ -31,10 +35,13 @@ async function getCoverArtElement(): Promise<HTMLImageElement | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDominantColorsFromImage(img: HTMLImageElement, count: number = 2): string[] {
|
||||
function getDominantColorsFromImage(
|
||||
img: HTMLImageElement,
|
||||
count: number = 2,
|
||||
): string[] {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return ["#ffffff", "#88aaff"]; // fallback
|
||||
const w = 64;
|
||||
const h = 64;
|
||||
@@ -54,8 +61,8 @@ function getDominantColorsFromImage(img: HTMLImageElement, count: number = 2): s
|
||||
}
|
||||
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('')}`;
|
||||
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 {
|
||||
@@ -66,7 +73,7 @@ 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 (!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);
|
||||
@@ -90,7 +97,10 @@ function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function rgbaFromHexAndAlpha(hex: string, alphaPercent: number | undefined): string {
|
||||
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})`;
|
||||
@@ -100,14 +110,14 @@ function rgbaFromHexAndAlpha(hex: string, alphaPercent: number | undefined): str
|
||||
function applySingleColor(color: string) {
|
||||
const alpha = (settings as any).singleAlpha ?? 100;
|
||||
const rgba = rgbaFromHexAndAlpha(color, alpha);
|
||||
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');
|
||||
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) {
|
||||
@@ -115,17 +125,17 @@ function applyGradient(start: string, end: string, angle: number) {
|
||||
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');
|
||||
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');
|
||||
document.body.classList.remove("colorama-single", "colorama-gradient");
|
||||
}
|
||||
|
||||
async function applyCoverColors(gradient: boolean) {
|
||||
@@ -144,15 +154,15 @@ async function applyCoverColors(gradient: boolean) {
|
||||
|
||||
function applyColoramaLyrics(): void {
|
||||
if (!settings.enabled) {
|
||||
document.body.classList.remove('colorama-single', 'colorama-gradient');
|
||||
document.body.classList.remove("colorama-single", "colorama-gradient");
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle only-active-line mode class
|
||||
if (settings.excludeInactive) {
|
||||
document.body.classList.add('colorama-only-active');
|
||||
document.body.classList.add("colorama-only-active");
|
||||
} else {
|
||||
document.body.classList.remove('colorama-only-active');
|
||||
document.body.classList.remove("colorama-only-active");
|
||||
}
|
||||
resetModeClasses();
|
||||
switch (settings.mode) {
|
||||
@@ -160,7 +170,11 @@ function applyColoramaLyrics(): void {
|
||||
applySingleColor(settings.singleColor);
|
||||
break;
|
||||
case "gradient-experimental":
|
||||
applyGradient(settings.gradientStart, settings.gradientEnd, settings.gradientAngle);
|
||||
applyGradient(
|
||||
settings.gradientStart,
|
||||
settings.gradientEnd,
|
||||
settings.gradientAngle,
|
||||
);
|
||||
break;
|
||||
case "cover":
|
||||
applyCoverColors(false);
|
||||
@@ -180,7 +194,7 @@ function observeTrackChanges(): void {
|
||||
const currentTrackId = PlayState.playbackContext?.actualProductId;
|
||||
if (currentTrackId && currentTrackId !== lastTrackId) {
|
||||
lastTrackId = currentTrackId;
|
||||
if (settings.mode === 'cover' || settings.mode === 'cover-gradient') {
|
||||
if (settings.mode === "cover" || settings.mode === "cover-gradient") {
|
||||
setTimeout(() => applyColoramaLyrics(), 200);
|
||||
}
|
||||
}
|
||||
@@ -199,23 +213,23 @@ function hookRadiantUpdates(): void {
|
||||
const w = window as any;
|
||||
const wrap = (name: string) => {
|
||||
const fn = w[name];
|
||||
if (typeof fn === 'function' && !fn.__coloramaPatched) {
|
||||
if (typeof fn === "function" && !fn.__coloramaPatched) {
|
||||
const orig = fn.bind(w);
|
||||
const patched = (...args: unknown[]) => {
|
||||
const result = orig(...args);
|
||||
try { applyColoramaLyrics(); } catch {}
|
||||
try {
|
||||
applyColoramaLyrics();
|
||||
} catch {}
|
||||
return result;
|
||||
};
|
||||
(patched as any).__coloramaPatched = true;
|
||||
w[name] = patched;
|
||||
}
|
||||
};
|
||||
wrap('updateRadiantLyricsStyles');
|
||||
wrap('updateRadiantLyricsNowPlayingBackground');
|
||||
wrap('updateRadiantLyricsGlobalBackground');
|
||||
wrap('updateRadiantLyricsTextGlow');
|
||||
wrap("updateRadiantLyricsStyles");
|
||||
wrap("updateRadiantLyricsNowPlayingBackground");
|
||||
wrap("updateRadiantLyricsGlobalBackground");
|
||||
wrap("updateRadiantLyricsTextGlow");
|
||||
}
|
||||
|
||||
setTimeout(() => hookRadiantUpdates(), 0);
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
.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"] {
|
||||
.colorama-single
|
||||
[class^="_lyricsContainer"]
|
||||
> div
|
||||
> div
|
||||
> span[data-current="true"] {
|
||||
color: var(--cl-lyrics-color) !important;
|
||||
background: none !important;
|
||||
-webkit-background-clip: initial !important;
|
||||
@@ -24,8 +28,16 @@
|
||||
.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;
|
||||
.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;
|
||||
@@ -36,7 +48,11 @@
|
||||
|
||||
/* 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"] {
|
||||
.colorama-gradient
|
||||
[class^="_lyricsContainer"]
|
||||
> div
|
||||
> div
|
||||
> span[data-current="true"] {
|
||||
filter: brightness(1.1) !important;
|
||||
}
|
||||
|
||||
@@ -44,23 +60,43 @@
|
||||
|
||||
/* 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-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;
|
||||
.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;
|
||||
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;
|
||||
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;
|
||||
@@ -69,8 +105,13 @@
|
||||
}
|
||||
|
||||
/* 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"]) {
|
||||
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;
|
||||
@@ -81,8 +122,13 @@ body.colorama-only-active.colorama-gradient [class*="_lyricsText"] > div > span:
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
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;
|
||||
@@ -90,5 +136,3 @@ body.colorama-only-active.colorama-gradient [class*="_lyricsText"] > div > span:
|
||||
-webkit-text-fill-color: initial !important;
|
||||
text-shadow: initial !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -73,7 +73,11 @@ const onMouseUp = function (event: MouseEvent): void {
|
||||
if (span.hasAttribute("data-current")) {
|
||||
hasCorrectAttribute = true;
|
||||
text += span.textContent + "\n";
|
||||
if ([...span.classList].some((className) => className.startsWith("endOfStanza--"))) {
|
||||
if (
|
||||
[...span.classList].some((className) =>
|
||||
className.startsWith("endOfStanza--"),
|
||||
)
|
||||
) {
|
||||
text += "\n";
|
||||
}
|
||||
}
|
||||
@@ -95,7 +99,10 @@ const onClickHooked = function (event: MouseEvent): boolean | void {
|
||||
if (!isSelecting) return;
|
||||
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.tagName.toLowerCase() === "span" && target.hasAttribute("data-current")) {
|
||||
if (
|
||||
target.tagName.toLowerCase() === "span" &&
|
||||
target.hasAttribute("data-current")
|
||||
) {
|
||||
// Prevent default behavior and stop event propagation
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -9,7 +9,7 @@ export const settings = await ReactiveStore.getPluginStorage("ElementHider", {
|
||||
className: string;
|
||||
textContent: string;
|
||||
timestamp: number;
|
||||
}>
|
||||
}>,
|
||||
});
|
||||
|
||||
export const Settings = () => {
|
||||
|
||||
@@ -32,7 +32,7 @@ function generateElementSelector(element: HTMLElement): string {
|
||||
}
|
||||
|
||||
// Priority 2: data-test attribute (very specific for Tidal <3)
|
||||
const dataTest = element.getAttribute('data-test');
|
||||
const dataTest = element.getAttribute("data-test");
|
||||
if (dataTest) {
|
||||
return `[data-test="${dataTest}"]`;
|
||||
}
|
||||
@@ -41,28 +41,43 @@ function generateElementSelector(element: HTMLElement): string {
|
||||
let selector = element.tagName.toLowerCase();
|
||||
|
||||
// Get filtered classes (exclude our temporary classes)
|
||||
const classes = element.className ? element.className.trim().split(/\s+/).filter(cls => {
|
||||
return cls.length > 0 &&
|
||||
!cls.startsWith('element-hider-') &&
|
||||
cls !== 'element-hider-target' &&
|
||||
cls !== 'element-hider-hiding' &&
|
||||
cls !== 'element-hider-hidden';
|
||||
}) : [];
|
||||
const classes = element.className
|
||||
? element.className
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((cls) => {
|
||||
return (
|
||||
cls.length > 0 &&
|
||||
!cls.startsWith("element-hider-") &&
|
||||
cls !== "element-hider-target" &&
|
||||
cls !== "element-hider-hiding" &&
|
||||
cls !== "element-hider-hidden"
|
||||
);
|
||||
})
|
||||
: [];
|
||||
|
||||
// Only use classes if we have them and they're not generic and dumb
|
||||
if (classes.length > 0) {
|
||||
// Use ALL classes to be very specific
|
||||
selector += '.' + classes.join('.');
|
||||
selector += "." + classes.join(".");
|
||||
|
||||
// Add parent context for extra specificity (for when the element is inside another element)
|
||||
const parent = element.parentElement;
|
||||
if (parent && parent.tagName !== 'BODY' && parent.tagName !== 'HTML') {
|
||||
const parentClasses = parent.className ? parent.className.trim().split(/\s+/).filter(cls => {
|
||||
return cls.length > 0 && !cls.startsWith('element-hider-');
|
||||
}) : [];
|
||||
if (parent && parent.tagName !== "BODY" && parent.tagName !== "HTML") {
|
||||
const parentClasses = parent.className
|
||||
? parent.className
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((cls) => {
|
||||
return cls.length > 0 && !cls.startsWith("element-hider-");
|
||||
})
|
||||
: [];
|
||||
|
||||
if (parentClasses.length > 0) {
|
||||
const parentSelector = parent.tagName.toLowerCase() + '.' + parentClasses.slice(0, 2).join('.');
|
||||
const parentSelector =
|
||||
parent.tagName.toLowerCase() +
|
||||
"." +
|
||||
parentClasses.slice(0, 2).join(".");
|
||||
selector = `${parentSelector} > ${selector}`;
|
||||
}
|
||||
}
|
||||
@@ -70,19 +85,29 @@ function generateElementSelector(element: HTMLElement): string {
|
||||
// If no useful classes, use position-based selector with parent context
|
||||
const parent = element.parentElement;
|
||||
if (parent) {
|
||||
const siblings = Array.from(parent.children).filter(child => child.tagName === element.tagName);
|
||||
const siblings = Array.from(parent.children).filter(
|
||||
(child) => child.tagName === element.tagName,
|
||||
);
|
||||
const index = siblings.indexOf(element);
|
||||
if (index >= 0) {
|
||||
selector += `:nth-of-type(${index + 1})`;
|
||||
|
||||
// Add parent context
|
||||
if (parent.tagName !== 'BODY' && parent.tagName !== 'HTML') {
|
||||
const parentClasses = parent.className ? parent.className.trim().split(/\s+/).filter(cls => {
|
||||
return cls.length > 0 && !cls.startsWith('element-hider-');
|
||||
}) : [];
|
||||
if (parent.tagName !== "BODY" && parent.tagName !== "HTML") {
|
||||
const parentClasses = parent.className
|
||||
? parent.className
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((cls) => {
|
||||
return cls.length > 0 && !cls.startsWith("element-hider-");
|
||||
})
|
||||
: [];
|
||||
|
||||
if (parentClasses.length > 0) {
|
||||
const parentSelector = parent.tagName.toLowerCase() + '.' + parentClasses.slice(0, 2).join('.');
|
||||
const parentSelector =
|
||||
parent.tagName.toLowerCase() +
|
||||
"." +
|
||||
parentClasses.slice(0, 2).join(".");
|
||||
selector = `${parentSelector} > ${selector}`;
|
||||
}
|
||||
}
|
||||
@@ -100,14 +125,14 @@ function saveHiddenElement(element: HTMLElement): void {
|
||||
const elementInfo = {
|
||||
selector: selector,
|
||||
tagName: element.tagName,
|
||||
className: element.className || '',
|
||||
textContent: element.textContent?.substring(0, 100) || '',
|
||||
timestamp: Date.now()
|
||||
className: element.className || "",
|
||||
textContent: element.textContent?.substring(0, 100) || "",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// Check if element is already saved
|
||||
const existingIndex = settings.hiddenElements.findIndex(
|
||||
stored => stored.selector === elementInfo.selector
|
||||
(stored) => stored.selector === elementInfo.selector,
|
||||
);
|
||||
|
||||
if (existingIndex === -1) {
|
||||
@@ -122,7 +147,9 @@ function saveHiddenElement(element: HTMLElement): void {
|
||||
// Remove hidden element from persistent storage (for unhiding)
|
||||
function removeSavedElement(element: HTMLElement): void {
|
||||
const selector = generateElementSelector(element);
|
||||
const index = settings.hiddenElements.findIndex(stored => stored.selector === selector);
|
||||
const index = settings.hiddenElements.findIndex(
|
||||
(stored) => stored.selector === selector,
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
settings.hiddenElements.splice(index, 1);
|
||||
@@ -154,14 +181,18 @@ function hideElementDirectly(element: HTMLElement): void {
|
||||
element.classList.add("element-hider-hidden");
|
||||
hiddenElements.add(element);
|
||||
hiddenElementsArray.push(element);
|
||||
trace.log(`Hidden element: ${element.tagName}${element.className ? '.' + element.className.split(' ')[0] : ''}`);
|
||||
trace.log(
|
||||
`Hidden element: ${element.tagName}${element.className ? "." + element.className.split(" ")[0] : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Hide the target element with animation
|
||||
function hideTargetElement(): void {
|
||||
if (!targetElement) return;
|
||||
|
||||
trace.log(`Hiding with animation: ${targetElement.tagName}${targetElement.className ? '.' + targetElement.className.split(' ')[0] : ''}`);
|
||||
trace.log(
|
||||
`Hiding with animation: ${targetElement.tagName}${targetElement.className ? "." + targetElement.className.split(" ")[0] : ""}`,
|
||||
);
|
||||
|
||||
// Add hiding animation class
|
||||
targetElement.classList.add("element-hider-hiding");
|
||||
@@ -175,7 +206,10 @@ function hideTargetElement(): void {
|
||||
// Wait for animation to complete, then hide
|
||||
setTimeout(() => {
|
||||
elementToHide.classList.add("element-hider-hidden");
|
||||
elementToHide.classList.remove("element-hider-hiding", "element-hider-target");
|
||||
elementToHide.classList.remove(
|
||||
"element-hider-hiding",
|
||||
"element-hider-target",
|
||||
);
|
||||
hiddenElements.add(elementToHide);
|
||||
hiddenElementsArray.push(elementToHide);
|
||||
}, 300);
|
||||
@@ -186,10 +220,12 @@ function hideTargetElement(): void {
|
||||
|
||||
// Unhide all elements permanently (remove from storage)
|
||||
function unhideAllElements(): void {
|
||||
trace.log(`Permanently unhiding ${settings.hiddenElements.length} saved elements`);
|
||||
trace.log(
|
||||
`Permanently unhiding ${settings.hiddenElements.length} saved elements`,
|
||||
);
|
||||
|
||||
// Show all currently hidden elements
|
||||
hiddenElementsArray.forEach(element => {
|
||||
hiddenElementsArray.forEach((element) => {
|
||||
if (document.body.contains(element)) {
|
||||
element.classList.remove("element-hider-hidden", "element-hider-hiding");
|
||||
}
|
||||
@@ -205,7 +241,9 @@ function unhideAllElements(): void {
|
||||
function processAllElements(): void {
|
||||
if (settings.hiddenElements.length === 0) return;
|
||||
|
||||
trace.log(`Scanning document for ${settings.hiddenElements.length} stored selectors`);
|
||||
trace.log(
|
||||
`Scanning document for ${settings.hiddenElements.length} stored selectors`,
|
||||
);
|
||||
let hiddenCount = 0;
|
||||
|
||||
// Use querySelectorAll for each stored selector with validation
|
||||
@@ -217,7 +255,9 @@ function processAllElements(): void {
|
||||
|
||||
// Limit to prevent over-hiding (safety check)
|
||||
if (elements.length > 10) {
|
||||
trace.warn(`Selector too broad (${elements.length} matches), skipping: ${storedElement.selector}`);
|
||||
trace.warn(
|
||||
`Selector too broad (${elements.length} matches), skipping: ${storedElement.selector}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -226,7 +266,9 @@ function processAllElements(): void {
|
||||
if (!hiddenElements.has(htmlElement)) {
|
||||
hideElementDirectly(htmlElement);
|
||||
hiddenCount++;
|
||||
trace.log(`Hid element ${elemIndex + 1}/${elements.length} for selector ${index + 1}`);
|
||||
trace.log(
|
||||
`Hid element ${elemIndex + 1}/${elements.length} for selector ${index + 1}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -241,7 +283,7 @@ function processAllElements(): void {
|
||||
|
||||
// Process new elements that are added to the DOM
|
||||
function processNewElements(addedNodes: NodeList): void {
|
||||
addedNodes.forEach(node => {
|
||||
addedNodes.forEach((node) => {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
|
||||
const element = node as HTMLElement;
|
||||
@@ -252,8 +294,8 @@ function processNewElements(addedNodes: NodeList): void {
|
||||
}
|
||||
|
||||
// Check all descendant elements
|
||||
const descendants = element.querySelectorAll('*');
|
||||
descendants.forEach(descendant => {
|
||||
const descendants = element.querySelectorAll("*");
|
||||
descendants.forEach((descendant) => {
|
||||
if (matchesStoredSelector(descendant as HTMLElement)) {
|
||||
hideElementDirectly(descendant as HTMLElement);
|
||||
}
|
||||
@@ -267,7 +309,7 @@ function setupElementObserver(): void {
|
||||
|
||||
elementObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
||||
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
|
||||
processNewElements(mutation.addedNodes);
|
||||
}
|
||||
});
|
||||
@@ -275,7 +317,7 @@ function setupElementObserver(): void {
|
||||
|
||||
elementObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
trace.log(`Set up reactive element observer`);
|
||||
@@ -297,19 +339,19 @@ function setupElementObserver(): void {
|
||||
// Handle highlighting target element
|
||||
function highlightElement(element: HTMLElement): void {
|
||||
// Remove previous highlights
|
||||
document.querySelectorAll('.element-hider-target').forEach(el => {
|
||||
el.classList.remove('element-hider-target');
|
||||
document.querySelectorAll(".element-hider-target").forEach((el) => {
|
||||
el.classList.remove("element-hider-target");
|
||||
});
|
||||
|
||||
// Highlight current element
|
||||
element.classList.add('element-hider-target');
|
||||
element.classList.add("element-hider-target");
|
||||
targetElement = element;
|
||||
}
|
||||
|
||||
// Remove highlight
|
||||
function removeHighlight(): void {
|
||||
if (targetElement) {
|
||||
targetElement.classList.remove('element-hider-target');
|
||||
targetElement.classList.remove("element-hider-target");
|
||||
targetElement = null;
|
||||
}
|
||||
}
|
||||
@@ -321,11 +363,17 @@ let contextMenuTimeout: number | null = null;
|
||||
let waitingForBuiltInMenu = false;
|
||||
|
||||
// Listen for right-click events to capture the target for context menu
|
||||
document.addEventListener('contextmenu', (event: MouseEvent) => {
|
||||
document.addEventListener(
|
||||
"contextmenu",
|
||||
(event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// Don't interfere with native context menus on inputs, textareas, etc.
|
||||
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
|
||||
if (
|
||||
target.tagName === "INPUT" ||
|
||||
target.tagName === "TEXTAREA" ||
|
||||
target.isContentEditable
|
||||
) {
|
||||
currentContextElement = null;
|
||||
return;
|
||||
}
|
||||
@@ -359,10 +407,14 @@ document.addEventListener('contextmenu', (event: MouseEvent) => {
|
||||
}, 150); // Wait 150ms for built-in menu
|
||||
|
||||
// Don't prevent default initially - let Luna try to handle the context menu
|
||||
}, true);
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
// Listen for clicks to close custom menu
|
||||
document.addEventListener('click', (event: MouseEvent) => {
|
||||
document.addEventListener(
|
||||
"click",
|
||||
(event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// If clicking outside our custom menu, close it
|
||||
@@ -370,10 +422,12 @@ document.addEventListener('click', (event: MouseEvent) => {
|
||||
closeCustomMenu();
|
||||
removeHighlight();
|
||||
}
|
||||
}, true);
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
// Handle escape key to close custom menu and remove highlights
|
||||
document.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
document.addEventListener("keydown", (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
if (customMenu) {
|
||||
closeCustomMenu();
|
||||
@@ -464,8 +518,15 @@ const contextMenuObserver = new MutationObserver((mutations) => {
|
||||
const element = node as HTMLElement;
|
||||
|
||||
// Look for Tidal's context menu
|
||||
if (element.matches('[data-test="contextmenu"]') || element.querySelector('[data-test="contextmenu"]')) {
|
||||
const contextMenu = element.matches('[data-test="contextmenu"]') ? element : element.querySelector('[data-test="contextmenu"]') as HTMLElement;
|
||||
if (
|
||||
element.matches('[data-test="contextmenu"]') ||
|
||||
element.querySelector('[data-test="contextmenu"]')
|
||||
) {
|
||||
const contextMenu = element.matches('[data-test="contextmenu"]')
|
||||
? element
|
||||
: (element.querySelector(
|
||||
'[data-test="contextmenu"]',
|
||||
) as HTMLElement);
|
||||
|
||||
if (contextMenu && currentContextElement && waitingForBuiltInMenu) {
|
||||
// Built-in menu appeared, cancel custom menu timeout
|
||||
@@ -485,8 +546,8 @@ const contextMenuObserver = new MutationObserver((mutations) => {
|
||||
// Add our options to the existing context menu
|
||||
function addElementHiderOptions(contextMenu: HTMLElement): void {
|
||||
// Create hide element button
|
||||
const hideButton = document.createElement('button');
|
||||
hideButton.className = 'element-hider-menu-item';
|
||||
const hideButton = document.createElement("button");
|
||||
hideButton.className = "element-hider-menu-item";
|
||||
hideButton.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -503,7 +564,7 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
|
||||
`;
|
||||
hideButton.innerHTML = `Hide This Element`;
|
||||
|
||||
hideButton.addEventListener('click', () => {
|
||||
hideButton.addEventListener("click", () => {
|
||||
if (currentContextElement) {
|
||||
targetElement = currentContextElement;
|
||||
hideTargetElement();
|
||||
@@ -511,37 +572,38 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
|
||||
});
|
||||
|
||||
// Add hover effects for highlighting
|
||||
hideButton.addEventListener('mouseenter', () => {
|
||||
hideButton.style.background = 'var(--wave-color-background-hover, #3a3a3a)';
|
||||
hideButton.addEventListener("mouseenter", () => {
|
||||
hideButton.style.background = "var(--wave-color-background-hover, #3a3a3a)";
|
||||
if (currentContextElement) {
|
||||
highlightElement(currentContextElement);
|
||||
}
|
||||
});
|
||||
|
||||
hideButton.addEventListener('mouseleave', () => {
|
||||
hideButton.style.background = 'transparent';
|
||||
hideButton.addEventListener("mouseleave", () => {
|
||||
hideButton.style.background = "transparent";
|
||||
removeHighlight();
|
||||
});
|
||||
|
||||
// Create unhide all button
|
||||
const unhideAllButton = document.createElement('button');
|
||||
unhideAllButton.className = 'element-hider-menu-item';
|
||||
const unhideAllButton = document.createElement("button");
|
||||
unhideAllButton.className = "element-hider-menu-item";
|
||||
unhideAllButton.style.cssText = hideButton.style.cssText;
|
||||
unhideAllButton.innerHTML = `Unhide All Elements (${hiddenElementsArray.length})`;
|
||||
|
||||
unhideAllButton.addEventListener('click', unhideAllElements);
|
||||
unhideAllButton.addEventListener("click", unhideAllElements);
|
||||
|
||||
// Add hover effects for unhide all button
|
||||
unhideAllButton.addEventListener('mouseenter', () => {
|
||||
unhideAllButton.style.background = 'var(--wave-color-background-hover, #3a3a3a)';
|
||||
unhideAllButton.addEventListener("mouseenter", () => {
|
||||
unhideAllButton.style.background =
|
||||
"var(--wave-color-background-hover, #3a3a3a)";
|
||||
});
|
||||
unhideAllButton.addEventListener('mouseleave', () => {
|
||||
unhideAllButton.style.background = 'transparent';
|
||||
unhideAllButton.addEventListener("mouseleave", () => {
|
||||
unhideAllButton.style.background = "transparent";
|
||||
});
|
||||
|
||||
// Add a separator if the menu has other items
|
||||
if (contextMenu.children.length > 0) {
|
||||
const separator = document.createElement('div');
|
||||
const separator = document.createElement("div");
|
||||
separator.style.cssText = `
|
||||
height: 1px;
|
||||
background: var(--wave-color-border, #444);
|
||||
@@ -558,7 +620,7 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
|
||||
// Start observing for context menus
|
||||
contextMenuObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
// Initialize plugin
|
||||
@@ -578,8 +640,8 @@ function initializePlugin() {
|
||||
}
|
||||
|
||||
// Run initialization when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePlugin);
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initializePlugin);
|
||||
} else {
|
||||
initializePlugin();
|
||||
}
|
||||
|
||||
@@ -57,7 +57,9 @@
|
||||
|
||||
/* Animation for hiding */
|
||||
.element-hider-hiding {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
@@ -9,8 +9,11 @@ export const settings = await ReactiveStore.getPluginStorage("OLEDTheme", {
|
||||
});
|
||||
|
||||
export const Settings = () => {
|
||||
const [qualityColorMatchedSeekBar, setQualityColorMatchedSeekBar] = React.useState(settings.qualityColorMatchedSeekBar);
|
||||
const [oledFriendlyButtons, setOledFriendlyButtons] = React.useState(settings.oledFriendlyButtons);
|
||||
const [qualityColorMatchedSeekBar, setQualityColorMatchedSeekBar] =
|
||||
React.useState(settings.qualityColorMatchedSeekBar);
|
||||
const [oledFriendlyButtons, setOledFriendlyButtons] = React.useState(
|
||||
settings.oledFriendlyButtons,
|
||||
);
|
||||
const [lightMode, setLightMode] = React.useState(settings.lightMode);
|
||||
|
||||
return (
|
||||
@@ -20,8 +23,13 @@ export const Settings = () => {
|
||||
desc="Color the Seek/Progress Bar based on audio quality"
|
||||
checked={qualityColorMatchedSeekBar}
|
||||
onChange={(_, checked) => {
|
||||
console.log("Quality Color Matched Seek Bar:", checked ? "enabled" : "disabled");
|
||||
setQualityColorMatchedSeekBar((settings.qualityColorMatchedSeekBar = checked));
|
||||
console.log(
|
||||
"Quality Color Matched Seek Bar:",
|
||||
checked ? "enabled" : "disabled",
|
||||
);
|
||||
setQualityColorMatchedSeekBar(
|
||||
(settings.qualityColorMatchedSeekBar = checked),
|
||||
);
|
||||
// Update styles immediately when setting changes
|
||||
if ((window as any).updateOLEDThemeStyles) {
|
||||
(window as any).updateOLEDThemeStyles();
|
||||
@@ -33,7 +41,10 @@ export const Settings = () => {
|
||||
desc="Remove button styling from OLED theme to keep buttons with original Tidal appearance"
|
||||
checked={oledFriendlyButtons}
|
||||
onChange={(_, checked) => {
|
||||
console.log("OLED Friendly Buttons:", checked ? "enabled" : "disabled");
|
||||
console.log(
|
||||
"OLED Friendly Buttons:",
|
||||
checked ? "enabled" : "disabled",
|
||||
);
|
||||
setOledFriendlyButtons((settings.oledFriendlyButtons = checked));
|
||||
// Update styles immediately when setting changes
|
||||
if ((window as any).updateOLEDThemeStyles) {
|
||||
|
||||
@@ -26,25 +26,29 @@
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 400;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 500;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 600;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 700;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
[class^="followingButton"],
|
||||
@@ -154,7 +158,6 @@
|
||||
border: 0px none;
|
||||
}
|
||||
|
||||
|
||||
[data-test="feed-sidebar"] {
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -236,7 +239,7 @@ button[data-test="close-now-playing"]:hover {
|
||||
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important;
|
||||
}
|
||||
|
||||
[data-wave-color=textUrl] {
|
||||
[data-wave-color="textUrl"] {
|
||||
color: var(--wave-color-solid-accent-fill);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { LunaUnload, Tracer } from "@luna/core";
|
||||
import { StyleTag, observePromise, PlayState, Quality, type MediaItem } from "@luna/lib";
|
||||
import {
|
||||
StyleTag,
|
||||
observePromise,
|
||||
PlayState,
|
||||
Quality,
|
||||
type MediaItem,
|
||||
} from "@luna/lib";
|
||||
import { settings, Settings } from "./Settings";
|
||||
|
||||
// Import CSS files directly using Luna's file:// syntax - Took me a while to figure out <3
|
||||
@@ -21,7 +27,7 @@ const themeStyleTag = new StyleTag("OLED-Theme", unloads);
|
||||
const QUALITY_COLORS = {
|
||||
MAX: "#FED330", // Max/HiFi
|
||||
HIGH: "#31FFEE", // High
|
||||
LOW: "#FFFFFE" // Low
|
||||
LOW: "#FFFFFE", // Low
|
||||
};
|
||||
|
||||
// Function to get quality color based on audio quality
|
||||
@@ -39,11 +45,16 @@ const getQualityColor = (audioQuality: string): string => {
|
||||
// Function to Reset Seek Bar Color (if setting gets disabled while playing)
|
||||
const resetSeekBarColor = async (): Promise<void> => {
|
||||
try {
|
||||
const progressBarWrapper = await observePromise<HTMLElement>(unloads, `[class^="_progressBarWrapper"]`);
|
||||
const progressBarWrapper = await observePromise<HTMLElement>(
|
||||
unloads,
|
||||
`[class^="_progressBarWrapper"]`,
|
||||
);
|
||||
if (!progressBarWrapper) return;
|
||||
progressBarWrapper.style.removeProperty('color');
|
||||
progressBarWrapper.querySelectorAll('[class*="progress"], [class*="bar"]').forEach(el => {
|
||||
if (el instanceof HTMLElement) el.style.removeProperty('color');
|
||||
progressBarWrapper.style.removeProperty("color");
|
||||
progressBarWrapper
|
||||
.querySelectorAll('[class*="progress"], [class*="bar"]')
|
||||
.forEach((el) => {
|
||||
if (el instanceof HTMLElement) el.style.removeProperty("color");
|
||||
});
|
||||
} catch (error) {
|
||||
trace.msg.err(`Failed to reset seek bar color: ${error}`);
|
||||
@@ -54,14 +65,20 @@ const resetSeekBarColor = async (): Promise<void> => {
|
||||
const applyQualityColors = async (): Promise<void> => {
|
||||
if (!settings.qualityColorMatchedSeekBar) return;
|
||||
try {
|
||||
const progressBarWrapper = await observePromise<HTMLElement>(unloads, `[class^="_progressBarWrapper"]`);
|
||||
const progressBarWrapper = await observePromise<HTMLElement>(
|
||||
unloads,
|
||||
`[class^="_progressBarWrapper"]`,
|
||||
);
|
||||
if (!progressBarWrapper) return;
|
||||
const audioQuality = PlayState.playbackContext?.actualAudioQuality;
|
||||
if (!audioQuality) return;
|
||||
const qualityColor = getQualityColor(audioQuality);
|
||||
progressBarWrapper.style.setProperty('color', qualityColor, 'important');
|
||||
progressBarWrapper.querySelectorAll('[class*="progress"], [class*="bar"]').forEach(el => {
|
||||
if (el instanceof HTMLElement) el.style.setProperty('color', qualityColor, 'important');
|
||||
progressBarWrapper.style.setProperty("color", qualityColor, "important");
|
||||
progressBarWrapper
|
||||
.querySelectorAll('[class*="progress"], [class*="bar"]')
|
||||
.forEach((el) => {
|
||||
if (el instanceof HTMLElement)
|
||||
el.style.setProperty("color", qualityColor, "important");
|
||||
});
|
||||
//trace.msg.log(`Applied quality color ${qualityColor}`);
|
||||
} catch (error) {
|
||||
@@ -101,13 +118,18 @@ const applyThemeStyles = function(): void {
|
||||
selectedStyle = lightTheme;
|
||||
} else {
|
||||
// Dark mode
|
||||
selectedStyle = settings.oledFriendlyButtons ? oledFriendlyTheme : darkTheme;
|
||||
selectedStyle = settings.oledFriendlyButtons
|
||||
? oledFriendlyTheme
|
||||
: darkTheme;
|
||||
}
|
||||
|
||||
// Remove SeekBar coloring if Quality Color Matched Seek Bar is enabled
|
||||
// This allows our manual coloring to take precedence
|
||||
if (settings.qualityColorMatchedSeekBar) {
|
||||
selectedStyle = selectedStyle.replace(/\[class\^="_progressBarWrapper"\]\s*\{[^}]*\}/g, '');
|
||||
selectedStyle = selectedStyle.replace(
|
||||
/\[class\^="_progressBarWrapper"\]\s*\{[^}]*\}/g,
|
||||
"",
|
||||
);
|
||||
setupQualityMonitoring();
|
||||
} else {
|
||||
// If disabling, reset the seek bar color
|
||||
@@ -116,8 +138,6 @@ const applyThemeStyles = function(): void {
|
||||
|
||||
// Apply the selected theme using StyleTag
|
||||
themeStyleTag.css = selectedStyle;
|
||||
|
||||
|
||||
};
|
||||
|
||||
// Make this function available globally so Settings can call it
|
||||
@@ -125,4 +145,3 @@ const applyThemeStyles = function(): void {
|
||||
|
||||
// Apply the OLED theme initially
|
||||
applyThemeStyles();
|
||||
|
||||
|
||||
@@ -26,25 +26,29 @@
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 400;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 500;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 600;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 700;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
[class^="followingButton"],
|
||||
@@ -129,8 +133,6 @@
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
|
||||
|
||||
[class^="_explicitBadge"] {
|
||||
color: var(--wave-color-solid-accent-fill);
|
||||
}
|
||||
@@ -171,7 +173,6 @@
|
||||
border: 0px none;
|
||||
}
|
||||
|
||||
|
||||
[data-test="feed-sidebar"] {
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -278,7 +279,7 @@ button[data-test="close-now-playing"]:hover {
|
||||
border: 1px solid rgba(200, 200, 200, 0.7) !important;
|
||||
}
|
||||
|
||||
[data-wave-color=textUrl] {
|
||||
[data-wave-color="textUrl"] {
|
||||
color: var(--wave-color-solid-accent-fill);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,25 +26,29 @@
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 400;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 500;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 600;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 700;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
[class^="followingButton"],
|
||||
@@ -169,7 +173,7 @@
|
||||
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important;
|
||||
}
|
||||
|
||||
[data-wave-color=textUrl] {
|
||||
[data-wave-color="textUrl"] {
|
||||
color: var(--wave-color-solid-accent-fill);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,23 +15,44 @@ export const settings = await ReactiveStore.getPluginStorage("RadiantLyrics", {
|
||||
backgroundBlur: 80,
|
||||
backgroundBrightness: 40,
|
||||
spinSpeed: 45,
|
||||
settingsAffectNowPlaying: true
|
||||
settingsAffectNowPlaying: true,
|
||||
});
|
||||
|
||||
export const Settings = () => {
|
||||
const [hideUIEnabled, setHideUIEnabled] = React.useState(settings.hideUIEnabled);
|
||||
const [playerBarVisible, setPlayerBarVisible] = React.useState(settings.playerBarVisible);
|
||||
const [lyricsGlowEnabled, setLyricsGlowEnabled] = React.useState(settings.lyricsGlowEnabled);
|
||||
const [hideUIEnabled, setHideUIEnabled] = React.useState(
|
||||
settings.hideUIEnabled,
|
||||
);
|
||||
const [playerBarVisible, setPlayerBarVisible] = React.useState(
|
||||
settings.playerBarVisible,
|
||||
);
|
||||
const [lyricsGlowEnabled, setLyricsGlowEnabled] = React.useState(
|
||||
settings.lyricsGlowEnabled,
|
||||
);
|
||||
const [textGlow, setTextGlow] = React.useState(settings.textGlow);
|
||||
const [spinningCoverEverywhere, setSpinningCoverEverywhere] = React.useState(settings.spinningCoverEverywhere);
|
||||
const [performanceMode, setPerformanceMode] = React.useState(settings.performanceMode);
|
||||
const [spinningArtEnabled, setSpinningArtEnabled] = React.useState(settings.spinningArtEnabled);
|
||||
const [backgroundContrast, setBackgroundContrast] = React.useState(settings.backgroundContrast);
|
||||
const [backgroundBlur, setBackgroundBlur] = React.useState(settings.backgroundBlur);
|
||||
const [backgroundBrightness, setBackgroundBrightness] = React.useState(settings.backgroundBrightness);
|
||||
const [spinningCoverEverywhere, setSpinningCoverEverywhere] = React.useState(
|
||||
settings.spinningCoverEverywhere,
|
||||
);
|
||||
const [performanceMode, setPerformanceMode] = React.useState(
|
||||
settings.performanceMode,
|
||||
);
|
||||
const [spinningArtEnabled, setSpinningArtEnabled] = React.useState(
|
||||
settings.spinningArtEnabled,
|
||||
);
|
||||
const [backgroundContrast, setBackgroundContrast] = React.useState(
|
||||
settings.backgroundContrast,
|
||||
);
|
||||
const [backgroundBlur, setBackgroundBlur] = React.useState(
|
||||
settings.backgroundBlur,
|
||||
);
|
||||
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);
|
||||
const [settingsAffectNowPlaying, setSettingsAffectNowPlaying] =
|
||||
React.useState(settings.settingsAffectNowPlaying);
|
||||
const [trackTitleGlow, setTrackTitleGlow] = React.useState(
|
||||
settings.trackTitleGlow,
|
||||
);
|
||||
|
||||
return (
|
||||
<LunaSettings>
|
||||
@@ -84,8 +105,13 @@ export const Settings = () => {
|
||||
desc="Apply the spinning Cover Art background to the entire app, not just the Now Playing view, Heavily Inspired by Cover-Theme by @Inrixia"
|
||||
checked={spinningCoverEverywhere}
|
||||
onChange={(_, checked: boolean) => {
|
||||
console.log("Spinning Cover Everywhere:", checked ? "enabled" : "disabled");
|
||||
setSpinningCoverEverywhere((settings.spinningCoverEverywhere = checked));
|
||||
console.log(
|
||||
"Spinning Cover Everywhere:",
|
||||
checked ? "enabled" : "disabled",
|
||||
);
|
||||
setSpinningCoverEverywhere(
|
||||
(settings.spinningCoverEverywhere = checked),
|
||||
);
|
||||
// Update styles immediately when setting changes
|
||||
if ((window as any).updateRadiantLyricsGlobalBackground) {
|
||||
(window as any).updateRadiantLyricsGlobalBackground();
|
||||
@@ -113,12 +139,18 @@ export const Settings = () => {
|
||||
desc="Enable the spinning cover art background animation"
|
||||
checked={spinningArtEnabled}
|
||||
onChange={(_, checked: boolean) => {
|
||||
console.log("Background Cover Spin:", checked ? "enabled" : "disabled");
|
||||
console.log(
|
||||
"Background Cover Spin:",
|
||||
checked ? "enabled" : "disabled",
|
||||
);
|
||||
setSpinningArtEnabled((settings.spinningArtEnabled = checked));
|
||||
if ((window as any).updateRadiantLyricsGlobalBackground) {
|
||||
(window as any).updateRadiantLyricsGlobalBackground();
|
||||
}
|
||||
if (settings.settingsAffectNowPlaying && (window as any).updateRadiantLyricsNowPlayingBackground) {
|
||||
if (
|
||||
settings.settingsAffectNowPlaying &&
|
||||
(window as any).updateRadiantLyricsNowPlayingBackground
|
||||
) {
|
||||
(window as any).updateRadiantLyricsNowPlayingBackground();
|
||||
}
|
||||
}}
|
||||
@@ -150,7 +182,10 @@ export const Settings = () => {
|
||||
if ((window as any).updateRadiantLyricsGlobalBackground) {
|
||||
(window as any).updateRadiantLyricsGlobalBackground();
|
||||
}
|
||||
if (settings.settingsAffectNowPlaying && (window as any).updateRadiantLyricsNowPlayingBackground) {
|
||||
if (
|
||||
settings.settingsAffectNowPlaying &&
|
||||
(window as any).updateRadiantLyricsNowPlayingBackground
|
||||
) {
|
||||
(window as any).updateRadiantLyricsNowPlayingBackground();
|
||||
}
|
||||
}}
|
||||
@@ -168,7 +203,10 @@ export const Settings = () => {
|
||||
if ((window as any).updateRadiantLyricsGlobalBackground) {
|
||||
(window as any).updateRadiantLyricsGlobalBackground();
|
||||
}
|
||||
if (settings.settingsAffectNowPlaying && (window as any).updateRadiantLyricsNowPlayingBackground) {
|
||||
if (
|
||||
settings.settingsAffectNowPlaying &&
|
||||
(window as any).updateRadiantLyricsNowPlayingBackground
|
||||
) {
|
||||
(window as any).updateRadiantLyricsNowPlayingBackground();
|
||||
}
|
||||
}}
|
||||
@@ -186,7 +224,10 @@ export const Settings = () => {
|
||||
if ((window as any).updateRadiantLyricsGlobalBackground) {
|
||||
(window as any).updateRadiantLyricsGlobalBackground();
|
||||
}
|
||||
if (settings.settingsAffectNowPlaying && (window as any).updateRadiantLyricsNowPlayingBackground) {
|
||||
if (
|
||||
settings.settingsAffectNowPlaying &&
|
||||
(window as any).updateRadiantLyricsNowPlayingBackground
|
||||
) {
|
||||
(window as any).updateRadiantLyricsNowPlayingBackground();
|
||||
}
|
||||
}}
|
||||
@@ -204,7 +245,10 @@ export const Settings = () => {
|
||||
if ((window as any).updateRadiantLyricsGlobalBackground) {
|
||||
(window as any).updateRadiantLyricsGlobalBackground();
|
||||
}
|
||||
if (settings.settingsAffectNowPlaying && (window as any).updateRadiantLyricsNowPlayingBackground) {
|
||||
if (
|
||||
settings.settingsAffectNowPlaying &&
|
||||
(window as any).updateRadiantLyricsNowPlayingBackground
|
||||
) {
|
||||
(window as any).updateRadiantLyricsNowPlayingBackground();
|
||||
}
|
||||
}}
|
||||
@@ -214,8 +258,13 @@ export const Settings = () => {
|
||||
desc="Apply background settings to Now Playing view"
|
||||
checked={settingsAffectNowPlaying}
|
||||
onChange={(_, checked: boolean) => {
|
||||
console.log("Settings Affect Now Playing:", checked ? "enabled" : "disabled");
|
||||
setSettingsAffectNowPlaying((settings.settingsAffectNowPlaying = checked));
|
||||
console.log(
|
||||
"Settings Affect Now Playing:",
|
||||
checked ? "enabled" : "disabled",
|
||||
);
|
||||
setSettingsAffectNowPlaying(
|
||||
(settings.settingsAffectNowPlaying = checked),
|
||||
);
|
||||
// Update Now Playing background immediately when setting changes
|
||||
if ((window as any).updateRadiantLyricsNowPlayingBackground) {
|
||||
(window as any).updateRadiantLyricsNowPlayingBackground();
|
||||
|
||||
@@ -16,9 +16,6 @@ export { Settings };
|
||||
// clean up resources
|
||||
export const unloads = new Set<LunaUnload>();
|
||||
|
||||
|
||||
|
||||
|
||||
// Marker: Styles and Settings Integration
|
||||
// StyleTag instances for different CSS modules
|
||||
const lyricsStyleTag = new StyleTag("RadiantLyrics-lyrics", unloads);
|
||||
@@ -34,11 +31,10 @@ if (settings.lyricsGlowEnabled) {
|
||||
// Update CSS variables for lyrics text glow based on settings
|
||||
const updateRadiantLyricsTextGlow = function (): void {
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--rl-glow-outer', `${settings.textGlow}px`);
|
||||
root.style.setProperty('--rl-glow-inner', '2px');
|
||||
root.style.setProperty("--rl-glow-outer", `${settings.textGlow}px`);
|
||||
root.style.setProperty("--rl-glow-inner", "2px");
|
||||
};
|
||||
|
||||
|
||||
// Function to update styles when settings change
|
||||
const updateRadiantLyricsStyles = function (): void {
|
||||
if (isHidden) {
|
||||
@@ -58,65 +54,68 @@ const updateRadiantLyricsStyles = function(): void {
|
||||
const lyricsContainer = document.querySelector('[class^="_lyricsContainer"]');
|
||||
if (lyricsContainer && !isHidden) {
|
||||
if (settings.lyricsGlowEnabled) {
|
||||
lyricsContainer.classList.remove('lyrics-glow-disabled');
|
||||
lyricsContainer.classList.remove("lyrics-glow-disabled");
|
||||
lyricsGlowStyleTag.css = lyricsGlow;
|
||||
updateRadiantLyricsTextGlow();
|
||||
} else {
|
||||
lyricsContainer.classList.add('lyrics-glow-disabled');
|
||||
lyricsContainer.classList.add("lyrics-glow-disabled");
|
||||
lyricsGlowStyleTag.remove();
|
||||
}
|
||||
} else if (!isHidden) {
|
||||
observePromise<HTMLElement>(unloads, '[class^="_lyricsContainer"]').then(el => {
|
||||
observePromise<HTMLElement>(unloads, '[class^="_lyricsContainer"]')
|
||||
.then((el) => {
|
||||
if (!el) return;
|
||||
if (settings.lyricsGlowEnabled) {
|
||||
el.classList.remove('lyrics-glow-disabled');
|
||||
el.classList.remove("lyrics-glow-disabled");
|
||||
lyricsGlowStyleTag.css = lyricsGlow;
|
||||
updateRadiantLyricsTextGlow();
|
||||
} else {
|
||||
el.classList.add('lyrics-glow-disabled');
|
||||
el.classList.add("lyrics-glow-disabled");
|
||||
lyricsGlowStyleTag.remove();
|
||||
}
|
||||
}).catch(() => {});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// Track title glow toggle based on settings
|
||||
const trackTitleEl = document.querySelector('[data-test="now-playing-track-title"]') as HTMLElement | null;
|
||||
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');
|
||||
trackTitleEl.classList.remove("rl-title-glow-disabled");
|
||||
} else {
|
||||
trackTitleEl.classList.add('rl-title-glow-disabled');
|
||||
trackTitleEl.classList.add("rl-title-glow-disabled");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// Marker: UI Visibility Control
|
||||
// UI state shared across features
|
||||
var isHidden = false;
|
||||
let unhideButtonAutoFadeTimeout: number | null = null;
|
||||
|
||||
const updateButtonStates = function (): void {
|
||||
const hideButton = document.querySelector('.hide-ui-button') as HTMLElement;
|
||||
const unhideButton = document.querySelector('.unhide-ui-button') as HTMLElement;
|
||||
const hideButton = document.querySelector(".hide-ui-button") as HTMLElement;
|
||||
const unhideButton = document.querySelector(
|
||||
".unhide-ui-button",
|
||||
) as HTMLElement;
|
||||
|
||||
if (hideButton) {
|
||||
if (settings.hideUIEnabled && !isHidden) {
|
||||
hideButton.style.display = 'flex';
|
||||
hideButton.style.display = "flex";
|
||||
// Small delay to ensure display is set first, then fade in
|
||||
setTimeout(() => {
|
||||
hideButton.style.opacity = '1';
|
||||
hideButton.style.visibility = 'visible';
|
||||
hideButton.style.pointerEvents = 'auto';
|
||||
hideButton.style.opacity = "1";
|
||||
hideButton.style.visibility = "visible";
|
||||
hideButton.style.pointerEvents = "auto";
|
||||
}, 50);
|
||||
} else {
|
||||
// Hide UI button immediately when clicked - (couldn't get the fade to work)
|
||||
hideButton.style.display = 'none';
|
||||
hideButton.style.opacity = '0';
|
||||
hideButton.style.visibility = 'hidden';
|
||||
hideButton.style.pointerEvents = 'none';
|
||||
hideButton.style.display = "none";
|
||||
hideButton.style.opacity = "0";
|
||||
hideButton.style.visibility = "hidden";
|
||||
hideButton.style.pointerEvents = "none";
|
||||
}
|
||||
}
|
||||
if (unhideButton) {
|
||||
@@ -127,49 +126,53 @@ const updateButtonStates = function(): void {
|
||||
}
|
||||
|
||||
if (settings.hideUIEnabled && isHidden) {
|
||||
unhideButton.style.display = 'flex';
|
||||
unhideButton.style.display = "flex";
|
||||
// Remove the hide-immediately class and let it fade in smoothly
|
||||
unhideButton.classList.remove('hide-immediately');
|
||||
unhideButton.classList.remove('auto-faded');
|
||||
unhideButton.classList.remove("hide-immediately");
|
||||
unhideButton.classList.remove("auto-faded");
|
||||
// Small delay to ensure display is set first, then fade in - (Works for unhide button.. but not hide button.. because uhh idk)
|
||||
setTimeout(() => {
|
||||
unhideButton.style.opacity = '1';
|
||||
unhideButton.style.visibility = 'visible';
|
||||
unhideButton.style.pointerEvents = 'auto';
|
||||
unhideButton.style.opacity = "1";
|
||||
unhideButton.style.visibility = "visible";
|
||||
unhideButton.style.pointerEvents = "auto";
|
||||
|
||||
// Set up auto-fade after 2 seconds
|
||||
unhideButtonAutoFadeTimeout = window.setTimeout(() => {
|
||||
if (isHidden && unhideButton && !unhideButton.matches(':hover')) {
|
||||
unhideButton.classList.add('auto-faded');
|
||||
if (isHidden && unhideButton && !unhideButton.matches(":hover")) {
|
||||
unhideButton.classList.add("auto-faded");
|
||||
}
|
||||
}, 2000);
|
||||
}, 50);
|
||||
} else {
|
||||
// Smooth fade out for Unhide UI button
|
||||
unhideButton.style.opacity = '0';
|
||||
unhideButton.style.visibility = 'hidden';
|
||||
unhideButton.style.pointerEvents = 'none';
|
||||
unhideButton.classList.remove('auto-faded');
|
||||
unhideButton.style.opacity = "0";
|
||||
unhideButton.style.visibility = "hidden";
|
||||
unhideButton.style.pointerEvents = "none";
|
||||
unhideButton.classList.remove("auto-faded");
|
||||
// Keep display: flex to maintain transitions, then hide after fade
|
||||
setTimeout(() => {
|
||||
if (unhideButton.style.opacity === '0') {
|
||||
unhideButton.style.display = 'none';
|
||||
if (unhideButton.style.opacity === "0") {
|
||||
unhideButton.style.display = "none";
|
||||
}
|
||||
}, 500); // Wait for transition to complete
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Toggle hide/unhide UI
|
||||
const toggleRadiantLyrics = function (): void {
|
||||
const nowPlayingContainer = document.querySelector('[class*="_nowPlayingContainer"]') as HTMLElement;
|
||||
const nowPlayingContainer = document.querySelector(
|
||||
'[class*="_nowPlayingContainer"]',
|
||||
) as HTMLElement;
|
||||
if (isHidden) {
|
||||
const unhideButton = document.querySelector('.unhide-ui-button') as HTMLElement;
|
||||
if (unhideButton) unhideButton.classList.add('hide-immediately');
|
||||
const unhideButton = document.querySelector(
|
||||
".unhide-ui-button",
|
||||
) as HTMLElement;
|
||||
if (unhideButton) unhideButton.classList.add("hide-immediately");
|
||||
isHidden = !isHidden;
|
||||
if (nowPlayingContainer) nowPlayingContainer.classList.remove('radiant-lyrics-ui-hidden');
|
||||
document.body.classList.remove('radiant-lyrics-ui-hidden');
|
||||
if (nowPlayingContainer)
|
||||
nowPlayingContainer.classList.remove("radiant-lyrics-ui-hidden");
|
||||
document.body.classList.remove("radiant-lyrics-ui-hidden");
|
||||
setTimeout(() => {
|
||||
if (!isHidden) {
|
||||
lyricsStyleTag.remove();
|
||||
@@ -183,88 +186,106 @@ const toggleRadiantLyrics = function(): void {
|
||||
updateButtonStates();
|
||||
setTimeout(() => {
|
||||
updateRadiantLyricsStyles();
|
||||
if (nowPlayingContainer) nowPlayingContainer.classList.add('radiant-lyrics-ui-hidden');
|
||||
document.body.classList.add('radiant-lyrics-ui-hidden');
|
||||
if (nowPlayingContainer)
|
||||
nowPlayingContainer.classList.add("radiant-lyrics-ui-hidden");
|
||||
document.body.classList.add("radiant-lyrics-ui-hidden");
|
||||
}, 50);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create buttons
|
||||
const createHideUIButton = function (): void {
|
||||
setTimeout(() => {
|
||||
if (!settings.hideUIEnabled) return;
|
||||
const fullscreenButton = document.querySelector('[data-test="request-fullscreen"]');
|
||||
const fullscreenButton = document.querySelector(
|
||||
'[data-test="request-fullscreen"]',
|
||||
);
|
||||
if (!fullscreenButton || !fullscreenButton.parentElement) {
|
||||
setTimeout(() => createHideUIButton(), 1000);
|
||||
return;
|
||||
}
|
||||
if (document.querySelector('.hide-ui-button')) return;
|
||||
if (document.querySelector(".hide-ui-button")) return;
|
||||
const buttonContainer = fullscreenButton.parentElement;
|
||||
const hideUIButton = document.createElement("button");
|
||||
hideUIButton.className = 'hide-ui-button';
|
||||
hideUIButton.setAttribute('aria-label', 'Hide UI');
|
||||
hideUIButton.setAttribute('title', 'Hide UI');
|
||||
hideUIButton.textContent = 'Hide UI';
|
||||
hideUIButton.style.backgroundColor = 'var(--wave-color-solid-accent-fill)';
|
||||
hideUIButton.style.color = 'black';
|
||||
hideUIButton.style.border = 'none';
|
||||
hideUIButton.style.borderRadius = '12px';
|
||||
hideUIButton.style.height = '40px';
|
||||
hideUIButton.style.padding = '0 12px';
|
||||
hideUIButton.style.marginLeft = '8px';
|
||||
hideUIButton.style.cursor = 'pointer';
|
||||
hideUIButton.style.display = 'flex';
|
||||
hideUIButton.style.alignItems = 'center';
|
||||
hideUIButton.style.justifyContent = 'center';
|
||||
hideUIButton.style.fontSize = '12px';
|
||||
hideUIButton.style.fontWeight = '600';
|
||||
hideUIButton.style.whiteSpace = 'nowrap';
|
||||
hideUIButton.style.transition = 'opacity 0.5s ease-in-out, visibility 0.5s ease-in-out, background-color 0.2s ease-in-out';
|
||||
hideUIButton.style.opacity = '0';
|
||||
hideUIButton.style.visibility = 'hidden';
|
||||
hideUIButton.style.pointerEvents = 'none';
|
||||
hideUIButton.addEventListener('mouseenter', () => { hideUIButton.style.backgroundColor = 'lightgray'; });
|
||||
hideUIButton.addEventListener('mouseleave', () => { hideUIButton.style.backgroundColor = 'var(--wave-color-solid-accent-fill)'; });
|
||||
hideUIButton.className = "hide-ui-button";
|
||||
hideUIButton.setAttribute("aria-label", "Hide UI");
|
||||
hideUIButton.setAttribute("title", "Hide UI");
|
||||
hideUIButton.textContent = "Hide UI";
|
||||
hideUIButton.style.backgroundColor = "var(--wave-color-solid-accent-fill)";
|
||||
hideUIButton.style.color = "black";
|
||||
hideUIButton.style.border = "none";
|
||||
hideUIButton.style.borderRadius = "12px";
|
||||
hideUIButton.style.height = "40px";
|
||||
hideUIButton.style.padding = "0 12px";
|
||||
hideUIButton.style.marginLeft = "8px";
|
||||
hideUIButton.style.cursor = "pointer";
|
||||
hideUIButton.style.display = "flex";
|
||||
hideUIButton.style.alignItems = "center";
|
||||
hideUIButton.style.justifyContent = "center";
|
||||
hideUIButton.style.fontSize = "12px";
|
||||
hideUIButton.style.fontWeight = "600";
|
||||
hideUIButton.style.whiteSpace = "nowrap";
|
||||
hideUIButton.style.transition =
|
||||
"opacity 0.5s ease-in-out, visibility 0.5s ease-in-out, background-color 0.2s ease-in-out";
|
||||
hideUIButton.style.opacity = "0";
|
||||
hideUIButton.style.visibility = "hidden";
|
||||
hideUIButton.style.pointerEvents = "none";
|
||||
hideUIButton.addEventListener("mouseenter", () => {
|
||||
hideUIButton.style.backgroundColor = "lightgray";
|
||||
});
|
||||
hideUIButton.addEventListener("mouseleave", () => {
|
||||
hideUIButton.style.backgroundColor =
|
||||
"var(--wave-color-solid-accent-fill)";
|
||||
});
|
||||
hideUIButton.onclick = toggleRadiantLyrics;
|
||||
buttonContainer.insertBefore(hideUIButton, fullscreenButton.nextSibling);
|
||||
setTimeout(() => {
|
||||
if (settings.hideUIEnabled && !isHidden) {
|
||||
hideUIButton.style.opacity = '1';
|
||||
hideUIButton.style.visibility = 'visible';
|
||||
hideUIButton.style.pointerEvents = 'auto';
|
||||
hideUIButton.style.opacity = "1";
|
||||
hideUIButton.style.visibility = "visible";
|
||||
hideUIButton.style.pointerEvents = "auto";
|
||||
}
|
||||
}, 100);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
|
||||
const createUnhideUIButton = function (): void {
|
||||
setTimeout(() => {
|
||||
if (!settings.hideUIEnabled) return;
|
||||
if (document.querySelector('.unhide-ui-button')) return;
|
||||
const nowPlayingContainer = document.querySelector('[class*="_nowPlayingContainer"]') as HTMLElement;
|
||||
if (document.querySelector(".unhide-ui-button")) return;
|
||||
const nowPlayingContainer = document.querySelector(
|
||||
'[class*="_nowPlayingContainer"]',
|
||||
) as HTMLElement;
|
||||
if (!nowPlayingContainer) {
|
||||
setTimeout(() => createUnhideUIButton(), 1000);
|
||||
return;
|
||||
}
|
||||
const unhideUIButton = document.createElement("button");
|
||||
unhideUIButton.className = 'unhide-ui-button';
|
||||
unhideUIButton.setAttribute('aria-label', 'Unhide UI');
|
||||
unhideUIButton.setAttribute('title', 'Unhide UI');
|
||||
unhideUIButton.textContent = 'Unhide';
|
||||
unhideUIButton.className = "unhide-ui-button";
|
||||
unhideUIButton.setAttribute("aria-label", "Unhide UI");
|
||||
unhideUIButton.setAttribute("title", "Unhide UI");
|
||||
unhideUIButton.textContent = "Unhide";
|
||||
unhideUIButton.style.cssText = `position: absolute; top: 10px; right: 10px; background-color: rgba(255,255,255,0.2); color: white; border: 1px solid rgba(255,255,255,0.3); border-radius: 12px; height: 40px; padding: 0 12px; cursor: pointer; display: none; align-items: center; justify-content: center; transition: all 0.5s ease-in-out; font-size: 12px; font-weight: 600; white-space: nowrap; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); opacity: 0; visibility: hidden; pointer-events: none;`;
|
||||
unhideUIButton.addEventListener('mouseenter', () => { unhideUIButton.style.backgroundColor = 'rgba(255,255,255,0.3)'; unhideUIButton.style.transform = 'scale(1.05)'; unhideUIButton.classList.remove('auto-faded'); });
|
||||
unhideUIButton.addEventListener('mouseleave', () => { unhideUIButton.style.backgroundColor = 'rgba(255,255,255,0.2)'; unhideUIButton.style.transform = 'scale(1)'; window.setTimeout(() => { if (isHidden && !unhideUIButton.matches(':hover')) { unhideUIButton.classList.add('auto-faded'); } }, 2000); });
|
||||
unhideUIButton.addEventListener("mouseenter", () => {
|
||||
unhideUIButton.style.backgroundColor = "rgba(255,255,255,0.3)";
|
||||
unhideUIButton.style.transform = "scale(1.05)";
|
||||
unhideUIButton.classList.remove("auto-faded");
|
||||
});
|
||||
unhideUIButton.addEventListener("mouseleave", () => {
|
||||
unhideUIButton.style.backgroundColor = "rgba(255,255,255,0.2)";
|
||||
unhideUIButton.style.transform = "scale(1)";
|
||||
window.setTimeout(() => {
|
||||
if (isHidden && !unhideUIButton.matches(":hover")) {
|
||||
unhideUIButton.classList.add("auto-faded");
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
unhideUIButton.onclick = toggleRadiantLyrics;
|
||||
nowPlayingContainer.appendChild(unhideUIButton);
|
||||
updateButtonStates();
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// Marker: Background Rendering
|
||||
// Variable setup
|
||||
let globalSpinningBgStyleTag: StyleTag | null = null;
|
||||
@@ -283,7 +304,6 @@ let nowPlayingGradientOverlay: HTMLElement | null = null;
|
||||
let currentNowPlayingCoverSrc: string | null = null;
|
||||
let spinAnimationAdded = false;
|
||||
|
||||
|
||||
// Update Cover Art background for Now Playing and Global
|
||||
function updateCoverArtBackground(method: number = 0): void {
|
||||
if (method === 1) {
|
||||
@@ -293,23 +313,27 @@ function updateCoverArtBackground(method: number = 0): void {
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
let coverArtImageElement = document.querySelector('figure[class*="_albumImage"] > div > div > div > img') as HTMLImageElement;
|
||||
let coverArtImageElement = document.querySelector(
|
||||
'figure[class*="_albumImage"] > div > div > div > img',
|
||||
) as HTMLImageElement;
|
||||
let coverArtImageSrc: string | null = null;
|
||||
|
||||
if (coverArtImageElement) {
|
||||
coverArtImageSrc = coverArtImageElement.src;
|
||||
// Use higher resolution for better quality, but consider performance mode
|
||||
const targetRes = settings.performanceMode ? '640x640' : '1280x1280';
|
||||
const targetRes = settings.performanceMode ? "640x640" : "1280x1280";
|
||||
coverArtImageSrc = coverArtImageSrc.replace(/\d+x\d+/, targetRes);
|
||||
if (coverArtImageElement.src !== coverArtImageSrc) {
|
||||
coverArtImageElement.src = coverArtImageSrc;
|
||||
}
|
||||
} else {
|
||||
const videoElement = document.querySelector('figure[class*="_albumImage"] > div > div > div > video') as HTMLVideoElement;
|
||||
const videoElement = document.querySelector(
|
||||
'figure[class*="_albumImage"] > div > div > div > video',
|
||||
) as HTMLVideoElement;
|
||||
if (videoElement) {
|
||||
coverArtImageSrc = videoElement.getAttribute("poster");
|
||||
if (coverArtImageSrc) {
|
||||
const targetRes = settings.performanceMode ? '640x640' : '1280x1280';
|
||||
const targetRes = settings.performanceMode ? "640x640" : "1280x1280";
|
||||
coverArtImageSrc = coverArtImageSrc.replace(/\d+x\d+/, targetRes);
|
||||
}
|
||||
} else {
|
||||
@@ -326,16 +350,26 @@ function updateCoverArtBackground(method: number = 0): void {
|
||||
}
|
||||
|
||||
// Apply spinning CoverArt background to the Now Playing container - OPTIMIZED
|
||||
const nowPlayingContainerElement = document.querySelector('[class*="_nowPlayingContainer"]') as HTMLElement;
|
||||
const nowPlayingContainerElement = document.querySelector(
|
||||
'[class*="_nowPlayingContainer"]',
|
||||
) as HTMLElement;
|
||||
if (nowPlayingContainerElement) {
|
||||
// Create DOM structure if it doesn't exist (REUSE ELEMENTS)
|
||||
if (!nowPlayingBackgroundContainer || !nowPlayingContainerElement.contains(nowPlayingBackgroundContainer)) {
|
||||
if (
|
||||
!nowPlayingBackgroundContainer ||
|
||||
!nowPlayingContainerElement.contains(nowPlayingBackgroundContainer)
|
||||
) {
|
||||
// Clean up any old elements first
|
||||
nowPlayingContainerElement.querySelectorAll('.now-playing-background-image, .now-playing-black-bg, .now-playing-gradient-overlay').forEach(el => el.remove());
|
||||
nowPlayingContainerElement
|
||||
.querySelectorAll(
|
||||
".now-playing-background-image, .now-playing-black-bg, .now-playing-gradient-overlay",
|
||||
)
|
||||
.forEach((el) => el.remove());
|
||||
|
||||
// Create container
|
||||
nowPlayingBackgroundContainer = document.createElement('div');
|
||||
nowPlayingBackgroundContainer.className = 'now-playing-background-container';
|
||||
nowPlayingBackgroundContainer = document.createElement("div");
|
||||
nowPlayingBackgroundContainer.className =
|
||||
"now-playing-background-container";
|
||||
nowPlayingBackgroundContainer.style.cssText = `
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -349,8 +383,8 @@ function updateCoverArtBackground(method: number = 0): void {
|
||||
nowPlayingContainerElement.appendChild(nowPlayingBackgroundContainer);
|
||||
|
||||
// Create black background layer
|
||||
nowPlayingBlackBg = document.createElement('div');
|
||||
nowPlayingBlackBg.className = 'now-playing-black-bg';
|
||||
nowPlayingBlackBg = document.createElement("div");
|
||||
nowPlayingBlackBg.className = "now-playing-black-bg";
|
||||
nowPlayingBlackBg.style.cssText = `
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -364,8 +398,8 @@ function updateCoverArtBackground(method: number = 0): void {
|
||||
nowPlayingBackgroundContainer.appendChild(nowPlayingBlackBg);
|
||||
|
||||
// Create background image
|
||||
nowPlayingBackgroundImage = document.createElement('img');
|
||||
nowPlayingBackgroundImage.className = 'now-playing-background-image';
|
||||
nowPlayingBackgroundImage = document.createElement("img");
|
||||
nowPlayingBackgroundImage.className = "now-playing-background-image";
|
||||
nowPlayingBackgroundImage.style.cssText = `
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
@@ -378,8 +412,8 @@ function updateCoverArtBackground(method: number = 0): void {
|
||||
nowPlayingBackgroundContainer.appendChild(nowPlayingBackgroundImage);
|
||||
|
||||
// Create gradient overlay
|
||||
nowPlayingGradientOverlay = document.createElement('div');
|
||||
nowPlayingGradientOverlay.className = 'now-playing-gradient-overlay';
|
||||
nowPlayingGradientOverlay = document.createElement("div");
|
||||
nowPlayingGradientOverlay.className = "now-playing-gradient-overlay";
|
||||
nowPlayingGradientOverlay.style.cssText = `
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -394,7 +428,10 @@ function updateCoverArtBackground(method: number = 0): void {
|
||||
}
|
||||
|
||||
// Update image source efficiently
|
||||
if (nowPlayingBackgroundImage && nowPlayingBackgroundImage.src !== coverArtImageSrc) {
|
||||
if (
|
||||
nowPlayingBackgroundImage &&
|
||||
nowPlayingBackgroundImage.src !== coverArtImageSrc
|
||||
) {
|
||||
nowPlayingBackgroundImage.src = coverArtImageSrc;
|
||||
currentNowPlayingCoverSrc = coverArtImageSrc;
|
||||
}
|
||||
@@ -405,33 +442,47 @@ function updateCoverArtBackground(method: number = 0): void {
|
||||
// Performance mode with spinning enabled
|
||||
const blur = Math.min(settings.backgroundBlur, 20);
|
||||
const contrast = Math.min(settings.backgroundContrast, 150);
|
||||
if (nowPlayingBackgroundImage.style.width !== '70vw') nowPlayingBackgroundImage.style.width = '70vw';
|
||||
if (nowPlayingBackgroundImage.style.height !== '70vh') nowPlayingBackgroundImage.style.height = '70vh';
|
||||
if (nowPlayingBackgroundImage.style.width !== "70vw")
|
||||
nowPlayingBackgroundImage.style.width = "70vw";
|
||||
if (nowPlayingBackgroundImage.style.height !== "70vh")
|
||||
nowPlayingBackgroundImage.style.height = "70vh";
|
||||
const filt = `blur(${blur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${contrast}%)`;
|
||||
if (nowPlayingBackgroundImage.style.filter !== filt) nowPlayingBackgroundImage.style.filter = filt;
|
||||
const anim = settings.spinningArtEnabled ? `spin ${settings.spinSpeed}s linear infinite` : 'none';
|
||||
const wc = settings.spinningArtEnabled ? 'transform' : 'auto';
|
||||
if (nowPlayingBackgroundImage.style.animation !== anim) nowPlayingBackgroundImage.style.animation = anim;
|
||||
if (nowPlayingBackgroundImage.style.willChange !== wc) nowPlayingBackgroundImage.style.willChange = wc;
|
||||
nowPlayingBackgroundImage.classList.remove('performance-mode-static');
|
||||
if (nowPlayingBackgroundImage.style.filter !== filt)
|
||||
nowPlayingBackgroundImage.style.filter = filt;
|
||||
const anim = settings.spinningArtEnabled
|
||||
? `spin ${settings.spinSpeed}s linear infinite`
|
||||
: "none";
|
||||
const wc = settings.spinningArtEnabled ? "transform" : "auto";
|
||||
if (nowPlayingBackgroundImage.style.animation !== anim)
|
||||
nowPlayingBackgroundImage.style.animation = anim;
|
||||
if (nowPlayingBackgroundImage.style.willChange !== wc)
|
||||
nowPlayingBackgroundImage.style.willChange = wc;
|
||||
nowPlayingBackgroundImage.classList.remove("performance-mode-static");
|
||||
} else {
|
||||
// Normal mode
|
||||
if (nowPlayingBackgroundImage.style.width !== '90vw') nowPlayingBackgroundImage.style.width = '90vw';
|
||||
if (nowPlayingBackgroundImage.style.height !== '90vh') nowPlayingBackgroundImage.style.height = '90vh';
|
||||
if (nowPlayingBackgroundImage.style.width !== "90vw")
|
||||
nowPlayingBackgroundImage.style.width = "90vw";
|
||||
if (nowPlayingBackgroundImage.style.height !== "90vh")
|
||||
nowPlayingBackgroundImage.style.height = "90vh";
|
||||
const filt = `blur(${settings.backgroundBlur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${settings.backgroundContrast}%)`;
|
||||
if (nowPlayingBackgroundImage.style.filter !== filt) nowPlayingBackgroundImage.style.filter = filt;
|
||||
const anim = settings.spinningArtEnabled ? `spin ${settings.spinSpeed}s linear infinite` : 'none';
|
||||
const wc = settings.spinningArtEnabled ? 'transform' : 'auto';
|
||||
if (nowPlayingBackgroundImage.style.animation !== anim) nowPlayingBackgroundImage.style.animation = anim;
|
||||
if (nowPlayingBackgroundImage.style.willChange !== wc) nowPlayingBackgroundImage.style.willChange = wc;
|
||||
nowPlayingBackgroundImage.classList.remove('performance-mode-static');
|
||||
if (nowPlayingBackgroundImage.style.filter !== filt)
|
||||
nowPlayingBackgroundImage.style.filter = filt;
|
||||
const anim = settings.spinningArtEnabled
|
||||
? `spin ${settings.spinSpeed}s linear infinite`
|
||||
: "none";
|
||||
const wc = settings.spinningArtEnabled ? "transform" : "auto";
|
||||
if (nowPlayingBackgroundImage.style.animation !== anim)
|
||||
nowPlayingBackgroundImage.style.animation = anim;
|
||||
if (nowPlayingBackgroundImage.style.willChange !== wc)
|
||||
nowPlayingBackgroundImage.style.willChange = wc;
|
||||
nowPlayingBackgroundImage.classList.remove("performance-mode-static");
|
||||
}
|
||||
}
|
||||
|
||||
// Add keyframe animation only once
|
||||
if (!spinAnimationAdded) {
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.id = 'spinAnimation';
|
||||
const styleSheet = document.createElement("style");
|
||||
styleSheet.id = "spinAnimation";
|
||||
styleSheet.textContent = `
|
||||
@keyframes spin {
|
||||
from { transform: translate(-50%, -50%) rotate(0deg); }
|
||||
@@ -445,10 +496,11 @@ function updateCoverArtBackground(method: number = 0): void {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Function to apply spinning background to the entire app (cover everywhere)
|
||||
const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => {
|
||||
const appContainer = document.querySelector('[data-test="main"]') as HTMLElement;
|
||||
const appContainer = document.querySelector(
|
||||
'[data-test="main"]',
|
||||
) as HTMLElement;
|
||||
|
||||
if (!settings.spinningCoverEverywhere) {
|
||||
cleanUpGlobalSpinningBackground();
|
||||
@@ -457,7 +509,10 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => {
|
||||
|
||||
// Throttle updates to prevent excessive DOM manipulation
|
||||
const now = Date.now();
|
||||
if (now - lastUpdateTime < getUpdateThrottle() && currentGlobalCoverSrc === coverArtImageSrc) {
|
||||
if (
|
||||
now - lastUpdateTime < getUpdateThrottle() &&
|
||||
currentGlobalCoverSrc === coverArtImageSrc
|
||||
) {
|
||||
return;
|
||||
}
|
||||
lastUpdateTime = now;
|
||||
@@ -465,15 +520,19 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => {
|
||||
|
||||
// Add StyleTag if not present
|
||||
if (!globalSpinningBgStyleTag) {
|
||||
globalSpinningBgStyleTag = new StyleTag("RadiantLyrics-global-spinning-bg", unloads, coverEverywhereCss);
|
||||
globalSpinningBgStyleTag = new StyleTag(
|
||||
"RadiantLyrics-global-spinning-bg",
|
||||
unloads,
|
||||
coverEverywhereCss,
|
||||
);
|
||||
}
|
||||
|
||||
if (!appContainer) return;
|
||||
|
||||
// Create container structure if it doesn't exist (REUSE DOM ELEMENTS)
|
||||
if (!globalBackgroundContainer) {
|
||||
globalBackgroundContainer = document.createElement('div');
|
||||
globalBackgroundContainer.className = 'global-background-container';
|
||||
globalBackgroundContainer = document.createElement("div");
|
||||
globalBackgroundContainer.className = "global-background-container";
|
||||
globalBackgroundContainer.style.cssText = `
|
||||
position: fixed;
|
||||
left: 0;
|
||||
@@ -487,13 +546,13 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => {
|
||||
appContainer.appendChild(globalBackgroundContainer);
|
||||
|
||||
// Create black background layer
|
||||
globalBlackBg = document.createElement('div');
|
||||
globalBlackBg.className = 'global-spinning-black-bg';
|
||||
globalBlackBg = document.createElement("div");
|
||||
globalBlackBg.className = "global-spinning-black-bg";
|
||||
globalBackgroundContainer.appendChild(globalBlackBg);
|
||||
|
||||
// Create image element
|
||||
globalBackgroundImage = document.createElement('img');
|
||||
globalBackgroundImage.className = 'global-spinning-image';
|
||||
globalBackgroundImage = document.createElement("img");
|
||||
globalBackgroundImage.className = "global-spinning-image";
|
||||
globalBackgroundImage.style.cssText = `
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
@@ -517,37 +576,34 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => {
|
||||
// Performance mode optimizations
|
||||
if (settings.performanceMode) {
|
||||
// Performance mode with spinning enabled
|
||||
globalBackgroundImage.style.width = '100vw';
|
||||
globalBackgroundImage.style.height = '100vh';
|
||||
globalBackgroundImage.style.width = "100vw";
|
||||
globalBackgroundImage.style.height = "100vh";
|
||||
globalBackgroundImage.style.filter = `blur(${Math.min(settings.backgroundBlur, 20)}px) brightness(${settings.backgroundBrightness / 100}) contrast(${Math.min(settings.backgroundContrast, 150)}%)`;
|
||||
if (settings.spinningArtEnabled) {
|
||||
globalBackgroundImage.style.animation = `spinGlobal ${settings.spinSpeed}s linear infinite`;
|
||||
globalBackgroundImage.style.willChange = 'transform';
|
||||
globalBackgroundImage.style.willChange = "transform";
|
||||
} else {
|
||||
globalBackgroundImage.style.animation = "none";
|
||||
globalBackgroundImage.style.willChange = "auto";
|
||||
}
|
||||
else {
|
||||
globalBackgroundImage.style.animation = 'none';
|
||||
globalBackgroundImage.style.willChange = 'auto';
|
||||
}
|
||||
globalBackgroundImage.classList.remove('performance-mode-static');
|
||||
globalBackgroundImage.classList.remove("performance-mode-static");
|
||||
} else {
|
||||
// Normal mode
|
||||
globalBackgroundImage.style.width = '150vw';
|
||||
globalBackgroundImage.style.height = '150vh';
|
||||
globalBackgroundImage.style.width = "150vw";
|
||||
globalBackgroundImage.style.height = "150vh";
|
||||
globalBackgroundImage.style.filter = `blur(${settings.backgroundBlur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${settings.backgroundContrast}%)`;
|
||||
if (settings.spinningArtEnabled) {
|
||||
globalBackgroundImage.style.animation = `spinGlobal ${settings.spinSpeed}s linear infinite`;
|
||||
globalBackgroundImage.style.willChange = 'transform';
|
||||
globalBackgroundImage.style.willChange = "transform";
|
||||
} else {
|
||||
globalBackgroundImage.style.animation = "none";
|
||||
globalBackgroundImage.style.willChange = "auto";
|
||||
}
|
||||
else {
|
||||
globalBackgroundImage.style.animation = 'none';
|
||||
globalBackgroundImage.style.willChange = 'auto';
|
||||
}
|
||||
globalBackgroundImage.classList.remove('performance-mode-static');
|
||||
globalBackgroundImage.classList.remove("performance-mode-static");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// cleanup function
|
||||
const cleanUpGlobalSpinningBackground = function (): void {
|
||||
if (globalBackgroundContainer && globalBackgroundContainer.parentNode) {
|
||||
@@ -564,14 +620,13 @@ const cleanUpGlobalSpinningBackground = function(): void {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Function to update global background when settings change
|
||||
const updateRadiantLyricsGlobalBackground = function (): void {
|
||||
// Apply performance mode class to document body
|
||||
if (settings.performanceMode) {
|
||||
document.body.classList.add('performance-mode');
|
||||
document.body.classList.add("performance-mode");
|
||||
} else {
|
||||
document.body.classList.remove('performance-mode');
|
||||
document.body.classList.remove("performance-mode");
|
||||
}
|
||||
|
||||
if (settings.spinningCoverEverywhere) {
|
||||
@@ -582,10 +637,11 @@ const updateRadiantLyricsGlobalBackground = function(): void {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Function to update Now Playing background when settings change
|
||||
const updateRadiantLyricsNowPlayingBackground = function (): void {
|
||||
const nowPlayingBackgroundImages = document.querySelectorAll('.now-playing-background-image');
|
||||
const nowPlayingBackgroundImages = document.querySelectorAll(
|
||||
".now-playing-background-image",
|
||||
);
|
||||
nowPlayingBackgroundImages.forEach((img: Element) => {
|
||||
const imgElement = img as HTMLElement;
|
||||
|
||||
@@ -616,23 +672,21 @@ const updateRadiantLyricsNowPlayingBackground = function(): void {
|
||||
contrast = Math.min(contrast, 150);
|
||||
if (settings.spinningArtEnabled) {
|
||||
imgElement.style.animation = `spin ${spinSpeed}s linear infinite`;
|
||||
imgElement.style.willChange = 'transform';
|
||||
imgElement.style.willChange = "transform";
|
||||
} else {
|
||||
imgElement.style.animation = "none";
|
||||
imgElement.style.willChange = "auto";
|
||||
}
|
||||
else {
|
||||
imgElement.style.animation = 'none';
|
||||
imgElement.style.willChange = 'auto';
|
||||
}
|
||||
imgElement.classList.remove('performance-mode-static');
|
||||
imgElement.classList.remove("performance-mode-static");
|
||||
} else {
|
||||
if (settings.spinningArtEnabled) {
|
||||
imgElement.style.animation = `spin ${spinSpeed}s linear infinite`;
|
||||
imgElement.style.willChange = 'transform';
|
||||
imgElement.style.willChange = "transform";
|
||||
} else {
|
||||
imgElement.style.animation = "none";
|
||||
imgElement.style.willChange = "auto";
|
||||
}
|
||||
else {
|
||||
imgElement.style.animation = 'none';
|
||||
imgElement.style.willChange = 'auto';
|
||||
}
|
||||
imgElement.classList.remove('performance-mode-static');
|
||||
imgElement.classList.remove("performance-mode-static");
|
||||
}
|
||||
|
||||
imgElement.style.filter = `blur(${blur}px) brightness(${brightness / 100}) contrast(${contrast}%)`;
|
||||
@@ -641,15 +695,21 @@ const updateRadiantLyricsNowPlayingBackground = function(): void {
|
||||
|
||||
// Make these functions available globally so Settings can call them
|
||||
(window as any).updateRadiantLyricsStyles = updateRadiantLyricsStyles;
|
||||
(window as any).updateRadiantLyricsGlobalBackground = updateRadiantLyricsGlobalBackground;
|
||||
(window as any).updateRadiantLyricsNowPlayingBackground = updateRadiantLyricsNowPlayingBackground;
|
||||
(window as any).updateRadiantLyricsGlobalBackground =
|
||||
updateRadiantLyricsGlobalBackground;
|
||||
(window as any).updateRadiantLyricsNowPlayingBackground =
|
||||
updateRadiantLyricsNowPlayingBackground;
|
||||
(window as any).updateRadiantLyricsTextGlow = updateRadiantLyricsTextGlow;
|
||||
|
||||
|
||||
const cleanUpDynamicArt = function (): void {
|
||||
// Clean up cached Now Playing elements
|
||||
if (nowPlayingBackgroundContainer && nowPlayingBackgroundContainer.parentNode) {
|
||||
nowPlayingBackgroundContainer.parentNode.removeChild(nowPlayingBackgroundContainer);
|
||||
if (
|
||||
nowPlayingBackgroundContainer &&
|
||||
nowPlayingBackgroundContainer.parentNode
|
||||
) {
|
||||
nowPlayingBackgroundContainer.parentNode.removeChild(
|
||||
nowPlayingBackgroundContainer,
|
||||
);
|
||||
}
|
||||
nowPlayingBackgroundContainer = null;
|
||||
nowPlayingBackgroundImage = null;
|
||||
@@ -658,7 +718,9 @@ const cleanUpDynamicArt = function (): void {
|
||||
currentNowPlayingCoverSrc = null;
|
||||
|
||||
// Clean up any remaining elements (fallback)
|
||||
const nowPlayingBackgroundImages = document.getElementsByClassName("now-playing-background-image");
|
||||
const nowPlayingBackgroundImages = document.getElementsByClassName(
|
||||
"now-playing-background-image",
|
||||
);
|
||||
Array.from(nowPlayingBackgroundImages).forEach((element) => {
|
||||
element.remove();
|
||||
});
|
||||
@@ -667,21 +729,28 @@ const cleanUpDynamicArt = function (): void {
|
||||
cleanUpGlobalSpinningBackground();
|
||||
};
|
||||
|
||||
|
||||
// Reduce work when tab hidden: pause animations; restore on visible
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
const isHiddenDoc = document.hidden;
|
||||
const images = document.querySelectorAll('.global-spinning-image, .now-playing-background-image');
|
||||
images.forEach(img => {
|
||||
const images = document.querySelectorAll(
|
||||
".global-spinning-image, .now-playing-background-image",
|
||||
);
|
||||
images.forEach((img) => {
|
||||
const el = img as HTMLElement;
|
||||
if (isHiddenDoc) {
|
||||
// Pause animation but keep state
|
||||
if (el.style.animationPlayState !== 'paused') el.style.animationPlayState = 'paused';
|
||||
if (el.style.willChange !== 'auto') el.style.willChange = 'auto';
|
||||
if (el.style.animationPlayState !== "paused")
|
||||
el.style.animationPlayState = "paused";
|
||||
if (el.style.willChange !== "auto") el.style.willChange = "auto";
|
||||
} else {
|
||||
if (el.style.animationPlayState !== 'running') el.style.animationPlayState = 'running';
|
||||
if (el.classList.contains('global-spinning-image') || el.classList.contains('now-playing-background-image')) {
|
||||
if (el.style.willChange !== 'transform') el.style.willChange = 'transform';
|
||||
if (el.style.animationPlayState !== "running")
|
||||
el.style.animationPlayState = "running";
|
||||
if (
|
||||
el.classList.contains("global-spinning-image") ||
|
||||
el.classList.contains("now-playing-background-image")
|
||||
) {
|
||||
if (el.style.willChange !== "transform")
|
||||
el.style.willChange = "transform";
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -689,7 +758,7 @@ document.addEventListener('visibilitychange', () => {
|
||||
|
||||
// Apply initial performance mode class
|
||||
if (settings.performanceMode) {
|
||||
document.body.classList.add('performance-mode');
|
||||
document.body.classList.add("performance-mode");
|
||||
}
|
||||
|
||||
// Initialize text glow CSS variables on load
|
||||
@@ -708,18 +777,18 @@ unloads.add(() => {
|
||||
}
|
||||
|
||||
// Clean up our custom buttons
|
||||
const hideButton = document.querySelector('.hide-ui-button');
|
||||
const hideButton = document.querySelector(".hide-ui-button");
|
||||
if (hideButton && hideButton.parentNode) {
|
||||
hideButton.parentNode.removeChild(hideButton);
|
||||
}
|
||||
|
||||
const unhideButton = document.querySelector('.unhide-ui-button');
|
||||
const unhideButton = document.querySelector(".unhide-ui-button");
|
||||
if (unhideButton && unhideButton.parentNode) {
|
||||
unhideButton.parentNode.removeChild(unhideButton);
|
||||
}
|
||||
|
||||
// Clean up spin animations
|
||||
const spinAnimationStyle = document.querySelector('#spinAnimation');
|
||||
const spinAnimationStyle = document.querySelector("#spinAnimation");
|
||||
if (spinAnimationStyle && spinAnimationStyle.parentNode) {
|
||||
spinAnimationStyle.parentNode.removeChild(spinAnimationStyle);
|
||||
}
|
||||
@@ -728,9 +797,6 @@ unloads.add(() => {
|
||||
cleanUpGlobalSpinningBackground();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
// Marker: Observers
|
||||
// Shared observer-based hooks and polling fallbacks
|
||||
const observeTrackChanges = (): void => {
|
||||
@@ -746,7 +812,8 @@ const observeTrackChanges = (): void => {
|
||||
currentInterval = 250;
|
||||
}
|
||||
checkCount++;
|
||||
if (checkCount > 10 && currentInterval < 1000) currentInterval = Math.min(currentInterval * 1.2, 1000);
|
||||
if (checkCount > 10 && currentInterval < 1000)
|
||||
currentInterval = Math.min(currentInterval * 1.2, 1000);
|
||||
};
|
||||
const intervalId = setInterval(() => checkTrackChange(), currentInterval);
|
||||
unloads.add(() => clearInterval(intervalId));
|
||||
@@ -757,41 +824,47 @@ const observeTrackChanges = (): void => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function setupHeaderObserver(): void {
|
||||
const existing = document.querySelector('[data-test="header-container"]');
|
||||
if (existing && !document.querySelector('.hide-ui-button')) createHideUIButton();
|
||||
if (existing && !document.querySelector(".hide-ui-button"))
|
||||
createHideUIButton();
|
||||
observe<HTMLElement>(unloads, '[data-test="header-container"]', () => {
|
||||
if (!document.querySelector('.hide-ui-button')) createHideUIButton();
|
||||
if (!document.querySelector(".hide-ui-button")) createHideUIButton();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function setupNowPlayingObserver(): void {
|
||||
const existing = document.querySelector('[class*="_nowPlayingContainer"]');
|
||||
if (existing && !document.querySelector('.unhide-ui-button')) createUnhideUIButton();
|
||||
if (existing && !document.querySelector(".unhide-ui-button"))
|
||||
createUnhideUIButton();
|
||||
observe<HTMLElement>(unloads, '[class*="_nowPlayingContainer"]', () => {
|
||||
if (!document.querySelector('.unhide-ui-button')) createUnhideUIButton();
|
||||
if (!document.querySelector(".unhide-ui-button")) createUnhideUIButton();
|
||||
});
|
||||
}
|
||||
|
||||
function setupTrackTitleObserver(): void {
|
||||
const trackTitleEl = document.querySelector('[data-test="now-playing-track-title"]') as HTMLElement | null;
|
||||
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');
|
||||
trackTitleEl.classList.remove("rl-title-glow-disabled");
|
||||
} else {
|
||||
trackTitleEl.classList.add('rl-title-glow-disabled');
|
||||
trackTitleEl.classList.add("rl-title-glow-disabled");
|
||||
}
|
||||
}
|
||||
observe<HTMLElement>(unloads, '[data-test="now-playing-track-title"]', (el) => {
|
||||
observe<HTMLElement>(
|
||||
unloads,
|
||||
'[data-test="now-playing-track-title"]',
|
||||
(el) => {
|
||||
if (!el) return;
|
||||
if (settings.trackTitleGlow && settings.lyricsGlowEnabled) {
|
||||
el.classList.remove('rl-title-glow-disabled');
|
||||
el.classList.remove("rl-title-glow-disabled");
|
||||
} else {
|
||||
el.classList.add('rl-title-glow-disabled');
|
||||
el.classList.add("rl-title-glow-disabled");
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize the button creation and observers (non-polling)
|
||||
|
||||
@@ -2,49 +2,63 @@
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 400;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 500;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 600;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
font-weight: 700;
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2");
|
||||
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
|
||||
/* Enhanced lyrics styling with glow effects */
|
||||
[class*="_lyricsText"] > div > span[data-current="true"] {
|
||||
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;
|
||||
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;
|
||||
color: white !important;
|
||||
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-family:
|
||||
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
[class*="_lyricsText"] > div > span {
|
||||
text-shadow: 0 0 0px transparent, 0 0 0px transparent;
|
||||
text-shadow:
|
||||
0 0 0px transparent,
|
||||
0 0 0px transparent;
|
||||
transition-duration: 0.25s;
|
||||
color: rgba(128, 128, 128, 0.4);
|
||||
font-size: 40px;
|
||||
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-family:
|
||||
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
[class*="_lyricsText"] > div > span:hover {
|
||||
text-shadow: 0 0 var(--rl-glow-inner, 2px) lightgray, 0 0 var(--rl-glow-outer, 20px) lightgray !important;
|
||||
text-shadow:
|
||||
0 0 var(--rl-glow-inner, 2px) lightgray,
|
||||
0 0 var(--rl-glow-outer, 20px) lightgray !important;
|
||||
color: lightgray !important;
|
||||
padding-left: 20px;
|
||||
transition-duration: 0.7s;
|
||||
@@ -53,7 +67,9 @@
|
||||
/* Track title glow */
|
||||
[data-test="now-playing-track-title"] {
|
||||
/* 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;
|
||||
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;
|
||||
@@ -67,14 +83,19 @@
|
||||
|
||||
/* Current line transitions */
|
||||
[class*="_lyricsText"] > div > span {
|
||||
transition: text-shadow 0.7s ease-in-out, color 0.7s ease-in-out, padding 0.7s ease-in-out !important;
|
||||
transition:
|
||||
text-shadow 0.7s ease-in-out,
|
||||
color 0.7s ease-in-out,
|
||||
padding 0.7s ease-in-out !important;
|
||||
}
|
||||
|
||||
/* Lyrics container styling */
|
||||
[class^="_lyricsContainer"] > div > div > span {
|
||||
margin-bottom: 2rem;
|
||||
opacity: 1;
|
||||
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-family:
|
||||
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 38px !important;
|
||||
}
|
||||
|
||||
@@ -11,13 +11,15 @@
|
||||
|
||||
/* Tab items stay hidden - no hover functionality (if the song changes and it doesnt have lyrics.. and ya want them back.. you can unhide the UI <3) */
|
||||
|
||||
.radiant-lyrics-ui-hidden [data-test="header-container"]:not(.rl-header-has-hide-btn) {
|
||||
.radiant-lyrics-ui-hidden
|
||||
[data-test="header-container"]:not(.rl-header-has-hide-btn) {
|
||||
opacity: 0 !important;
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
/* Keep header visible if it contains the Hide UI button, but hide its other children */
|
||||
.radiant-lyrics-ui-hidden [data-test="header-container"].rl-header-has-hide-btn > *:not(.hide-ui-button) {
|
||||
.radiant-lyrics-ui-hidden [data-test="header-container"].rl-header-has-hide-btn
|
||||
> *:not(.hide-ui-button) {
|
||||
opacity: 0 !important;
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
}
|
||||
@@ -80,37 +82,64 @@
|
||||
|
||||
/* Hide bottom left controls completely - no hover functionality */
|
||||
/* Exclude heart button in player bar and make sure hidden buttons can't be clicked */
|
||||
.radiant-lyrics-ui-hidden [data-test="add-to-playlist"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [data-test="remove-from-playlist"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [data-test="like-toggle"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [data-test="dislike-toggle"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [data-test="favorite-toggle"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [data-test="heart-button"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [data-test="playlist-add"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
[data-test="add-to-playlist"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
[data-test="remove-from-playlist"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
[data-test="like-toggle"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
[data-test="dislike-toggle"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
[data-test="favorite-toggle"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
[data-test="heart-button"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
[data-test="playlist-add"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [class*="_trackActions"],
|
||||
.radiant-lyrics-ui-hidden [class*="_bottomLeftControls"],
|
||||
.radiant-lyrics-ui-hidden [class*="_actionButtons"],
|
||||
.radiant-lyrics-ui-hidden [class*="_favoriteButton"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
[class*="_favoriteButton"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [class*="_addToPlaylist"],
|
||||
.radiant-lyrics-ui-hidden [class*="_lowerLeft"],
|
||||
.radiant-lyrics-ui-hidden [class*="_bottomActions"],
|
||||
.radiant-lyrics-ui-hidden [class*="_mediaControls"] > div:first-child,
|
||||
.radiant-lyrics-ui-hidden button[title*="Add to"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[title*="Remove from"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[title*="Like"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[title*="Favorite"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[title*="Heart"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[aria-label*="Add to"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[aria-label*="Remove from"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[aria-label*="Like"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[aria-label*="Favorite"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[aria-label*="Heart"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
button[title*="Add to"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
button[title*="Remove from"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
button[title*="Like"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
button[title*="Favorite"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
button[title*="Heart"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
button[aria-label*="Add to"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
button[aria-label*="Remove from"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
button[aria-label*="Like"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
button[aria-label*="Favorite"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden
|
||||
button[aria-label*="Heart"]:not([data-test="footer-player"] *),
|
||||
/* Target buttons in bottom left area specifically - (idk if this is needed.. but it's here) */
|
||||
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] button[class*="_button"]:not(.unhide-ui-button),
|
||||
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] [class*="_iconButton"]:not(.unhide-ui-button),
|
||||
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"]
|
||||
button[class*="_button"]:not(.unhide-ui-button),
|
||||
.radiant-lyrics-ui-hidden
|
||||
[class*="_nowPlayingContainer"]
|
||||
[class*="_iconButton"]:not(.unhide-ui-button),
|
||||
/* Additional catch-all for bottom left area buttons - (idk if this is needed.. but it's here) */
|
||||
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] > div > div:first-child button:not(.unhide-ui-button),
|
||||
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] > div:first-child button:not(.unhide-ui-button) {
|
||||
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"]
|
||||
> div
|
||||
> div:first-child
|
||||
button:not(.unhide-ui-button),
|
||||
.radiant-lyrics-ui-hidden
|
||||
[class*="_nowPlayingContainer"]
|
||||
> div:first-child
|
||||
button:not(.unhide-ui-button) {
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
transition: opacity 0.5s ease-in-out !important;
|
||||
@@ -146,8 +175,13 @@ button[aria-label*="Favorite"],
|
||||
button[aria-label*="Heart"],
|
||||
[class*="_nowPlayingContainer"] button[class*="_button"]:not(.unhide-ui-button),
|
||||
[class*="_nowPlayingContainer"] [class*="_iconButton"]:not(.unhide-ui-button),
|
||||
[class*="_nowPlayingContainer"] > div > div:first-child button:not(.unhide-ui-button),
|
||||
[class*="_nowPlayingContainer"] > div:first-child button:not(.unhide-ui-button) {
|
||||
[class*="_nowPlayingContainer"]
|
||||
> div
|
||||
> div:first-child
|
||||
button:not(.unhide-ui-button),
|
||||
[class*="_nowPlayingContainer"]
|
||||
> div:first-child
|
||||
button:not(.unhide-ui-button) {
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@@ -198,7 +232,11 @@ figure[class*="_albumImage"] {
|
||||
|
||||
/* Hide UI button base styling - just the transition */
|
||||
.hide-ui-button {
|
||||
transition: opacity 0.5s ease-in-out, visibility 0.5s ease-in-out, background-color 0.2s ease-in-out, transform 0.2s ease-in-out !important;
|
||||
transition:
|
||||
opacity 0.5s ease-in-out,
|
||||
visibility 0.5s ease-in-out,
|
||||
background-color 0.2s ease-in-out,
|
||||
transform 0.2s ease-in-out !important;
|
||||
}
|
||||
|
||||
/* Auto-fade styling for unhide button - (Keeps Text Visible, just not full opacity) | Cheers @Zyhn for the idea*/
|
||||
@@ -209,7 +247,12 @@ figure[class*="_albumImage"] {
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
transition: background-color 0.8s ease-in-out, border-color 0.8s ease-in-out, box-shadow 0.8s ease-in-out, backdrop-filter 0.8s ease-in-out, color 0.8s ease-in-out;
|
||||
transition:
|
||||
background-color 0.8s ease-in-out,
|
||||
border-color 0.8s ease-in-out,
|
||||
box-shadow 0.8s ease-in-out,
|
||||
backdrop-filter 0.8s ease-in-out,
|
||||
color 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
/* Restore button styling on hover */
|
||||
@@ -220,5 +263,10 @@ figure[class*="_albumImage"] {
|
||||
backdrop-filter: blur(10px) !important;
|
||||
-webkit-backdrop-filter: blur(10px) !important;
|
||||
color: white !important;
|
||||
transition: background-color 0.3s ease-in-out, border-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out, backdrop-filter 0.3s ease-in-out, color 0.3s ease-in-out;
|
||||
transition:
|
||||
background-color 0.3s ease-in-out,
|
||||
border-color 0.3s ease-in-out,
|
||||
box-shadow 0.3s ease-in-out,
|
||||
backdrop-filter 0.3s ease-in-out,
|
||||
color 0.3s ease-in-out;
|
||||
}
|
||||
Reference in New Issue
Block a user