BIOME Formating

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