Merge pull request #51 from meowarex/dev

Background Scale & Radius
This commit is contained in:
Meow Meow
2025-09-09 21:10:32 +10:00
committed by GitHub
28 changed files with 4095 additions and 3101 deletions
+1 -1
View File
@@ -8,4 +8,4 @@
}, },
"main": "./src/index.ts", "main": "./src/index.ts",
"type": "module" "type": "module"
} }
+421 -340
View File
@@ -1,355 +1,436 @@
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(
barCount: 32, "AudioVisualizer",
barColor: "#ffffff", {
barRounding: true, barCount: 32,
customColors: [] as string[] barColor: "#ffffff",
}); barRounding: true,
customColors: [] as string[],
},
);
export const Settings = () => { export const Settings = () => {
const [barCount, setBarCount] = React.useState(settings.barCount); const [barCount, setBarCount] = React.useState(settings.barCount);
const [barColor, setBarColor] = React.useState(settings.barColor); const [barColor, setBarColor] = React.useState(settings.barColor);
const [barRounding, setBarRounding] = React.useState(settings.barRounding); const [barRounding, setBarRounding] = React.useState(settings.barRounding);
const [showColorPicker, setShowColorPicker] = React.useState(false); const [showColorPicker, setShowColorPicker] = 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 [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);
setTimeout(() => { setTimeout(() => {
setShowColorPicker(false); setShowColorPicker(false);
setShouldRender(false); setShouldRender(false);
}, 200); // Wait for animation to complete because i need to }, 200); // Wait for animation to complete because i need to
}; };
const openColorPicker = () => { const openColorPicker = () => {
setShowColorPicker(true); setShowColorPicker(true);
setShouldRender(true); setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10); setTimeout(() => setIsAnimatingIn(true), 10);
}; };
React.useEffect(() => { React.useEffect(() => {
if (showColorPicker) { if (showColorPicker) {
setShouldRender(true); setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10); setTimeout(() => setIsAnimatingIn(true), 10);
} }
}, [showColorPicker]); }, [showColorPicker]);
// 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) => {
setBarColor(color); setBarColor(color);
setCustomInput(color); setCustomInput(color);
settings.barColor = color; settings.barColor = color;
(window as any).updateAudioVisualizer?.(); (window as any).updateAudioVisualizer?.();
}; };
const addCustomColor = () => { const addCustomColor = () => {
if (customInput) { if (customInput) {
// Trim whitespace and convert to lowercase // Trim whitespace and convert to lowercase
const trimmedInput = customInput.trim().toLowerCase(); const trimmedInput = customInput.trim().toLowerCase();
// Validate hex color format
const hexColorRegex = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
if (hexColorRegex.test(trimmedInput) &&
!colorPresets.includes(trimmedInput) &&
!customColors.includes(trimmedInput)) {
const newCustomColors = [...customColors, trimmedInput];
setCustomColors(newCustomColors);
settings.customColors = newCustomColors;
}
}
};
const removeCustomColor = (colorToRemove: string) => { // Validate hex color format
const newCustomColors = customColors.filter(color => color !== colorToRemove); const hexColorRegex = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
setCustomColors(newCustomColors);
settings.customColors = newCustomColors;
// If the removed color was the selected color (reset to white)
if (barColor === colorToRemove) {
updateColor("#ffffff");
}
};
const allColors = [...colorPresets, ...customColors]; if (
hexColorRegex.test(trimmedInput) &&
!colorPresets.includes(trimmedInput) &&
!customColors.includes(trimmedInput)
) {
const newCustomColors = [...customColors, trimmedInput];
setCustomColors(newCustomColors);
settings.customColors = newCustomColors;
}
}
};
return ( const removeCustomColor = (colorToRemove: string) => {
<LunaSettings> const newCustomColors = customColors.filter(
<LunaSwitchSetting (color) => color !== colorToRemove,
title="Bar Roundness" );
desc="Enable rounded corners on visualizer bars" setCustomColors(newCustomColors);
checked={barRounding} settings.customColors = newCustomColors;
onChange={(_, checked) => {
setBarRounding(checked);
settings.barRounding = checked;
(window as any).updateAudioVisualizer?.();
}}
/>
<LunaNumberSetting
title="Bar Count"
desc="Number of frequency bars to display"
min={8}
max={64}
step={1}
value={barCount}
onNumber={(value: number) => {
setBarCount(value);
settings.barCount = value;
(window as any).updateAudioVisualizer?.();
}}
/>
{/* YUP YOUR EYES WORK... we do be using React code in the settings..*/}
{/* I'm not sure if this is a good idea, but it works & looks amazing */}
{/* Sorry @Inrixia <3 */}
<div style={{
padding: "16px 0",
display: "flex",
justifyContent: "space-between",
alignItems: "center"
}}>
<div>
<div style={{ fontWeight: "normal", fontSize: "1.075rem", marginBottom: "4px" }}>Bar Color</div>
<div style={{ opacity: 0.7, fontSize: "14px" }}>Color of the visualizer bars</div>
</div>
<div style={{ display: "flex", gap: "8px", alignItems: "center", position: "relative" }}>
<button
onClick={() => showColorPicker ? closeColorPicker() : openColorPicker()}
style={{
width: "32px",
height: "32px",
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: "6px",
cursor: "pointer",
background: barColor,
backdropFilter: "blur(10px)",
WebkitBackdropFilter: "blur(10px)",
position: "relative",
overflow: "hidden"
}}
>
<div style={{
position: "absolute",
inset: 0,
background: "rgba(0,0,0,0.1)",
backdropFilter: "blur(2px)"
}} />
</button>
{/* Custom Color Picker Modal */}
{shouldRender && (
<>
{/* Backdrop */}
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0,0,0,0.6)",
zIndex: 1000,
opacity: isAnimatingIn ? 1 : 0,
transition: "opacity 0.2s ease"
}}
onClick={closeColorPicker}
/>
{/* Color Picker Panel */}
<div style={{
position: "fixed",
top: "50%",
left: "50%",
background: "rgba(20,20,20,0.98)",
backdropFilter: "blur(20px)",
WebkitBackdropFilter: "blur(20px)",
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: "16px",
padding: "20px",
minWidth: "320px",
maxWidth: "90vw",
maxHeight: "90vh",
zIndex: 1001,
boxShadow: "0 20px 40px rgba(0,0,0,0.7)",
opacity: isAnimatingIn ? 1 : 0,
transform: isAnimatingIn ? "translate(-50%, -50%) scale(1)" : "translate(-50%, -50%) scale(0.9)",
transition: "all 0.2s ease"
}}>
<div style={{ marginBottom: "12px", color: "#fff", fontWeight: "bold", fontSize: "14px" }}>
Choose Color
</div>
{/* Color Grid */}
<div style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: "8px",
marginBottom: "16px"
}}>
{allColors.map((color, index) => {
const isCustomColor = customColors.includes(color);
const isHovered = hoveredColorIndex === index;
return (
<div
key={index}
style={{
position: "relative",
width: "32px",
height: "32px",
cursor: "pointer"
}}
className="color-item"
onMouseEnter={() => setHoveredColorIndex(index)}
onMouseLeave={() => setHoveredColorIndex(null)}
>
<button
onClick={() => {
updateColor(color);
closeColorPicker();
}}
style={{
width: "100%",
height: "100%",
borderRadius: "6px",
border: barColor === color ? "2px solid #fff" : "1px solid rgba(255,255,255,0.2)",
background: color,
cursor: "pointer",
transition: "all 0.2s ease"
}}
/>
{isCustomColor && (
<button
onClick={(e) => {
e.stopPropagation();
removeCustomColor(color);
}}
style={{
position: "absolute",
top: "-4px",
right: "-4px",
width: "16px",
height: "16px",
borderRadius: "50%",
border: "1px solid rgba(255,255,255,0.8)",
background: "rgba(0,0,0,0.8)",
color: "#fff",
cursor: "pointer",
fontSize: "10px",
display: "flex",
alignItems: "center",
justifyContent: "center",
opacity: isHovered ? 1 : 0,
transition: "opacity 0.2s ease",
zIndex: 10
}}
className="remove-button"
>
×
</button>
)}
</div>
);
})}
</div>
{/* Custom Hex Input */}
<div style={{ marginBottom: "12px" }}>
<div style={{ color: "rgba(255,255,255,0.7)", fontSize: "12px", marginBottom: "6px" }}>
Add Custom Color
</div>
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
<input
type="text"
value={customInput}
onChange={(e) => setCustomInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
updateColor(customInput);
addCustomColor();
}
}}
placeholder="#ffffff"
style={{
flex: 1,
padding: "8px 12px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.1)",
color: "#fff",
fontSize: "14px",
fontFamily: "monospace",
boxSizing: "border-box"
}}
/>
<button
onClick={() => {
updateColor(customInput);
addCustomColor();
}}
style={{
width: "32px",
height: "32px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.3)",
background: "rgba(255,255,255,0.15)",
color: "#fff",
cursor: "pointer",
fontSize: "16px",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.2s ease"
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.25)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.15)";
}}
>
+
</button>
</div>
</div>
{/* Close Button (Done) - Also runs when color chosen*/}
<button
onClick={closeColorPicker}
style={{
width: "100%",
padding: "8px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.1)",
color: "#fff",
cursor: "pointer",
fontSize: "12px"
}}
>
Done
</button>
</div>
</>
)}
</div>
</div>
</LunaSettings> // If the removed color was the selected color (reset to white)
); if (barColor === colorToRemove) {
}; updateColor("#ffffff");
}
};
const allColors = [...colorPresets, ...customColors];
return (
<LunaSettings>
<LunaSwitchSetting
title="Bar Roundness"
desc="Enable rounded corners on visualizer bars"
checked={barRounding}
onChange={(_, checked) => {
setBarRounding(checked);
settings.barRounding = checked;
(window as any).updateAudioVisualizer?.();
}}
/>
<LunaNumberSetting
title="Bar Count"
desc="Number of frequency bars to display"
min={8}
max={64}
step={1}
value={barCount}
onNumber={(value: number) => {
setBarCount(value);
settings.barCount = value;
(window as any).updateAudioVisualizer?.();
}}
/>
{/* YUP YOUR EYES WORK... we do be using React code in the settings..*/}
{/* I'm not sure if this is a good idea, but it works & looks amazing */}
{/* Sorry @Inrixia <3 */}
<div
style={{
padding: "16px 0",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div>
<div
style={{
fontWeight: "normal",
fontSize: "1.075rem",
marginBottom: "4px",
}}
>
Bar Color
</div>
<div style={{ opacity: 0.7, fontSize: "14px" }}>
Color of the visualizer bars
</div>
</div>
<div
style={{
display: "flex",
gap: "8px",
alignItems: "center",
position: "relative",
}}
>
<button
onClick={() =>
showColorPicker ? closeColorPicker() : openColorPicker()
}
style={{
width: "32px",
height: "32px",
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: "6px",
cursor: "pointer",
background: barColor,
backdropFilter: "blur(10px)",
WebkitBackdropFilter: "blur(10px)",
position: "relative",
overflow: "hidden",
}}
>
<div
style={{
position: "absolute",
inset: 0,
background: "rgba(0,0,0,0.1)",
backdropFilter: "blur(2px)",
}}
/>
</button>
{/* Custom Color Picker Modal */}
{shouldRender && (
<>
{/* Backdrop */}
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0,0,0,0.6)",
zIndex: 1000,
opacity: isAnimatingIn ? 1 : 0,
transition: "opacity 0.2s ease",
}}
onClick={closeColorPicker}
/>
{/* Color Picker Panel */}
<div
style={{
position: "fixed",
top: "50%",
left: "50%",
background: "rgba(20,20,20,0.98)",
backdropFilter: "blur(20px)",
WebkitBackdropFilter: "blur(20px)",
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: "16px",
padding: "20px",
minWidth: "320px",
maxWidth: "90vw",
maxHeight: "90vh",
zIndex: 1001,
boxShadow: "0 20px 40px rgba(0,0,0,0.7)",
opacity: isAnimatingIn ? 1 : 0,
transform: isAnimatingIn
? "translate(-50%, -50%) scale(1)"
: "translate(-50%, -50%) scale(0.9)",
transition: "all 0.2s ease",
}}
>
<div
style={{
marginBottom: "12px",
color: "#fff",
fontWeight: "bold",
fontSize: "14px",
}}
>
Choose Color
</div>
{/* Color Grid */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: "8px",
marginBottom: "16px",
}}
>
{allColors.map((color, index) => {
const isCustomColor = customColors.includes(color);
const isHovered = hoveredColorIndex === index;
return (
<div
key={index}
style={{
position: "relative",
width: "32px",
height: "32px",
cursor: "pointer",
}}
className="color-item"
onMouseEnter={() => setHoveredColorIndex(index)}
onMouseLeave={() => setHoveredColorIndex(null)}
>
<button
onClick={() => {
updateColor(color);
closeColorPicker();
}}
style={{
width: "100%",
height: "100%",
borderRadius: "6px",
border:
barColor === color
? "2px solid #fff"
: "1px solid rgba(255,255,255,0.2)",
background: color,
cursor: "pointer",
transition: "all 0.2s ease",
}}
/>
{isCustomColor && (
<button
onClick={(e) => {
e.stopPropagation();
removeCustomColor(color);
}}
style={{
position: "absolute",
top: "-4px",
right: "-4px",
width: "16px",
height: "16px",
borderRadius: "50%",
border: "1px solid rgba(255,255,255,0.8)",
background: "rgba(0,0,0,0.8)",
color: "#fff",
cursor: "pointer",
fontSize: "10px",
display: "flex",
alignItems: "center",
justifyContent: "center",
opacity: isHovered ? 1 : 0,
transition: "opacity 0.2s ease",
zIndex: 10,
}}
className="remove-button"
>
×
</button>
)}
</div>
);
})}
</div>
{/* Custom Hex Input */}
<div style={{ marginBottom: "12px" }}>
<div
style={{
color: "rgba(255,255,255,0.7)",
fontSize: "12px",
marginBottom: "6px",
}}
>
Add Custom Color
</div>
<div
style={{
display: "flex",
gap: "8px",
alignItems: "center",
}}
>
<input
type="text"
value={customInput}
onChange={(e) => setCustomInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
updateColor(customInput);
addCustomColor();
}
}}
placeholder="#ffffff"
style={{
flex: 1,
padding: "8px 12px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.1)",
color: "#fff",
fontSize: "14px",
fontFamily: "monospace",
boxSizing: "border-box",
}}
/>
<button
onClick={() => {
updateColor(customInput);
addCustomColor();
}}
style={{
width: "32px",
height: "32px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.3)",
background: "rgba(255,255,255,0.15)",
color: "#fff",
cursor: "pointer",
fontSize: "16px",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.2s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background =
"rgba(255,255,255,0.25)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background =
"rgba(255,255,255,0.15)";
}}
>
+
</button>
</div>
</div>
{/* Close Button (Done) - Also runs when color chosen*/}
<button
onClick={closeColorPicker}
style={{
width: "100%",
padding: "8px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.1)",
color: "#fff",
cursor: "pointer",
fontSize: "12px",
}}
>
Done
</button>
</div>
</>
)}
</div>
</div>
</LunaSettings>
);
};
+369 -340
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; }, },
sensitivity: 1.5, get color() {
smoothing: 0.8, return settings.barColor;
visualizerType: 'bars' as 'bars' | 'waveform' | 'circular' },
get barRounding() {
return settings.barRounding;
},
sensitivity: 1.5,
smoothing: 0.8,
visualizerType: "bars" as "bars" | "waveform" | "circular",
}; };
// Clean up resources // Clean up resources
@@ -49,128 +56,135 @@ let canvasContext: CanvasRenderingContext2D | null = null;
// Find the audio element - this is a bit of a hack but it works // Find the audio element - this is a bit of a hack but it works
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 (
return element; element &&
} (element.tagName === "AUDIO" || element.tagName === "VIDEO")
} ) {
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) {
return audioEl; return audioEl;
} }
} }
return null; return null;
}; };
// Initialize audio visualization // Initialize audio visualization
const initializeAudioVisualizer = async (): Promise<void> => { const initializeAudioVisualizer = async (): Promise<void> => {
try { try {
// Find the audio element // Find the audio element
const audioElement = findAudioElement(); const audioElement = findAudioElement();
if (!audioElement) { if (!audioElement) {
return; return;
} }
// create audio context // create audio context
if (!audioContext) { if (!audioContext) {
audioContext = new AudioContext(); audioContext = new AudioContext();
log("Created AudioContext"); log("Created AudioContext");
} }
// create analyser
if (!analyser) {
analyser = audioContext.createAnalyser();
analyser.fftSize = 512; // Fixed power of 2 that provides enough frequency bins
analyser.smoothingTimeConstant = config.smoothing;
dataArray = new Uint8Array(analyser.frequencyBinCount);
log("Created AnalyserNode");
}
// attempt audio connection if not already connected
if (!isSourceConnected && audioElement !== currentAudioElement) {
try {
// Create audio source - this might fail if already connected elsewhere
audioSource = audioContext.createMediaElementSource(audioElement);
audioSource.connect(analyser);
// CRITICAL: connect back to destination for audio output (otherwise no sound)
analyser.connect(audioContext.destination);
currentAudioElement = audioElement;
isSourceConnected = true;
log("Connected to audio stream with output");
} catch (error) {
// Audio is connected elsewhere - that's fine, we just can't visualize
if (error instanceof Error && error.message.includes('already connected')) {
log("Audio already connected elsewhere - skipping visualization");
}
return;
}
}
// Resume context only if needed and don't wait for it // create analyser
// (otherwise it will wait for the audio to start playing) if (!analyser) {
if (audioContext.state === 'suspended') { analyser = audioContext.createAnalyser();
audioContext.resume().catch(() => {}); // Fire and forget analyser.fftSize = 512; // Fixed power of 2 that provides enough frequency bins
} analyser.smoothingTimeConstant = config.smoothing;
dataArray = new Uint8Array(analyser.frequencyBinCount);
log("Created AnalyserNode");
}
// Create UI only if it doesn't exist // attempt audio connection if not already connected
if (!visualizerContainer) { if (!isSourceConnected && audioElement !== currentAudioElement) {
createVisualizerUI(); try {
} // Create audio source - this might fail if already connected elsewhere
audioSource = audioContext.createMediaElementSource(audioElement);
// Start animation only if not already running audioSource.connect(analyser);
if (!animationId) { // CRITICAL: connect back to destination for audio output (otherwise no sound)
animate(); analyser.connect(audioContext.destination);
}
currentAudioElement = audioElement;
} catch (err) { isSourceConnected = true;
// log errors log("Connected to audio stream with output");
console.error(err); } catch (error) {
} // Audio is connected elsewhere - that's fine, we just can't visualize
if (
error instanceof Error &&
error.message.includes("already connected")
) {
log("Audio already connected elsewhere - skipping visualization");
}
return;
}
}
// Resume context only if needed and don't wait for it
// (otherwise it will wait for the audio to start playing)
if (audioContext.state === "suspended") {
audioContext.resume().catch(() => {}); // Fire and forget
}
// Create UI only if it doesn't exist
if (!visualizerContainer) {
createVisualizerUI();
}
// Start animation only if not already running
if (!animationId) {
animate();
}
} catch (err) {
// log errors
console.error(err);
}
}; };
// Create the visualizer UI container and canvas // Create the visualizer UI container and canvas
const createVisualizerUI = (): void => { const createVisualizerUI = (): void => {
// Remove existing visualizer if it exists // Remove existing visualizer if it exists
removeVisualizerUI(); removeVisualizerUI();
if (!config.enabled) return;
// Find the search bar if (!config.enabled) return;
const searchField = document.querySelector('input[class*="_searchField"]') as HTMLInputElement;
if (!searchField) {
warn("Search field not found");
return;
}
const searchContainer = searchField.parentElement; // Find the search bar
if (!searchContainer) { const searchField = document.querySelector(
warn("Search container not found"); 'input[class*="_searchField"]',
return; ) as HTMLInputElement;
} if (!searchField) {
warn("Search field not found");
return;
}
// Create visualizer container const searchContainer = searchField.parentElement;
visualizerContainer = document.createElement('div'); if (!searchContainer) {
visualizerContainer.id = 'audio-visualizer-container'; warn("Search container not found");
visualizerContainer.style.cssText = ` return;
}
// Create visualizer container
visualizerContainer = document.createElement("div");
visualizerContainer.id = "audio-visualizer-container";
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;
@@ -178,154 +192,168 @@ const createVisualizerUI = (): void => {
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
`; `;
// 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 = `
width: ${config.width}px; width: ${config.width}px;
height: ${config.height}px; height: ${config.height}px;
border-radius: 4px; border-radius: 4px;
`; `;
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(
} else { visualizerContainer,
searchContainer.parentElement?.insertBefore(visualizerContainer, searchContainer.nextSibling); searchContainer,
} );
} else {
searchContainer.parentElement?.insertBefore(
visualizerContainer,
searchContainer.nextSibling,
);
}
}; };
// Remove visualizer UI // Remove visualizer UI
const removeVisualizerUI = (): void => { const removeVisualizerUI = (): void => {
if (visualizerContainer) { if (visualizerContainer) {
visualizerContainer.remove(); visualizerContainer.remove();
visualizerContainer = null; visualizerContainer = null;
canvas = null; canvas = null;
canvasContext = null; canvasContext = null;
} }
}; };
// Animation loop for rendering visualizer // Animation loop for rendering visualizer
const animate = (): void => { const animate = (): void => {
if (!canvasContext || !canvas) { if (!canvasContext || !canvas) {
animationId = null; animationId = null;
return; return;
} }
// Update canvas color in case it changed // Update canvas color in case it changed
canvasContext.fillStyle = config.color; canvasContext.fillStyle = config.color;
canvasContext.strokeStyle = config.color; canvasContext.strokeStyle = config.color;
// Check if we have real audio data - this might not be needed but its a good idea // Check if we have real audio data - this might not be needed but its a good idea
let hasRealAudio = false; let hasRealAudio = false;
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 =
hasRealAudio = avgVolume > 5; // Threshold for detecting actual audio dataArray.reduce((sum, val) => sum + val, 0) / dataArray.length;
} hasRealAudio = avgVolume > 5; // Threshold for detecting actual audio
}
// Clear canvas // Clear canvas
canvasContext.clearRect(0, 0, canvas.width, canvas.height); canvasContext.clearRect(0, 0, canvas.width, canvas.height);
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;
} }
} else { } else {
// Draw cool scrolling wave effect when no audio // Draw cool scrolling wave effect when no audio
drawScrollingWave(); drawScrollingWave();
} }
animationId = requestAnimationFrame(animate); animationId = requestAnimationFrame(animate);
}; };
// Global wave animation state // Global wave animation state
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.beginPath(); ctx: CanvasRenderingContext2D,
ctx.roundRect(x, y, width, height, radius); x: number,
ctx.fill(); y: number,
width: number,
height: number,
radius: number,
): void => {
ctx.beginPath();
ctx.roundRect(x, y, width, height, radius);
ctx.fill();
}; };
// Draw scrolling wave effect when no audio is detected // Draw scrolling wave effect when no audio is detected
const drawScrollingWave = (): void => { const drawScrollingWave = (): void => {
if (!canvasContext || !canvas) return; if (!canvasContext || !canvas) return;
waveTime += 0.05; // Speed of wave animation waveTime += 0.05; // Speed of wave animation
const barCount = config.barCount; const barCount = config.barCount;
const barWidth = canvas.width / barCount; const barWidth = canvas.width / barCount;
const maxHeight = canvas.height * 0.6; const maxHeight = canvas.height * 0.6;
canvasContext.fillStyle = config.color; canvasContext.fillStyle = config.color;
for (let i = 0; i < barCount; i++) { for (let i = 0; i < barCount; i++) {
// Create a sine wave that scrolls back and forth // Create a sine wave that scrolls back and forth
const x = i / barCount; const x = i / barCount;
const wave1 = Math.sin(x * Math.PI * 2 + waveTime) * 0.3; const wave1 = Math.sin(x * Math.PI * 2 + waveTime) * 0.3;
const wave2 = Math.sin(x * Math.PI * 4 + waveTime * 1.3) * 0.2; const wave2 = Math.sin(x * Math.PI * 4 + waveTime * 1.3) * 0.2;
const wave3 = Math.sin(x * Math.PI * 6 + waveTime * 0.7) * 0.1; const wave3 = Math.sin(x * Math.PI * 6 + waveTime * 0.7) * 0.1;
// Combine waves for complex pattern // Combine waves for complex pattern
const combinedWave = (wave1 + wave2 + wave3 + 1) / 2; // Normalize to 0-1 const combinedWave = (wave1 + wave2 + wave3 + 1) / 2; // Normalize to 0-1
// Add a traveling wave effect // Add a traveling wave effect
const travelWave = Math.sin(x * Math.PI * 3 - waveTime * 2) * 0.5 + 0.5; const travelWave = Math.sin(x * Math.PI * 3 - waveTime * 2) * 0.5 + 0.5;
// Final height calculation // Final height calculation
const barHeight = maxHeight * combinedWave * travelWave * 0.8 + 2; // Minimum height of 2px const barHeight = maxHeight * combinedWave * travelWave * 0.8 + 2; // Minimum height of 2px
const xPos = i * barWidth; const xPos = i * barWidth;
const yPos = (canvas.height - barHeight) / 2; const yPos = (canvas.height - barHeight) / 2;
// Draw rounded or square bars based on setting // Draw rounded or square bars based on setting
if (config.barRounding) { if (config.barRounding) {
drawRoundedRect(canvasContext, xPos, yPos, barWidth - 1, barHeight, 2); drawRoundedRect(canvasContext, xPos, yPos, barWidth - 1, barHeight, 2);
} else { } else {
canvasContext.fillRect(xPos, yPos, barWidth - 1, barHeight); canvasContext.fillRect(xPos, yPos, barWidth - 1, barHeight);
} }
} }
}; };
// Draw frequency bars - default // Draw frequency bars - default
const drawBars = (): void => { const drawBars = (): void => {
if (!canvasContext || !dataArray || !canvas) return; if (!canvasContext || !dataArray || !canvas) return;
const barWidth = canvas.width / config.barCount; const barWidth = canvas.width / config.barCount;
const heightScale = canvas.height / 255; const heightScale = canvas.height / 255;
canvasContext.fillStyle = config.color; canvasContext.fillStyle = config.color;
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;
// Draw rounded or square bars based on setting // Draw rounded or square bars based on setting
if (config.barRounding) { if (config.barRounding) {
drawRoundedRect(canvasContext, x, y, barWidth - 1, barHeight, 2); drawRoundedRect(canvasContext, x, y, barWidth - 1, barHeight, 2);
} else { } else {
canvasContext.fillRect(x, y, barWidth - 1, barHeight); canvasContext.fillRect(x, y, barWidth - 1, barHeight);
} }
} }
}; };
// Draw waveform visualization - NOT IMPLEMENTED YET // Draw waveform visualization - NOT IMPLEMENTED YET
@@ -342,10 +370,10 @@ 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 amplitude = (dataArray[dataIndex] - 128) * config.sensitivity * amplitudeScale; // const amplitude = (dataArray[dataIndex] - 128) * config.sensitivity * amplitudeScale;
// const x = (i / config.barCount) * canvas.width; // const x = (i / config.barCount) * canvas.width;
// const y = centerY + amplitude; // const y = centerY + amplitude;
// if (i === 0) { // if (i === 0) {
// canvasContext.moveTo(x, y); // canvasContext.moveTo(x, y);
// } else { // } else {
@@ -370,13 +398,13 @@ 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 amplitude = (dataArray[dataIndex] * config.sensitivity) / 255; // const amplitude = (dataArray[dataIndex] * config.sensitivity) / 255;
// const angle = (i / config.barCount) * Math.PI * 2; // const angle = (i / config.barCount) * Math.PI * 2;
// const startX = centerX + Math.cos(angle) * radius * 0.7; // const startX = centerX + Math.cos(angle) * radius * 0.7;
// const startY = centerY + Math.sin(angle) * radius * 0.7; // const startY = centerY + Math.sin(angle) * radius * 0.7;
// const endX = centerX + Math.cos(angle) * radius * (0.7 + amplitude * 0.3); // const endX = centerX + Math.cos(angle) * radius * (0.7 + amplitude * 0.3);
// const endY = centerY + Math.sin(angle) * radius * (0.7 + amplitude * 0.3); // const endY = centerY + Math.sin(angle) * radius * (0.7 + amplitude * 0.3);
// canvasContext.beginPath(); // canvasContext.beginPath();
// canvasContext.moveTo(startX, startY); // canvasContext.moveTo(startX, startY);
// canvasContext.lineTo(endX, endY); // canvasContext.lineTo(endX, endY);
@@ -386,22 +414,22 @@ const drawBars = (): void => {
// Update visualizer settings // Update visualizer settings
const updateAudioVisualizer = (): void => { const updateAudioVisualizer = (): void => {
if (analyser) { if (analyser) {
// use a fixed size that provides enough frequency bins // use a fixed size that provides enough frequency bins
analyser.fftSize = 512; // Fixed power of 2 - important analyser.fftSize = 512; // Fixed power of 2 - important
analyser.smoothingTimeConstant = config.smoothing; analyser.smoothingTimeConstant = config.smoothing;
dataArray = new Uint8Array(analyser.frequencyBinCount); dataArray = new Uint8Array(analyser.frequencyBinCount);
} }
if (canvas) { if (canvas) {
canvas.width = config.width; canvas.width = config.width;
canvas.height = config.height; canvas.height = config.height;
canvas.style.width = `${config.width}px`; canvas.style.width = `${config.width}px`;
canvas.style.height = `${config.height}px`; canvas.style.height = `${config.height}px`;
} }
// Recreate UI if position changed // Recreate UI if position changed
createVisualizerUI(); createVisualizerUI();
}; };
// Make updateAudioVisualizer available globally for settings // Make updateAudioVisualizer available globally for settings
@@ -409,120 +437,121 @@ const updateAudioVisualizer = (): void => {
// Clean up function // Clean up function
const cleanupAudioVisualizer = (): void => { const cleanupAudioVisualizer = (): void => {
// stop animation and hide UI - don't touch audio connections (otherwise it will reconnect) // stop animation and hide UI - don't touch audio connections (otherwise it will reconnect)
if (animationId) { if (animationId) {
cancelAnimationFrame(animationId); cancelAnimationFrame(animationId);
animationId = null; animationId = null;
} }
removeVisualizerUI(); removeVisualizerUI();
// i was killing audio connections - But it was reconnecting and being a pain // i was killing audio connections - But it was reconnecting and being a pain
// so i just left it alone - it works fine // so i just left it alone - it works fine
}; };
// Initialize when DOM is ready and track is playing // Initialize when DOM is ready and track is playing
const observePlayState = (): void => { const observePlayState = (): void => {
let hasTriedInitialization = false; let hasTriedInitialization = false;
let checkCount = 0; let checkCount = 0;
const checkAndInitialize = () => {
checkCount++;
// Only try to initialize once when music starts playing
if (PlayState.playing && !hasTriedInitialization) {
hasTriedInitialization = true;
log("Initializing audio visualizer...");
// Initialize immediately - no delay (after audio starts playing ofc)
initializeAudioVisualizer().then(() => {
if (audioContext && analyser) {
log("Audio visualizer ready!");
} else {
hasTriedInitialization = false; // Allow retry if failed
}
});
} else if (!PlayState.playing && hasTriedInitialization) {
// Reset try flag when music stops so it can try again next time (otherwise it explode)
hasTriedInitialization = false;
}
// Keep animation running regardless of play state
if (!animationId) {
animate();
}
};
// Start with fast checking, then slow down const checkAndInitialize = () => {
const fastInterval = setInterval(() => { checkCount++;
checkAndInitialize();
if (checkCount > 10) { // After 10 quick checks, switch to slower // Only try to initialize once when music starts playing
clearInterval(fastInterval); if (PlayState.playing && !hasTriedInitialization) {
const slowInterval = setInterval(checkAndInitialize, 2000); hasTriedInitialization = true;
unloads.add(() => clearInterval(slowInterval)); log("Initializing audio visualizer...");
}
}, 200); // Check every 200ms initially // Initialize immediately - no delay (after audio starts playing ofc)
initializeAudioVisualizer().then(() => {
unloads.add(() => clearInterval(fastInterval)); if (audioContext && analyser) {
log("Audio visualizer ready!");
// Immediate first check } else {
checkAndInitialize(); hasTriedInitialization = false; // Allow retry if failed
}
});
} else if (!PlayState.playing && hasTriedInitialization) {
// Reset try flag when music stops so it can try again next time (otherwise it explode)
hasTriedInitialization = false;
}
// Keep animation running regardless of play state
if (!animationId) {
animate();
}
};
// Start with fast checking, then slow down
const fastInterval = setInterval(() => {
checkAndInitialize();
if (checkCount > 10) {
// After 10 quick checks, switch to slower
clearInterval(fastInterval);
const slowInterval = setInterval(checkAndInitialize, 2000);
unloads.add(() => clearInterval(slowInterval));
}
}, 200); // Check every 200ms initially
unloads.add(() => clearInterval(fastInterval));
// Immediate first check
checkAndInitialize();
}; };
// Initialize the plugin // Initialize the plugin
const initialize = (): void => { const initialize = (): void => {
log("Audio Visualizer plugin initializing..."); log("Audio Visualizer plugin initializing...");
// Start immediately - DOM should be ready by plugin load // Start immediately - DOM should be ready by plugin load
setTimeout(() => { setTimeout(() => {
log("Starting visualizer..."); log("Starting visualizer...");
// Create UI immediately so wave effect shows // Create UI immediately so wave effect shows
createVisualizerUI(); createVisualizerUI();
// Start animation loop immediately // Start animation loop immediately
animate(); animate();
// Also observe play state for audio detection // Also observe play state for audio detection
observePlayState(); observePlayState();
}, 100); // Minimal delay to ensure DOM is ready }, 100); // Minimal delay to ensure DOM is ready
}; };
// Complete cleanup function for plugin unload // Complete cleanup function for plugin unload
const completeCleanup = (): void => { const completeCleanup = (): void => {
log("Complete cleanup - plugin unloading"); log("Complete cleanup - plugin unloading");
if (animationId) { if (animationId) {
cancelAnimationFrame(animationId); cancelAnimationFrame(animationId);
animationId = null; animationId = null;
} }
removeVisualizerUI(); removeVisualizerUI();
// Fully disconnect and reset everything // Fully disconnect and reset everything
if (audioSource) { if (audioSource) {
try { try {
audioSource.disconnect(); audioSource.disconnect();
log("Disconnected audio source completely"); log("Disconnected audio source completely");
} catch (e) { } catch (e) {
log("Audio source already disconnected"); log("Audio source already disconnected");
} }
} }
// 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");
} }
// Reset all references // Reset all references
audioContext = null; audioContext = null;
analyser = null; analyser = null;
audioSource = null; audioSource = null;
dataArray = null; dataArray = null;
currentAudioElement = null; currentAudioElement = null;
isSourceConnected = false; isSourceConnected = false;
}; };
// Register cleanup // Register cleanup
unloads.add(completeCleanup); unloads.add(completeCleanup);
// Start initialization // Start initialization
initialize(); initialize();
+28 -28
View File
@@ -1,56 +1,56 @@
/* Audio Visualizer CSS - Only applies to the Visualizer */ /* Audio Visualizer CSS - Only applies to the Visualizer */
#audio-visualizer-container { #audio-visualizer-container {
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
} }
#audio-visualizer-container:hover { #audio-visualizer-container:hover {
transform: scale(1.02); transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
} }
#audio-visualizer-container canvas { #audio-visualizer-container canvas {
display: block; display: block;
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
} }
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
#audio-visualizer-container { #audio-visualizer-container {
margin: 4px; margin: 4px;
padding: 2px; padding: 2px;
} }
#audio-visualizer-container canvas { #audio-visualizer-container canvas {
max-width: 150px; max-width: 150px;
max-height: 30px; max-height: 30px;
} }
} }
/* Where to put the thingy */ /* Where to put the thingy */
[class*="_searchField"] { [class*="_searchField"] {
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
} }
/* Shadow when active - doesnt seem to only apply when active but thats better */ /* Shadow when active - doesnt seem to only apply when active but thats better */
#audio-visualizer-container.active { #audio-visualizer-container.active {
box-shadow: 0 0 20px rgba(255, 255, 255, 0.3); box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
} }
/* Fade in animation */ /* Fade in animation */
@keyframes fadeIn { @keyframes fadeIn {
from { from {
opacity: 0; opacity: 0;
transform: scale(0.8); transform: scale(0.8);
} }
to { to {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }
} }
#audio-visualizer-container { #audio-visualizer-container {
animation: fadeIn 0.5s ease-out; animation: fadeIn 0.5s ease-out;
} }
+9 -10
View File
@@ -1,12 +1,11 @@
{ {
"name": "@meowarex/colorama-lyrics", "name": "@meowarex/colorama-lyrics",
"description": "Customize lyrics colors: single, gradient & auto from cover art", "description": "Customize lyrics colors: single, gradient & auto from cover art",
"author": { "author": {
"name": "meowarex", "name": "meowarex",
"url": "https://github.com/meowarex", "url": "https://github.com/meowarex",
"avatarUrl": "https://avatars.githubusercontent.com/u/90243579" "avatarUrl": "https://avatars.githubusercontent.com/u/90243579"
}, },
"main": "./src/index.ts", "main": "./src/index.ts",
"type": "module" "type": "module"
} }
File diff suppressed because it is too large Load Diff
+178 -164
View File
@@ -13,181 +13,195 @@ 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(
if (img) return img; 'figure[class*="_albumImage"] > div > div > div > img',
const video = document.querySelector('figure[class*="_albumImage"] > div > div > div > video') as HTMLVideoElement | null; ) as HTMLImageElement | null;
if (video) { if (img) return img;
const poster = video.getAttribute("poster"); const video = document.querySelector(
if (!poster) return null; 'figure[class*="_albumImage"] > div > div > div > video',
const tempImg = new Image(); ) as HTMLVideoElement | null;
tempImg.crossOrigin = "anonymous"; if (video) {
tempImg.src = poster; const poster = video.getAttribute("poster");
await new Promise<void>((resolve) => { if (!poster) return null;
tempImg.onload = () => resolve(); const tempImg = new Image();
tempImg.onerror = () => resolve(); tempImg.crossOrigin = "anonymous";
}); tempImg.src = poster;
return tempImg as unknown as HTMLImageElement; await new Promise<void>((resolve) => {
} tempImg.onload = () => resolve();
return null; tempImg.onerror = () => resolve();
});
return tempImg as unknown as HTMLImageElement;
}
return null;
} }
function getDominantColorsFromImage(img: HTMLImageElement, count: number = 2): string[] { function getDominantColorsFromImage(
try { img: HTMLImageElement,
const canvas = document.createElement('canvas'); count: number = 2,
const ctx = canvas.getContext('2d'); ): string[] {
if (!ctx) return ["#ffffff", "#88aaff"]; // fallback try {
const w = 64; const canvas = document.createElement("canvas");
const h = 64; const ctx = canvas.getContext("2d");
canvas.width = w; if (!ctx) return ["#ffffff", "#88aaff"]; // fallback
canvas.height = h; const w = 64;
ctx.drawImage(img, 0, 0, w, h); const h = 64;
const data = ctx.getImageData(0, 0, w, h).data; canvas.width = w;
canvas.height = h;
ctx.drawImage(img, 0, 0, w, h);
const data = ctx.getImageData(0, 0, w, h).data;
// Simple k-means-ish binning into 16 buckets per channel // Simple k-means-ish binning into 16 buckets per channel
const buckets = new Map<string, number>(); const buckets = new Map<string, number>();
for (let i = 0; i < data.length; i += 4) { for (let i = 0; i < data.length; i += 4) {
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 {
return ["#ffffff", "#88aaff"]; // fallback return ["#ffffff", "#88aaff"]; // fallback
} }
} }
// 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);
const b = parseInt(v[3] + v[3], 16); const b = parseInt(v[3] + v[3], 16);
return { r, g, b }; return { r, g, b };
} }
if (/^#([0-9a-fA-F]{6})$/.test(v)) { if (/^#([0-9a-fA-F]{6})$/.test(v)) {
const r = parseInt(v.slice(1, 3), 16); const r = parseInt(v.slice(1, 3), 16);
const g = parseInt(v.slice(3, 5), 16); const g = parseInt(v.slice(3, 5), 16);
const b = parseInt(v.slice(5, 7), 16); const b = parseInt(v.slice(5, 7), 16);
return { r, g, b }; return { r, g, b };
} }
// 8-digit hex expects #AARRGGBB. Indices 1-3 are the alpha byte (ignored here), // 8-digit hex expects #AARRGGBB. Indices 1-3 are the alpha byte (ignored here),
// so r/g/b are extracted from v.slice(3,5), v.slice(5,7), v.slice(7,9) respectively. // so r/g/b are extracted from v.slice(3,5), v.slice(5,7), v.slice(7,9) respectively.
if (/^#([0-9a-fA-F]{8})$/.test(v)) { if (/^#([0-9a-fA-F]{8})$/.test(v)) {
const r = parseInt(v.slice(3, 5), 16); const r = parseInt(v.slice(3, 5), 16);
const g = parseInt(v.slice(5, 7), 16); const g = parseInt(v.slice(5, 7), 16);
const b = parseInt(v.slice(7, 9), 16); const b = parseInt(v.slice(7, 9), 16);
return { r, g, b }; return { r, g, b };
} }
return null; return null;
} }
function rgbaFromHexAndAlpha(hex: string, alphaPercent: number | undefined): string { function rgbaFromHexAndAlpha(
const rgb = hexToRgb(hex); hex: string,
const a = Math.max(0.05, Math.min(100, alphaPercent ?? 100)) / 100; alphaPercent: number | undefined,
if (!rgb) return `rgba(255,255,255,${a})`; ): string {
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`; const rgb = hexToRgb(hex);
const a = Math.max(0.05, Math.min(100, alphaPercent ?? 100)) / 100;
if (!rgb) return `rgba(255,255,255,${a})`;
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`;
} }
function applySingleColor(color: string) { function applySingleColor(color: string) {
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) {
const startAlpha = (settings as any).gradientStartAlpha ?? 100; const startAlpha = (settings as any).gradientStartAlpha ?? 100;
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) {
const img = await getCoverArtElement(); const img = await getCoverArtElement();
if (!img) return; if (!img) return;
const colors = getDominantColorsFromImage(img, gradient ? 2 : 1); const colors = getDominantColorsFromImage(img, gradient ? 2 : 1);
if (gradient) { if (gradient) {
const start = colors[0] ?? settings.gradientStart; const start = colors[0] ?? settings.gradientStart;
const end = colors[1] ?? settings.gradientEnd; const end = colors[1] ?? settings.gradientEnd;
applyGradient(start, end, settings.gradientAngle); applyGradient(start, end, settings.gradientAngle);
} else { } else {
const color = colors[0] ?? settings.singleColor; const color = colors[0] ?? settings.singleColor;
applySingleColor(color); applySingleColor(color);
} }
} }
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) {
case "single": case "single":
applySingleColor(settings.singleColor); applySingleColor(settings.singleColor);
break; break;
case "gradient-experimental": case "gradient-experimental":
applyGradient(settings.gradientStart, settings.gradientEnd, settings.gradientAngle); applyGradient(
break; settings.gradientStart,
case "cover": settings.gradientEnd,
applyCoverColors(false); settings.gradientAngle,
break; );
case "cover-gradient": break;
applyCoverColors(true); case "cover":
break; applyCoverColors(false);
} break;
case "cover-gradient":
applyCoverColors(true);
break;
}
} }
(window as any).applyColoramaLyrics = applyColoramaLyrics; (window as any).applyColoramaLyrics = applyColoramaLyrics;
// Re-apply on track changes (for auto modes) // Re-apply on track changes (for auto modes)
function observeTrackChanges(): void { function observeTrackChanges(): void {
let lastTrackId: string | null = null; let lastTrackId: string | null = null;
const check = () => { const check = () => {
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);
} }
} }
}; };
const interval = setInterval(check, 500); const interval = setInterval(check, 500);
unloads.add(() => clearInterval(interval)); unloads.add(() => clearInterval(interval));
check(); check();
} }
// Initial apply and observers // Initial apply and observers
@@ -196,26 +210,26 @@ observeTrackChanges();
// for some reason, re-apply after Radiant updates its styles/backgrounds // for some reason, re-apply after Radiant updates its styles/backgrounds
function hookRadiantUpdates(): void { function hookRadiantUpdates(): void {
const w = window as any; const w = window as any;
const wrap = (name: string) => { const wrap = (name: string) => {
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 {
return result; applyColoramaLyrics();
}; } catch {}
(patched as any).__coloramaPatched = true; return result;
w[name] = patched; };
} (patched as any).__coloramaPatched = true;
}; w[name] = patched;
wrap('updateRadiantLyricsStyles'); }
wrap('updateRadiantLyricsNowPlayingBackground'); };
wrap('updateRadiantLyricsGlobalBackground'); wrap("updateRadiantLyricsStyles");
wrap('updateRadiantLyricsTextGlow'); wrap("updateRadiantLyricsNowPlayingBackground");
wrap("updateRadiantLyricsGlobalBackground");
wrap("updateRadiantLyricsTextGlow");
} }
setTimeout(() => hookRadiantUpdates(), 0); setTimeout(() => hookRadiantUpdates(), 0);
+95 -51
View File
@@ -1,94 +1,138 @@
/* Variables used by Colorama Lyrics */ /* Variables used by Colorama Lyrics */
:root { :root {
--cl-lyrics-color: #ffffff; --cl-lyrics-color: #ffffff;
--cl-grad-start: #ffffff; --cl-grad-start: #ffffff;
--cl-grad-end: #88aaff; --cl-grad-end: #88aaff;
--cl-grad-angle: 0deg; --cl-grad-angle: 0deg;
--cl-glow1: #ffffff; --cl-glow1: #ffffff;
--cl-glow2: #ffffff; --cl-glow2: #ffffff;
} }
/* Apply solid color to lyrics text */ /* Apply solid color to lyrics text */
.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
color: var(--cl-lyrics-color) !important; [class^="_lyricsContainer"]
background: none !important; > div
-webkit-background-clip: initial !important; > div
background-clip: initial !important; > span[data-current="true"] {
-webkit-text-fill-color: initial !important; color: var(--cl-lyrics-color) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
} }
/* Apply gradient to lyrics text */ /* Apply gradient to lyrics text */
.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"]
-webkit-background-clip: text !important; > div
background-clip: text !important; > div
color: transparent !important; > span[data-current="true"] {
-webkit-text-fill-color: transparent !important; background: linear-gradient(
var(--cl-grad-angle),
var(--cl-grad-start),
var(--cl-grad-end)
) !important;
-webkit-background-clip: text !important;
background-clip: text !important;
color: transparent !important;
-webkit-text-fill-color: transparent !important;
} }
/* Only-active: apply container class only on the active line via JS */ /* Only-active: apply container class only on the active line via JS */
/* Slight emphasis on current line (uniform to single mode) */ /* Slight emphasis on current line (uniform to single mode) */
.colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"], .colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-gradient [class^="_lyricsContainer"] > div > div > span[data-current="true"] { .colorama-gradient
filter: brightness(1.1) !important; [class^="_lyricsContainer"]
> div
> div
> span[data-current="true"] {
filter: brightness(1.1) !important;
} }
/* Keep song title color unchanged; its glow is controlled in Radiant CSS */ /* Keep song title color unchanged; its glow is controlled in Radiant CSS */
/* 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(
-webkit-background-clip: text !important; var(--cl-grad-angle),
background-clip: text !important; var(--cl-grad-start),
color: transparent !important; var(--cl-grad-end)
-webkit-text-fill-color: transparent !important; ) !important;
/* Do not increase glow strength on hover for gradients */ -webkit-background-clip: text !important;
background-clip: text !important;
color: transparent !important;
-webkit-text-fill-color: transparent !important;
/* Do not increase glow strength on hover for gradients */
} }
/* Only color active line mode */ /* 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
/* Match Radiant inactive styling */ > span:not([data-current="true"]),
color: rgba(128, 128, 128, 0.4) !important; body.colorama-only-active.colorama-gradient
background: none !important; [class*="_lyricsText"]
-webkit-background-clip: initial !important; > div
background-clip: initial !important; > span:not([data-current="true"]) {
-webkit-text-fill-color: initial !important; /* Match Radiant inactive styling */
text-shadow: initial !important; color: rgba(128, 128, 128, 0.4) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
} }
/* In only-active mode, keep TIDAL defaults even on hover for inactive lines */ /* 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
color: lightgray !important; > span:not([data-current="true"]):hover,
background: none !important; body.colorama-only-active.colorama-gradient
-webkit-background-clip: initial !important; [class*="_lyricsText"]
background-clip: initial !important; > div
-webkit-text-fill-color: initial !important; > span:not([data-current="true"]):hover {
text-shadow: initial !important; color: lightgray !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
} }
+1 -1
View File
@@ -8,4 +8,4 @@
}, },
"main": "./src/index.ts", "main": "./src/index.ts",
"type": "module" "type": "module"
} }
+109 -84
View File
@@ -1,4 +1,4 @@
import { LunaUnload, Tracer } from "@luna/core"; import { type LunaUnload, Tracer } from "@luna/core";
import { StyleTag } from "@luna/lib"; import { StyleTag } from "@luna/lib";
// Import CSS directly using Luna's file:// syntax - Took me a while to figure out <3 // Import CSS directly using Luna's file:// syntax - Took me a while to figure out <3
@@ -9,110 +9,135 @@ export const { trace } = Tracer("[Copy Lyrics]");
// clean up resources // clean up resources
export const unloads = new Set<LunaUnload>(); export const unloads = new Set<LunaUnload>();
// StyleTag for lyrics selection styling // Style injection via side effect
const lyricsStyleTag = new StyleTag("Copy-Lyrics", unloads, unlockSelection); new StyleTag("Copy-Lyrics", unloads, unlockSelection);
function SetClipboard(text: string): void { function SetClipboard(text: string): void {
const textarea = document.createElement("textarea"); const textarea = document.createElement("textarea");
textarea.value = text; textarea.value = text;
textarea.style.position = "fixed"; // Avoid scrolling to bottom textarea.style.position = "fixed"; // Avoid scrolling to bottom
document.body.appendChild(textarea); document.body.appendChild(textarea);
textarea.select(); textarea.select();
try { try {
const success = document.execCommand("copy"); const success = document.execCommand("copy");
if (!success) throw new Error("Failed to copy text."); if (!success) throw new Error("Failed to copy text.");
} catch (err) { } catch (err) {
trace.msg.err(err instanceof Error ? err.message : String(err)); trace.msg.err(err instanceof Error ? err.message : String(err));
} finally { } finally {
document.body.removeChild(textarea); document.body.removeChild(textarea);
} }
} }
let isSelecting = false; let isSelecting = false;
const onMouseDown = function (): void { const onMouseDown = (): void => {
isSelecting = true; isSelecting = true;
}; };
const onMouseUp = function (event: MouseEvent): void { const onMouseUp = (): void => {
if (isSelecting) { if (isSelecting) {
const selection = window.getSelection(); const selection = window.getSelection();
if (selection && selection.toString().length > 0) { if (selection?.toString().length > 0) {
const selectedSpans: HTMLSpanElement[] = []; const selectedSpans: HTMLSpanElement[] = [];
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
let container = range.commonAncestorContainer; let container: Node | null = range.commonAncestorContainer;
// If the container is NOT an element and a document, adjust it.
if (
container.nodeType !== Node.ELEMENT_NODE &&
container.nodeType !== Node.DOCUMENT_NODE
) {
// Get the parent element if it's a text node
const parentElement = container.parentElement;
if (parentElement && parentElement.hasAttribute("data-current")) {
let text_ = selection.toString().trim();
SetClipboard(text_);
trace.msg.log("Copied to clipboard!");
return;
}
}
// Get all the spans inside the container. // Normalize container: if it's a text node, use its parent element/node
const spans = (container as Element).getElementsByTagName("span"); if (container && container.nodeType === Node.TEXT_NODE) {
for (let span of spans) { container = (container.parentElement ?? container.parentNode) as Node | null;
if (selection.containsNode(span, true)) { }
selectedSpans.push(span as HTMLSpanElement);
}
}
// Concat the text of the selected spans. // If parent has data-current, treat as single-line copy case
let hasCorrectAttribute = false; if (
let text = ""; container &&
selectedSpans.forEach((span) => { container.nodeType === Node.ELEMENT_NODE &&
if (span.hasAttribute("data-current")) { (container as Element).hasAttribute("data-current")
hasCorrectAttribute = true; ) {
text += span.textContent + "\n"; const text_ = selection.toString().trim();
if ([...span.classList].some((className) => className.startsWith("endOfStanza--"))) { SetClipboard(text_);
text += "\n"; trace.msg.log("Copied to clipboard!");
} return;
} }
});
text = text.trim(); // Ensure we have an Element or Document before querying
if (
!container ||
(container.nodeType !== Node.ELEMENT_NODE &&
container.nodeType !== Node.DOCUMENT_NODE)
) {
isSelecting = false;
return;
}
if (hasCorrectAttribute) { // Get all the spans inside the container.
SetClipboard(text); const spans = (container as Element | Document).getElementsByTagName(
trace.msg.log("Copied to clipboard!"); "span",
selection.removeAllRanges(); );
} for (const span of spans) {
} if (selection.containsNode(span, true)) {
isSelecting = false; selectedSpans.push(span as HTMLSpanElement);
} }
}
// Concat the text of the selected spans.
let hasCorrectAttribute = false;
let text = "";
selectedSpans.forEach((span) => {
if (span.hasAttribute("data-current")) {
hasCorrectAttribute = true;
text += span.textContent + "\n";
if (
[...span.classList].some((className) =>
className.startsWith("endOfStanza--"),
)
) {
text += "\n";
}
}
});
text = text.trim();
if (hasCorrectAttribute) {
SetClipboard(text);
trace.msg.log("Copied to clipboard!");
selection.removeAllRanges();
}
}
isSelecting = false;
}
}; };
const onClickHooked = function (event: MouseEvent): boolean | void { const onClickHooked = (event: MouseEvent): boolean | undefined => {
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 (
// Prevent default behavior and stop event propagation target.tagName.toLowerCase() === "span" &&
event.preventDefault(); target.hasAttribute("data-current")
event.stopPropagation(); ) {
event.stopImmediatePropagation(); // Prevent default behavior and stop event propagation
return false; event.preventDefault();
} event.stopPropagation();
event.stopImmediatePropagation();
return false;
}
return undefined;
}; };
// Add event listener with capture phase to intercept events before they reach other handlers // Add event listener with capture phase to intercept events before they reach other handlers
document.addEventListener("click", onClickHooked, true); document.addEventListener("click", onClickHooked, true);
document.addEventListener("mousedown", onMouseDown); document.addEventListener("mousedown", onMouseDown);
document.addEventListener("mouseup", onMouseUp); document.addEventListener("mouseup", onMouseUp);
// Add cleanup to unloads // Add cleanup to unloads
unloads.add(() => { unloads.add((): void => {
// Remove event listeners // Remove event listeners
document.removeEventListener("click", onClickHooked, true); document.removeEventListener("click", onClickHooked, true);
document.removeEventListener("mousedown", onMouseDown); document.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mouseup", onMouseUp); document.removeEventListener("mouseup", onMouseUp);
}); });
+6 -6
View File
@@ -1,9 +1,9 @@
[class^="_lyricsText"]>div>span { [class^="_lyricsText"] > div > span {
user-select: text; user-select: text;
cursor: text; cursor: text;
} }
::selection { ::selection {
background: rgb(72, 0, 60); background: rgb(72, 0, 60);
color: rgb(255, 255, 255); color: rgb(255, 255, 255);
} }
+1 -1
View File
@@ -8,4 +8,4 @@
}, },
"main": "./src/index.ts", "main": "./src/index.ts",
"type": "module" "type": "module"
} }
+2 -2
View File
@@ -9,9 +9,9 @@ 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 = () => {
return null; return null;
}; };
+259 -192
View File
@@ -1,5 +1,5 @@
import { LunaUnload, Tracer } from "@luna/core"; import { type LunaUnload, Tracer } from "@luna/core";
import { StyleTag, ContextMenu } from "@luna/lib"; import { StyleTag } from "@luna/lib";
import { settings, Settings } from "./Settings"; import { settings, Settings } from "./Settings";
// Import CSS directly using Luna's file:// syntax // Import CSS directly using Luna's file:// syntax
@@ -13,8 +13,8 @@ export { Settings };
// Clean up resources // Clean up resources
export const unloads = new Set<LunaUnload>(); export const unloads = new Set<LunaUnload>();
// StyleTag for element hider // StyleTag for element hider (side-effect)
const styleTag = new StyleTag("Element-Hider", unloads, styles); new StyleTag("Element-Hider", unloads, styles);
// State management // State management
let targetElement: HTMLElement | null = null; let targetElement: HTMLElement | null = null;
@@ -30,39 +30,54 @@ function generateElementSelector(element: HTMLElement): string {
if (element.id) { if (element.id) {
return `#${element.id}`; return `#${element.id}`;
} }
// 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}"]`;
} }
// Priority 3: Combination of tag + specific classes + position // Priority 3: Combination of tag + specific classes + position
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,26 +85,36 @@ 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}`;
} }
} }
} }
} }
} }
trace.log(`Generated specific selector: ${selector}`); trace.log(`Generated specific selector: ${selector}`);
return selector; return selector;
} }
@@ -100,16 +125,16 @@ 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) {
settings.hiddenElements.push(elementInfo); settings.hiddenElements.push(elementInfo);
trace.log(`Saved element: ${elementInfo.selector}`); trace.log(`Saved element: ${elementInfo.selector}`);
@@ -119,17 +144,18 @@ function saveHiddenElement(element: HTMLElement): void {
} }
} }
// Remove hidden element from persistent storage (for unhiding) // Remove hidden element from persistent storage (for unhiding) - currently unused
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) { // );
settings.hiddenElements.splice(index, 1); // if (index !== -1) {
trace.log(`Permanently removed: ${selector}`); // settings.hiddenElements.splice(index, 1);
trace.log(`Remaining stored: ${settings.hiddenElements.length}`); // trace.log(`Permanently removed: ${selector}`);
} // trace.log(`Remaining stored: ${settings.hiddenElements.length}`);
} // }
// }
// Check if an element matches any stored selector (EXACT match only) // Check if an element matches any stored selector (EXACT match only)
function matchesStoredSelector(element: HTMLElement): boolean { function matchesStoredSelector(element: HTMLElement): boolean {
@@ -143,58 +169,67 @@ function matchesStoredSelector(element: HTMLElement): boolean {
trace.warn(`Invalid selector: ${storedElement.selector}`, error); trace.warn(`Invalid selector: ${storedElement.selector}`, error);
} }
} }
return false; return false;
} }
// Hide element directly without animation // Hide element directly without animation
function hideElementDirectly(element: HTMLElement): void { function hideElementDirectly(element: HTMLElement): void {
if (hiddenElements.has(element)) return; if (hiddenElements.has(element)) return;
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");
// Store reference to the element // Store reference to the element
const elementToHide = targetElement; const elementToHide = targetElement;
// Save to persistent storage // Save to persistent storage
saveHiddenElement(elementToHide); saveHiddenElement(elementToHide);
// 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);
// Clear target reference // Clear target reference
targetElement = null; targetElement = null;
} }
// 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");
} }
}); });
// Clear both storage and runtime collections // Clear both storage and runtime collections
settings.hiddenElements = []; settings.hiddenElements = [];
hiddenElements = new WeakSet<HTMLElement>(); hiddenElements = new WeakSet<HTMLElement>();
@@ -204,36 +239,42 @@ function unhideAllElements(): void {
// Process all elements in the document to hide matching ones (with strict matching) // Process all elements in the document to hide matching ones (with strict matching)
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
settings.hiddenElements.forEach((storedElement, index) => { settings.hiddenElements.forEach((storedElement, index) => {
try { try {
trace.log(`Searching for: ${storedElement.selector}`); trace.log(`Searching for: ${storedElement.selector}`);
const elements = document.querySelectorAll(storedElement.selector); const elements = document.querySelectorAll(storedElement.selector);
trace.log(`Found ${elements.length} matches for selector ${index + 1}`); trace.log(`Found ${elements.length} matches for selector ${index + 1}`);
// 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;
} }
elements.forEach((element, elemIndex) => { elements.forEach((element, elemIndex) => {
const htmlElement = element as HTMLElement; const htmlElement = element as HTMLElement;
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) {
trace.warn(`Invalid selector: ${storedElement.selector}`, error); trace.warn(`Invalid selector: ${storedElement.selector}`, error);
} }
}); });
if (hiddenCount > 0) { if (hiddenCount > 0) {
trace.log(`Total elements hidden: ${hiddenCount}`); trace.log(`Total elements hidden: ${hiddenCount}`);
} }
@@ -241,19 +282,19 @@ 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;
// Check the element itself // Check the element itself
if (matchesStoredSelector(element)) { if (matchesStoredSelector(element)) {
hideElementDirectly(element); hideElementDirectly(element);
} }
// 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);
} }
@@ -264,26 +305,33 @@ function processNewElements(addedNodes: NodeList): void {
// Set up reactive element observer // Set up reactive element observer
function setupElementObserver(): void { function setupElementObserver(): void {
if (elementObserver) return; if (elementObserver) return;
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);
} }
}); });
}); });
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`);
} }
// Global functions // Global functions
(window as any).showAllElementsFromSettings = unhideAllElements; declare global {
(window as any).debugElementHider = () => { interface Window {
showAllElementsFromSettings?: () => void;
debugElementHider?: () => void;
}
}
window.showAllElementsFromSettings = unhideAllElements;
window.debugElementHider = () => {
trace.log(`=== Element Hider Debug Info ===`); trace.log(`=== Element Hider Debug Info ===`);
trace.log(`Stored elements: ${settings.hiddenElements.length}`); trace.log(`Stored elements: ${settings.hiddenElements.length}`);
trace.log(`Currently hidden elements: ${hiddenElementsArray.length}`); trace.log(`Currently hidden elements: ${hiddenElementsArray.length}`);
@@ -297,19 +345,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,59 +369,70 @@ 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(
const target = event.target as HTMLElement; "contextmenu",
(event: MouseEvent) => {
// Don't interfere with native context menus on inputs, textareas, etc. const target = event.target as HTMLElement;
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
currentContextElement = null; // Don't interfere with native context menus on inputs, textareas, etc.
return; if (
} target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
// Don't show menu on our own custom menu target.isContentEditable
if (target.closest(".element-hider-custom-menu")) { ) {
return; currentContextElement = null;
} return;
// Close any existing custom menu
closeCustomMenu();
// Store the right-clicked element for context menu
currentContextElement = target;
waitingForBuiltInMenu = true;
// Store event coordinates for potential custom menu
const eventX = event.clientX;
const eventY = event.clientY;
// Prevent default immediately if we plan to handle it
event.preventDefault();
// Wait to see if the built-in context menu appears
contextMenuTimeout = window.setTimeout(() => {
// If we're still waiting and no built-in menu appeared, show our custom menu
if (waitingForBuiltInMenu && currentContextElement) {
showCustomMenu(eventX, eventY);
} }
waitingForBuiltInMenu = false;
}, 150); // Wait 150ms for built-in menu // Don't show menu on our own custom menu
if (target.closest(".element-hider-custom-menu")) {
// Don't prevent default initially - let Luna try to handle the context menu return;
}, true); }
// Close any existing custom menu
closeCustomMenu();
// Store the right-clicked element for context menu
currentContextElement = target;
waitingForBuiltInMenu = true;
// Store event coordinates for potential custom menu
const eventX = event.clientX;
const eventY = event.clientY;
// Allow native context menu by default; we'll show our custom menu only if needed
// Wait to see if the built-in context menu appears
contextMenuTimeout = window.setTimeout(() => {
// If we're still waiting and no built-in menu appeared, show our custom menu
if (waitingForBuiltInMenu && currentContextElement) {
showCustomMenu(eventX, eventY);
}
waitingForBuiltInMenu = false;
}, 150); // Wait 150ms for built-in menu
// Don't prevent default initially - let Luna try to handle the context menu
},
true,
);
// Listen for clicks to close custom menu // Listen for clicks to close custom menu
document.addEventListener('click', (event: MouseEvent) => { document.addEventListener(
const target = event.target as HTMLElement; "click",
(event: MouseEvent) => {
// If clicking outside our custom menu, close it const target = event.target as HTMLElement;
if (customMenu && !target.closest(".element-hider-custom-menu")) {
closeCustomMenu(); // If clicking outside our custom menu, close it
removeHighlight(); if (customMenu && !target.closest(".element-hider-custom-menu")) {
} closeCustomMenu();
}, true); removeHighlight();
}
},
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();
@@ -386,7 +445,7 @@ document.addEventListener('keydown', (event: KeyboardEvent) => {
function createCustomMenu(): HTMLElement { function createCustomMenu(): HTMLElement {
const menu = document.createElement("div"); const menu = document.createElement("div");
menu.className = "element-hider-custom-menu"; menu.className = "element-hider-custom-menu";
// Hide Element option // Hide Element option
const hideItem = document.createElement("button"); const hideItem = document.createElement("button");
hideItem.className = "element-hider-menu-item"; hideItem.className = "element-hider-menu-item";
@@ -398,18 +457,18 @@ function createCustomMenu(): HTMLElement {
closeCustomMenu(); closeCustomMenu();
} }
}); });
// Add hover effects for highlighting // Add hover effects for highlighting
hideItem.addEventListener("mouseenter", () => { hideItem.addEventListener("mouseenter", () => {
if (currentContextElement) { if (currentContextElement) {
highlightElement(currentContextElement); highlightElement(currentContextElement);
} }
}); });
hideItem.addEventListener("mouseleave", () => { hideItem.addEventListener("mouseleave", () => {
removeHighlight(); removeHighlight();
}); });
// Unhide All Elements option // Unhide All Elements option
const unhideAllItem = document.createElement("button"); const unhideAllItem = document.createElement("button");
unhideAllItem.className = "element-hider-menu-item"; unhideAllItem.className = "element-hider-menu-item";
@@ -418,28 +477,28 @@ function createCustomMenu(): HTMLElement {
unhideAllElements(); unhideAllElements();
closeCustomMenu(); closeCustomMenu();
}); });
menu.appendChild(hideItem); menu.appendChild(hideItem);
menu.appendChild(unhideAllItem); menu.appendChild(unhideAllItem);
return menu; return menu;
} }
// Show custom context menu // Show custom context menu
function showCustomMenu(x: number, y: number): void { function showCustomMenu(x: number, y: number): void {
closeCustomMenu(); closeCustomMenu();
customMenu = createCustomMenu(); customMenu = createCustomMenu();
document.body.appendChild(customMenu); document.body.appendChild(customMenu);
// Position the menu // Position the menu
const rect = customMenu.getBoundingClientRect(); const rect = customMenu.getBoundingClientRect();
const finalX = Math.min(x, window.innerWidth - rect.width - 10); const finalX = Math.min(x, window.innerWidth - rect.width - 10);
const finalY = Math.min(y, window.innerHeight - rect.height - 10); const finalY = Math.min(y, window.innerHeight - rect.height - 10);
customMenu.style.left = `${finalX}px`; customMenu.style.left = `${finalX}px`;
customMenu.style.top = `${finalY}px`; customMenu.style.top = `${finalY}px`;
trace.log(`Context menu opened for: ${currentContextElement?.tagName}`); trace.log(`Context menu opened for: ${currentContextElement?.tagName}`);
} }
@@ -449,7 +508,7 @@ function closeCustomMenu(): void {
customMenu.remove(); customMenu.remove();
customMenu = null; customMenu = null;
} }
if (contextMenuTimeout) { if (contextMenuTimeout) {
clearTimeout(contextMenuTimeout); clearTimeout(contextMenuTimeout);
contextMenuTimeout = null; contextMenuTimeout = null;
@@ -462,11 +521,18 @@ const contextMenuObserver = new MutationObserver((mutations) => {
mutation.addedNodes.forEach((node) => { mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) { if (node.nodeType === Node.ELEMENT_NODE) {
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
waitingForBuiltInMenu = false; waitingForBuiltInMenu = false;
@@ -485,8 +551,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;
@@ -502,46 +568,47 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
font-size: 14px; font-size: 14px;
`; `;
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();
} }
}); });
// 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);
@@ -549,7 +616,7 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
`; `;
contextMenu.appendChild(separator); contextMenu.appendChild(separator);
} }
// Add our buttons // Add our buttons
contextMenu.appendChild(hideButton); contextMenu.appendChild(hideButton);
contextMenu.appendChild(unhideAllButton); contextMenu.appendChild(unhideAllButton);
@@ -558,28 +625,28 @@ 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...");
// Process immediately when DOM is ready // Process immediately when DOM is ready
trace.log("Starting element processing..."); trace.log("Starting element processing...");
// Process existing elements // Process existing elements
processAllElements(); processAllElements();
// Set up reactive observer for new elements // Set up reactive observer for new elements
setupElementObserver(); setupElementObserver();
trace.log("Plugin fully initialized"); trace.log("Plugin fully initialized");
} }
// 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();
} }
@@ -592,18 +659,18 @@ unloads.add(() => {
elementObserver = null; elementObserver = null;
} }
contextMenuObserver.disconnect(); contextMenuObserver.disconnect();
// Close any open custom menu // Close any open custom menu
closeCustomMenu(); closeCustomMenu();
// Remove highlights // Remove highlights
removeHighlight(); removeHighlight();
// Clean up global functions // Clean up global functions
(window as any).showAllElementsFromSettings = undefined; window.showAllElementsFromSettings = undefined;
(window as any).debugElementHider = undefined; window.debugElementHider = undefined;
trace.log("Plugin unloaded"); trace.log("Plugin unloaded");
}); });
trace.log("Plugin loaded - Right-click any element to hide it!"); trace.log("Plugin loaded - Right-click any element to hide it!");
+37 -35
View File
@@ -2,62 +2,64 @@
/* Custom context menu for elements without built-in menu */ /* Custom context menu for elements without built-in menu */
.element-hider-custom-menu { .element-hider-custom-menu {
position: fixed; position: fixed;
background: var(--wave-color-background-elevated, #2a2a2a); background: var(--wave-color-background-elevated, #2a2a2a);
border: 1px solid var(--wave-color-border, #444); border: 1px solid var(--wave-color-border, #444);
border-radius: 8px; border-radius: 8px;
padding: 8px 0; padding: 8px 0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 999999; z-index: 999999;
min-width: 180px; min-width: 180px;
font-family: inherit; font-family: inherit;
font-size: 14px; font-size: 14px;
} }
.element-hider-menu-item { .element-hider-menu-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 8px 16px; padding: 8px 16px;
cursor: pointer; cursor: pointer;
color: var(--wave-color-text, #ffffff); color: var(--wave-color-text, #ffffff);
background: transparent; background: transparent;
border: none; border: none;
width: 100%; width: 100%;
text-align: left; text-align: left;
transition: background-color 0.15s ease; transition: background-color 0.15s ease;
font-family: inherit; font-family: inherit;
font-size: 14px; font-size: 14px;
} }
.element-hider-menu-item:hover { .element-hider-menu-item:hover {
background: var(--wave-color-background-hover, #3a3a3a); background: var(--wave-color-background-hover, #3a3a3a);
} }
.element-hider-menu-item:active { .element-hider-menu-item:active {
background: var(--wave-color-background-active, #4a4a4a); background: var(--wave-color-background-active, #4a4a4a);
} }
.element-hider-menu-icon { .element-hider-menu-icon {
margin-right: 8px; margin-right: 8px;
width: 16px; width: 16px;
height: 16px; height: 16px;
} }
/* Highlight the target element */ /* Highlight the target element */
.element-hider-target { .element-hider-target {
outline: 2px solid #ff6b6b !important; outline: 2px solid #ff6b6b !important;
outline-offset: 2px !important; outline-offset: 2px !important;
box-shadow: 0 0 10px rgba(255, 107, 107, 0.6) !important; box-shadow: 0 0 10px rgba(255, 107, 107, 0.6) !important;
} }
/* Hidden elements */ /* Hidden elements */
.element-hider-hidden { .element-hider-hidden {
display: none !important; display: none !important;
} }
/* Animation for hiding */ /* Animation for hiding */
.element-hider-hiding { .element-hider-hiding {
transition: opacity 0.3s ease, transform 0.3s ease; transition:
opacity: 0; opacity 0.3s ease,
transform: scale(0.95); transform 0.3s ease;
} opacity: 0;
transform: scale(0.95);
}
+1 -1
View File
@@ -8,4 +8,4 @@
}, },
"main": "./src/index.ts", "main": "./src/index.ts",
"type": "module" "type": "module"
} }
+18 -7
View File
@@ -9,10 +9,13 @@ 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 (
<LunaSettings> <LunaSettings>
<LunaSwitchSetting <LunaSwitchSetting
@@ -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) {
@@ -56,4 +67,4 @@ export const Settings = () => {
/> />
</LunaSettings> </LunaSettings>
); );
}; };
+136 -133
View File
@@ -7,295 +7,298 @@
*/ */
::-webkit-scrollbar { ::-webkit-scrollbar {
display: none; display: none;
} }
:root { :root {
--wave-color-solid-accent-fill: white; --wave-color-solid-accent-fill: white;
--wave-color-solid-rainbow-yellow-fill: white; --wave-color-solid-rainbow-yellow-fill: white;
--wave-color-solid-contrast-fill: white; --wave-color-solid-contrast-fill: white;
--wave-color-solid-base-brighter: black; --wave-color-solid-base-brighter: black;
--wave-text-body-medium: white !important; --wave-text-body-medium: white !important;
--track-vibrant-color: white !important; --track-vibrant-color: white !important;
--wave-color-opacity-contrast-fill-ultra-thin: #fffafa1a !important; --wave-color-opacity-contrast-fill-ultra-thin: #fffafa1a !important;
--wave-color-solid-rainbow-yellow-darkest: #fffafa1a !important; --wave-color-solid-rainbow-yellow-darkest: #fffafa1a !important;
--wave-color-solid-accent-dark: rgb(128, 128, 128); --wave-color-solid-accent-dark: rgb(128, 128, 128);
} }
/* Credits to https://github.com/surfbryce for the fonts */ /* Credits to https://github.com/surfbryce for the fonts */
@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);
} }
[class^="_wave-badge-color-max"] { [class^="_wave-badge-color-max"] {
color: black !important; color: black !important;
background-color: var(--wave-color-solid-accent-fill); background-color: var(--wave-color-solid-accent-fill);
border-radius: 3px; border-radius: 3px;
} }
[data-test="main-layout-sidebar-wrapper"] { [data-test="main-layout-sidebar-wrapper"] {
border-right: rgb(25, 25, 25) 1px solid; border-right: rgb(25, 25, 25) 1px solid;
} }
[class^="_wave-badge"] { [class^="_wave-badge"] {
background-color: var(--wave-color-solid-accent-fill); background-color: var(--wave-color-solid-accent-fill);
border-radius: 4px; border-radius: 4px;
color: black; color: black;
} }
[class^="_progressBarWrapper"] { [class^="_progressBarWrapper"] {
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);
} }
[data-test="main-layout-header"] { [data-test="main-layout-header"] {
border-left: 0 !important; border-left: 0 !important;
} }
[class^="_sidebarItem"]:hover span { [class^="_sidebarItem"]:hover span {
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;
} }
[class^="_active"] { [class^="_active"] {
color: var(--wave-color-solid-accent-fill) !important; color: var(--wave-color-solid-accent-fill) !important;
} }
[class^="ReactVirtualized__Grid"] { [class^="ReactVirtualized__Grid"] {
border-radius: 10px; border-radius: 10px;
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;
} }
[class^="ReactVirtualized__Grid__innerScrollContainer"] { [class^="ReactVirtualized__Grid__innerScrollContainer"] {
border: none; border: none;
margin: 5px; margin: 5px;
} }
[class^="button"]>span { [class^="button"] > span {
color: black; color: black;
} }
[class^="_explicitBadge"] { [class^="_explicitBadge"] {
color: var(--wave-color-solid-accent-fill); color: var(--wave-color-solid-accent-fill);
} }
[class^="viewAllButton"] { [class^="viewAllButton"] {
border-radius: 4px; border-radius: 4px;
display: grid; display: grid;
place-items: center; place-items: center;
} }
[data-test="current-media-imagery"] { [data-test="current-media-imagery"] {
border: 0 !important; border: 0 !important;
margin: none; margin: none;
} }
[class^="_imageBorder"] { [class^="_imageBorder"] {
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;
} }
[data-test="footer-player"] { [data-test="footer-player"] {
width: calc(100% - 20px); width: calc(100% - 20px);
bottom: 10px; bottom: 10px;
left: 10px; left: 10px;
border: 1px solid rgb(25, 25, 25); border: 1px solid rgb(25, 25, 25);
border-radius: 4px !important; border-radius: 4px !important;
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;
} }
[class*="coverColumn"] { [class*="coverColumn"] {
padding-left: 5px !important; padding-left: 5px !important;
} }
[class^="actionList"] { [class^="actionList"] {
background-color: transparent; background-color: transparent;
margin: 0px; margin: 0px;
border-radius: 5px; border-radius: 5px;
} }
button[data-test="request-fullscreen"], button[data-test="request-fullscreen"],
button[data-test="close-now-playing"], button[data-test="close-now-playing"],
button[data-test="play-all"], button[data-test="play-all"],
button[data-test="shuffle-all"] { button[data-test="shuffle-all"] {
color: black; color: black;
background-color: var(--wave-color-solid-accent-fill); background-color: var(--wave-color-solid-accent-fill);
border-radius: 12px; border-radius: 12px;
} }
button[data-test="request-fullscreen"]:hover, button[data-test="request-fullscreen"]:hover,
button[data-test="close-now-playing"]:hover { button[data-test="close-now-playing"]:hover {
color: black; color: black;
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;
} }
[data-test="main-layout-header"], [data-test="main-layout-header"],
[data-test="feed-sidebar"], [data-test="feed-sidebar"],
[data-test="stream-metadata"], [data-test="stream-metadata"],
[data-test="footer-player"] { [data-test="footer-player"] {
background-color: rgba(0, 0, 0, 0.8) !important; background-color: rgba(0, 0, 0, 0.8) !important;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
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);
} }
[class^="_smallHeader"] { [class^="_smallHeader"] {
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;
background-color: transparent !important; background-color: transparent !important;
} }
[class^="__NEPTUNE_PAGE"], [class^="__NEPTUNE_PAGE"],
[data-test="main"] { [data-test="main"] {
margin-top: 35px; margin-top: 35px;
} }
[data-test="button-desktop-release-notes"], [data-test="button-desktop-release-notes"],
[data-test="button-release-notes"] { [data-test="button-release-notes"] {
background-color: white; background-color: white;
} }
[data-test="button-desktop-release-notes"]:hover, [data-test="button-desktop-release-notes"]:hover,
[data-test="button-release-notes"]:hover { [data-test="button-release-notes"]:hover {
background-color: lightgray !important; background-color: lightgray !important;
transition: none !important; transition: none !important;
} }
#playQueueSidebar { #playQueueSidebar {
top: 50px !important; top: 50px !important;
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin); border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin);
margin: 2px; margin: 2px;
margin-right: -14px !important; margin-right: -14px !important;
background-color: rgba(0, 0, 0, 0.8) !important; background-color: rgba(0, 0, 0, 0.8) !important;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
[class^="_bottomGradient"] { [class^="_bottomGradient"] {
visibility: hidden; visibility: hidden;
} }
[data-test="settings-page"] { [data-test="settings-page"] {
padding-bottom: 60px !important; padding-bottom: 60px !important;
} }
[data-test="query-suggestions"], [data-test="query-suggestions"],
[data-test="recent-searches-container"] { [data-test="recent-searches-container"] {
background-color: rgba(0, 0, 0, 0.6); background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
[data-test="contextmenu"] { [data-test="contextmenu"] {
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important; border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important;
} }
[class^="_dataContainer_"]::before { [class^="_dataContainer_"]::before {
background-image: var(--img); background-image: var(--img);
filter: blur(10px) brightness(0.4); filter: blur(10px) brightness(0.4);
} }
+117 -80
View File
@@ -1,5 +1,5 @@
import { LunaUnload, Tracer } from "@luna/core"; import { type LunaUnload, Tracer } from "@luna/core";
import { StyleTag, observePromise, PlayState, Quality, type MediaItem } from "@luna/lib"; import { StyleTag, observePromise, PlayState } 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
@@ -19,110 +19,147 @@ const themeStyleTag = new StyleTag("OLED-Theme", unloads);
// Quality color mapping // Quality color mapping
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
const getQualityColor = (audioQuality: string): string => { const getQualityColor = (audioQuality: string): string => {
const quality = audioQuality?.toUpperCase(); const quality = audioQuality?.toUpperCase();
if (quality?.includes("HI_RES_LOSSLESS")) { if (quality?.includes("HI_RES_LOSSLESS")) {
return QUALITY_COLORS.MAX; return QUALITY_COLORS.MAX;
} else if (quality?.includes("LOSSLESS")) { } else if (quality?.includes("LOSSLESS")) {
return QUALITY_COLORS.HIGH; return QUALITY_COLORS.HIGH;
} else { } else if (quality?.includes("HIGH")) {
return QUALITY_COLORS.LOW; return QUALITY_COLORS.HIGH;
} } else if (quality?.includes("LOW")) {
return QUALITY_COLORS.LOW;
}
return QUALITY_COLORS.LOW;
}; };
// Interval tracking for quality monitoring
let qualityMonitoringIntervalId: number | null = null;
// 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>(
if (!progressBarWrapper) return; unloads,
progressBarWrapper.style.removeProperty('color'); `[class^="_progressBarWrapper"]`,
progressBarWrapper.querySelectorAll('[class*="progress"], [class*="bar"]').forEach(el => { );
if (el instanceof HTMLElement) el.style.removeProperty('color'); if (!progressBarWrapper) return;
}); progressBarWrapper.style.removeProperty("color");
} catch (error) { progressBarWrapper
trace.msg.err(`Failed to reset seek bar color: ${error}`); .querySelectorAll('[class*="progress"], [class*="bar"]')
} .forEach((el) => {
if (el instanceof HTMLElement) el.style.removeProperty("color");
});
} catch (error) {
trace.msg.err(`Failed to reset seek bar color: ${error}`);
}
}; };
// Function to apply quality-based seek bar coloring (if enabled) // Function to apply quality-based seek bar coloring (if enabled)
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>(
if (!progressBarWrapper) return; unloads,
const audioQuality = PlayState.playbackContext?.actualAudioQuality; `[class^="_progressBarWrapper"]`,
if (!audioQuality) return; );
const qualityColor = getQualityColor(audioQuality); if (!progressBarWrapper) return;
progressBarWrapper.style.setProperty('color', qualityColor, 'important'); const audioQuality = PlayState.playbackContext?.actualAudioQuality;
progressBarWrapper.querySelectorAll('[class*="progress"], [class*="bar"]').forEach(el => { if (!audioQuality) return;
if (el instanceof HTMLElement) el.style.setProperty('color', qualityColor, 'important'); const qualityColor = getQualityColor(audioQuality);
}); progressBarWrapper.style.setProperty("color", qualityColor, "important");
//trace.msg.log(`Applied quality color ${qualityColor}`); progressBarWrapper
} catch (error) { .querySelectorAll('[class*="progress"], [class*="bar"]')
trace.msg.err(`Failed to apply quality colors: ${error}`); .forEach((el) => {
} if (el instanceof HTMLElement)
el.style.setProperty("color", qualityColor, "important");
});
//trace.msg.log(`Applied quality color ${qualityColor}`);
} catch (error) {
trace.msg.err(`Failed to apply quality colors: ${error}`);
}
}; };
// Function to monitor track changes using track ID // Function to monitor track changes using track ID
const setupQualityMonitoring = (): void => { const setupQualityMonitoring = (): void => {
let lastTrackId: string | null = null; if (qualityMonitoringIntervalId != null) return;
const interval = setInterval(() => { let lastTrackId: string | null = null;
if (!settings.qualityColorMatchedSeekBar) return; qualityMonitoringIntervalId = window.setInterval(() => {
const currentTrackId = PlayState.playbackContext?.actualProductId; if (!settings.qualityColorMatchedSeekBar) return;
if (currentTrackId && currentTrackId !== lastTrackId) { const currentTrackId = PlayState.playbackContext?.actualProductId;
//trace.msg.log(`[OLED Theme] Track ID changed: ${lastTrackId} -> ${currentTrackId}`); if (currentTrackId && currentTrackId !== lastTrackId) {
lastTrackId = currentTrackId; lastTrackId = currentTrackId;
applyQualityColors(); applyQualityColors();
} }
}, 250); }, 250);
unloads.add(() => clearInterval(interval));
// Initial color application (if a track is already loaded) // Initial color application (if a track is already loaded)
const currentTrackId = PlayState.playbackContext?.actualProductId; const currentTrackId = PlayState.playbackContext?.actualProductId;
if (settings.qualityColorMatchedSeekBar && currentTrackId) { if (settings.qualityColorMatchedSeekBar && currentTrackId) {
lastTrackId = currentTrackId; lastTrackId = currentTrackId;
applyQualityColors(); applyQualityColors();
} }
};
const stopQualityMonitoring = (): void => {
if (qualityMonitoringIntervalId != null) {
window.clearInterval(qualityMonitoringIntervalId);
qualityMonitoringIntervalId = null;
}
}; };
// Function to apply theme styles based on current settings // Function to apply theme styles based on current settings
const applyThemeStyles = function(): void { const applyThemeStyles = (): void => {
// Choose the appropriate CSS file based on settings // Choose the appropriate CSS file based on settings
let selectedStyle: string; let selectedStyle: string;
if (settings.lightMode) {
// Light mode - (OLED friendly doesn't apply to light theme)
selectedStyle = lightTheme;
} else {
// Dark mode
selectedStyle = settings.oledFriendlyButtons ? oledFriendlyTheme : darkTheme;
}
// Remove SeekBar coloring if Quality Color Matched Seek Bar is enabled
// This allows our manual coloring to take precedence
if (settings.qualityColorMatchedSeekBar) {
selectedStyle = selectedStyle.replace(/\[class\^="_progressBarWrapper"\]\s*\{[^}]*\}/g, '');
setupQualityMonitoring();
} else {
// If disabling, reset the seek bar color
resetSeekBarColor();
}
// Apply the selected theme using StyleTag
themeStyleTag.css = selectedStyle;
if (settings.lightMode) {
// Light mode - (OLED friendly doesn't apply to light theme)
selectedStyle = lightTheme;
} else {
// Dark mode
selectedStyle = settings.oledFriendlyButtons
? oledFriendlyTheme
: darkTheme;
}
// Remove SeekBar coloring if Quality Color Matched Seek Bar is enabled
// This allows our manual coloring to take precedence
if (settings.qualityColorMatchedSeekBar) {
selectedStyle = selectedStyle.replace(
/\[class\^="_progressBarWrapper"\]\s*\{[^}]*\}/g,
"",
);
setupQualityMonitoring();
} else {
// If disabling, reset the seek bar color and stop monitoring
resetSeekBarColor();
stopQualityMonitoring();
}
// Apply the selected theme using StyleTag
themeStyleTag.css = selectedStyle;
}; };
// Make this function available globally so Settings can call it // Make this function available globally so Settings can call it
(window as any).updateOLEDThemeStyles = applyThemeStyles; declare global {
interface Window {
updateOLEDThemeStyles?: () => void;
}
}
window.updateOLEDThemeStyles = applyThemeStyles;
// Apply the OLED theme initially // Apply the OLED theme initially
applyThemeStyles(); applyThemeStyles();
// Ensure interval is cleared on unload
unloads.add(() => {
stopQualityMonitoring();
});
+165 -164
View File
@@ -7,90 +7,94 @@
*/ */
::-webkit-scrollbar { ::-webkit-scrollbar {
display: none; display: none;
} }
:root { :root {
--wave-color-solid-accent-fill: #666666; --wave-color-solid-accent-fill: #666666;
--wave-color-solid-rainbow-yellow-fill: #666666; --wave-color-solid-rainbow-yellow-fill: #666666;
--wave-color-solid-contrast-fill: #666666; --wave-color-solid-contrast-fill: #666666;
--wave-color-solid-base-brighter: #666666; --wave-color-solid-base-brighter: #666666;
--wave-text-body-medium: #333333 !important; --wave-text-body-medium: #333333 !important;
--track-vibrant-color: #666666 !important; --track-vibrant-color: #666666 !important;
--wave-color-opacity-contrast-fill-ultra-thin: #c0c0c0 !important; --wave-color-opacity-contrast-fill-ultra-thin: #c0c0c0 !important;
--wave-color-solid-rainbow-yellow-darkest: #c0c0c0 !important; --wave-color-solid-rainbow-yellow-darkest: #c0c0c0 !important;
--wave-color-solid-accent-dark: #555555; --wave-color-solid-accent-dark: #555555;
} }
/* Credits to https://github.com/surfbryce for the fonts */ /* Credits to https://github.com/surfbryce for the fonts */
@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);
} }
[class^="_wave-badge-color-max"] { [class^="_wave-badge-color-max"] {
color: #333333 !important; color: #333333 !important;
background-color: var(--wave-color-solid-accent-fill); background-color: var(--wave-color-solid-accent-fill);
border-radius: 3px; border-radius: 3px;
} }
[data-test="main-layout-sidebar-wrapper"] { [data-test="main-layout-sidebar-wrapper"] {
border-right: rgb(230, 230, 230) 1px solid; border-right: rgb(230, 230, 230) 1px solid;
background-color: rgba(250, 250, 250, 0.95) !important; background-color: rgba(250, 250, 250, 0.95) !important;
} }
[class^="_wave-badge"] { [class^="_wave-badge"] {
background-color: var(--wave-color-solid-accent-fill); background-color: var(--wave-color-solid-accent-fill);
border-radius: 4px; border-radius: 4px;
color: #333333; color: #333333;
} }
[class^="_progressBarWrapper"] { [class^="_progressBarWrapper"] {
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;
} }
[data-test="main-layout-header"] { [data-test="main-layout-header"] {
border-left: 0 !important; border-left: 0 !important;
} }
[class^="_sidebarItem"]:hover span { [class^="_sidebarItem"]:hover span {
color: #333333; color: #333333;
} }
[class^="_sidebarItem"] [class^="active"]>span { [class^="_sidebarItem"] [class^="active"] > span {
color: #333333 !important; color: #333333 !important;
} }
/* Sidebar icons and text - ensure grey colors */ /* Sidebar icons and text - ensure grey colors */
@@ -98,110 +102,107 @@
[data-test="main-layout-sidebar-wrapper"] path, [data-test="main-layout-sidebar-wrapper"] path,
[class^="_sidebarItem"] svg, [class^="_sidebarItem"] svg,
[class^="_sidebarItem"] path { [class^="_sidebarItem"] path {
fill: #666666 !important; fill: #666666 !important;
color: #666666 !important; color: #666666 !important;
} }
[data-test="main-layout-sidebar-wrapper"] span, [data-test="main-layout-sidebar-wrapper"] span,
[class^="_sidebarItem"] span { [class^="_sidebarItem"] span {
color: #666666 !important; color: #666666 !important;
} }
[class^="_active"] { [class^="_active"] {
color: var(--wave-color-solid-accent-fill) !important; color: var(--wave-color-solid-accent-fill) !important;
} }
[class^="ReactVirtualized__Grid"] { [class^="ReactVirtualized__Grid"] {
border-radius: 10px; border-radius: 10px;
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;
} }
[class^="ReactVirtualized__Grid__innerScrollContainer"] { [class^="ReactVirtualized__Grid__innerScrollContainer"] {
border: none; border: none;
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);
} }
[class^="viewAllButton"] { [class^="viewAllButton"] {
border-radius: 4px; border-radius: 4px;
display: grid; display: grid;
place-items: center; place-items: center;
} }
[data-test="current-media-imagery"] { [data-test="current-media-imagery"] {
border: 0 !important; border: 0 !important;
margin: none; margin: 0;
} }
[class^="_imageBorder"] { [class^="_imageBorder"] {
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;
} }
[data-test="footer-player"] { [data-test="footer-player"] {
width: calc(100% - 20px); width: calc(100% - 20px);
bottom: 10px; bottom: 10px;
left: 10px; left: 10px;
border: 1px solid rgba(200, 200, 200, 0.7); border: 1px solid rgba(200, 200, 200, 0.7);
border-radius: 4px !important; border-radius: 4px !important;
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;
} }
/* Track list text - ensure all text is dark */ /* Track list text - ensure all text is dark */
@@ -211,200 +212,200 @@
[class^="_albumTitle"], [class^="_albumTitle"],
[class^="_tableCell"] *, [class^="_tableCell"] *,
[class^="_tableCellContent"] * { [class^="_tableCellContent"] * {
color: #333333 !important; color: #333333 !important;
} }
[class*="coverColumn"] { [class*="coverColumn"] {
padding-left: 5px !important; padding-left: 5px !important;
} }
[class^="actionList"] { [class^="actionList"] {
background-color: transparent; background-color: transparent;
margin: 0px; margin: 0px;
border-radius: 5px; border-radius: 5px;
} }
button[data-test="request-fullscreen"], button[data-test="request-fullscreen"],
button[data-test="close-now-playing"], button[data-test="close-now-playing"],
button[data-test="play-all"], button[data-test="play-all"],
button[data-test="shuffle-all"] { button[data-test="shuffle-all"] {
color: #333333; color: #333333;
background-color: var(--wave-color-solid-accent-fill); background-color: var(--wave-color-solid-accent-fill);
border-radius: 12px; border-radius: 12px;
} }
button[data-test="request-fullscreen"]:hover, button[data-test="request-fullscreen"]:hover,
button[data-test="close-now-playing"]:hover { button[data-test="close-now-playing"]:hover {
color: #333333; color: #333333;
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;
} }
[data-test="main-layout-header"] { [data-test="main-layout-header"] {
background-color: rgba(235, 235, 235, 0.95) !important; background-color: rgba(235, 235, 235, 0.95) !important;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
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-test="feed-sidebar"] { [data-test="feed-sidebar"] {
background-color: rgba(225, 225, 225, 0.9) !important; background-color: rgba(225, 225, 225, 0.9) !important;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
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-test="stream-metadata"] { [data-test="stream-metadata"] {
background-color: rgba(230, 230, 230, 0.92) !important; background-color: rgba(230, 230, 230, 0.92) !important;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
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-test="footer-player"] { [data-test="footer-player"] {
background-color: rgba(255, 255, 255, 0.6) !important; background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(15px); backdrop-filter: blur(15px);
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);
} }
[class^="_smallHeader"] { [class^="_smallHeader"] {
margin-top: 7.5px; margin-top: 7.5px;
} }
/* Button styling using proper light theme approach */ /* Button styling using proper light theme approach */
:root { :root {
--button-light: #d9d9d9 !important; --button-light: #d9d9d9 !important;
--button-medium: #cbcbcb !important; --button-medium: #cbcbcb !important;
} }
/*buttons*/ /*buttons*/
._activeTab_f47dafa { ._activeTab_f47dafa {
background: #0000001c; background: #0000001c;
} }
/*canvas nav buttons*/ /*canvas nav buttons*/
.viewAllButton--Nb87U, .viewAllButton--Nb87U,
.css-7l8ggf { .css-7l8ggf {
background: #e0e0e0; background: #e0e0e0;
} }
.viewAllButton--Nb87U:hover, .viewAllButton--Nb87U:hover,
.css-7l8ggf:hover { .css-7l8ggf:hover {
background: #cbcbcb; background: #cbcbcb;
} }
/*tracks page*/ /*tracks page*/
.variantPrimary--pjymy, .variantPrimary--pjymy,
._button_3357ce6 { ._button_3357ce6 {
background-color: var(--button-light); background-color: var(--button-light);
} }
._button_f1c7fcb { ._button_f1c7fcb {
background: var(--wave-color-solid-base-brighter); background: var(--wave-color-solid-base-brighter);
} }
._button_84b8ffe { ._button_84b8ffe {
background-color: var(--wave-color-solid-base-brighter); background-color: var(--wave-color-solid-base-brighter);
} }
._button_84b8ffe:hover { ._button_84b8ffe:hover {
background-color: var(--wave-color-solid-base-brightest); background-color: var(--wave-color-solid-base-brightest);
} }
.button--_0I_t { .button--_0I_t {
background-color: var(--button-light); background-color: var(--button-light);
} }
.button--_0I_t:hover { .button--_0I_t:hover {
background-color: var(--wave-color-opacity-contrast-fill-regular); background-color: var(--wave-color-opacity-contrast-fill-regular);
} }
._button_94c5125 { ._button_94c5125 {
background: var(--wave-color-solid-base-brighter); background: var(--wave-color-solid-base-brighter);
} }
.primary--NLSX4 { .primary--NLSX4 {
background-color: #d5d5d5; background-color: #d5d5d5;
} }
.primary--NLSX4:hover { .primary--NLSX4:hover {
background-color: var(--wave-color-opacity-contrast-fill-regular) !important; background-color: var(--wave-color-opacity-contrast-fill-regular) !important;
} }
.primary--NLSX4:disabled { .primary--NLSX4:disabled {
background-color: #e7e7e8; background-color: #e7e7e8;
} }
.primary--NLSX4:disabled:hover { .primary--NLSX4:disabled:hover {
background-color: #e7e7e8; background-color: #e7e7e8;
} }
[class^="__NEPTUNE_PAGE"], [class^="__NEPTUNE_PAGE"],
[data-test="main"] { [data-test="main"] {
margin-top: 35px; margin-top: 35px;
} }
[data-test="button-desktop-release-notes"], [data-test="button-desktop-release-notes"],
[data-test="button-release-notes"] { [data-test="button-release-notes"] {
background-color: #333333; background-color: #333333;
} }
[data-test="button-desktop-release-notes"]:hover, [data-test="button-desktop-release-notes"]:hover,
[data-test="button-release-notes"]:hover { [data-test="button-release-notes"]:hover {
background-color: #555555 !important; background-color: #555555 !important;
transition: none !important; transition: none !important;
} }
#playQueueSidebar { #playQueueSidebar {
top: 50px !important; top: 50px !important;
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin); border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin);
margin: 2px; margin: 2px;
margin-right: -14px !important; margin-right: -14px !important;
background-color: rgba(220, 220, 220, 0.9) !important; background-color: rgba(220, 220, 220, 0.9) !important;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
[class^="_bottomGradient"] { [class^="_bottomGradient"] {
visibility: hidden; visibility: hidden;
} }
[data-test="settings-page"] { [data-test="settings-page"] {
padding-bottom: 60px !important; padding-bottom: 60px !important;
} }
[data-test="query-suggestions"], [data-test="query-suggestions"],
[data-test="recent-searches-container"] { [data-test="recent-searches-container"] {
background-color: rgba(227, 227, 227, 0.85); background-color: rgba(227, 227, 227, 0.85);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
[data-test="contextmenu"] { [data-test="contextmenu"] {
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important; border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important;
} }
[class^="_dataContainer_"]::before { [class^="_dataContainer_"]::before {
background-image: var(--img); background-image: var(--img);
filter: blur(10px) brightness(1.2); filter: blur(10px) brightness(1.2);
} }
/* Player bar text colors - fix white text issues */ /* Player bar text colors - fix white text issues */
[data-test="footer-player"] * { [data-test="footer-player"] * {
color: #333333 !important; color: #333333 !important;
} }
[data-test="footer-player"] [class*="trackTitle"], [data-test="footer-player"] [class*="trackTitle"],
@@ -413,12 +414,12 @@ button[data-test="close-now-playing"]:hover {
[data-test="footer-player"] [class*="duration"], [data-test="footer-player"] [class*="duration"],
[data-test="footer-player"] [class*="time"], [data-test="footer-player"] [class*="time"],
[data-test="footer-player"] [class*="timestamp"] { [data-test="footer-player"] [class*="timestamp"] {
color: #333333 !important; color: #333333 !important;
} }
/* Main page background */ /* Main page background */
body, body,
[data-test="main"], [data-test="main"],
[class^="__NEPTUNE_PAGE"] { [class^="__NEPTUNE_PAGE"] {
background-color: #f5f5f5 !important; background-color: #f5f5f5 !important;
} }
+95 -91
View File
@@ -7,209 +7,213 @@
*/ */
::-webkit-scrollbar { ::-webkit-scrollbar {
display: none; display: none;
} }
:root { :root {
--wave-color-solid-accent-fill: white; --wave-color-solid-accent-fill: white;
--wave-color-solid-rainbow-yellow-fill: white; --wave-color-solid-rainbow-yellow-fill: white;
--wave-color-solid-contrast-fill: white; --wave-color-solid-contrast-fill: white;
--wave-color-solid-base-brighter: black; --wave-color-solid-base-brighter: black;
--wave-text-body-medium: white !important; --wave-text-body-medium: white !important;
--track-vibrant-color: white !important; --track-vibrant-color: white !important;
--wave-color-opacity-contrast-fill-ultra-thin: #fffafa1a !important; --wave-color-opacity-contrast-fill-ultra-thin: #fffafa1a !important;
--wave-color-solid-rainbow-yellow-darkest: #fffafa1a !important; --wave-color-solid-rainbow-yellow-darkest: #fffafa1a !important;
--wave-color-solid-accent-dark: rgb(128, 128, 128); --wave-color-solid-accent-dark: rgb(128, 128, 128);
} }
/* Credits to https://github.com/surfbryce for the fonts */ /* Credits to https://github.com/surfbryce for the fonts */
@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);
} }
[class^="_wave-badge-color-max"] { [class^="_wave-badge-color-max"] {
color: black !important; color: black !important;
background-color: var(--wave-color-solid-accent-fill); background-color: var(--wave-color-solid-accent-fill);
border-radius: 3px; border-radius: 3px;
} }
[data-test="main-layout-sidebar-wrapper"] { [data-test="main-layout-sidebar-wrapper"] {
border-right: rgb(25, 25, 25) 1px solid; border-right: rgb(25, 25, 25) 1px solid;
} }
[class^="_wave-badge"] { [class^="_wave-badge"] {
background-color: var(--wave-color-solid-accent-fill); background-color: var(--wave-color-solid-accent-fill);
border-radius: 4px; border-radius: 4px;
color: black; color: black;
} }
[class^="_progressBarWrapper"] { [class^="_progressBarWrapper"] {
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);
} }
[data-test="main-layout-header"] { [data-test="main-layout-header"] {
border-left: 0 !important; border-left: 0 !important;
} }
[class^="_sidebarItem"]:hover span { [class^="_sidebarItem"]:hover span {
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;
} }
[class^="_active"] { [class^="_active"] {
color: var(--wave-color-solid-accent-fill) !important; color: var(--wave-color-solid-accent-fill) !important;
} }
[class^="ReactVirtualized__Grid"] { [class^="ReactVirtualized__Grid"] {
border-radius: 10px; border-radius: 10px;
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;
} }
[class^="ReactVirtualized__Grid__innerScrollContainer"] { [class^="ReactVirtualized__Grid__innerScrollContainer"] {
border: none; border: none;
margin: 5px; margin: 5px;
} }
[class^="_explicitBadge"] { [class^="_explicitBadge"] {
color: var(--wave-color-solid-accent-fill); color: var(--wave-color-solid-accent-fill);
} }
[data-test="current-media-imagery"] { [data-test="current-media-imagery"] {
border: 0 !important; border: 0 !important;
margin: none; margin: 0;
} }
[class^="_imageBorder"] { [class^="_imageBorder"] {
display: none; display: none;
} }
[data-test="feed-sidebar"] { [data-test="feed-sidebar"] {
margin-top: 10px; margin-top: 10px;
} }
[data-test="footer-player"] { [data-test="footer-player"] {
width: calc(100% - 20px); width: calc(100% - 20px);
bottom: 10px; bottom: 10px;
left: 10px; left: 10px;
border: 1px solid rgb(25, 25, 25); border: 1px solid rgb(25, 25, 25);
border-radius: 4px !important; border-radius: 4px !important;
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;
} }
[class*="coverColumn"] { [class*="coverColumn"] {
padding-left: 5px !important; padding-left: 5px !important;
} }
[class^="actionList"] { [class^="actionList"] {
background-color: transparent; background-color: transparent;
margin: 0px; margin: 0px;
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);
} }
[data-test="main-layout-header"], [data-test="main-layout-header"],
[data-test="feed-sidebar"], [data-test="feed-sidebar"],
[data-test="stream-metadata"], [data-test="stream-metadata"],
[data-test="footer-player"] { [data-test="footer-player"] {
background-color: rgba(0, 0, 0, 0.8) !important; background-color: rgba(0, 0, 0, 0.8) !important;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
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);
} }
[class^="_smallHeader"] { [class^="_smallHeader"] {
margin-top: 7.5px; margin-top: 7.5px;
} }
[class^="__NEPTUNE_PAGE"], [class^="__NEPTUNE_PAGE"],
[data-test="main"] { [data-test="main"] {
margin-top: 35px; margin-top: 35px;
} }
#playQueueSidebar { #playQueueSidebar {
top: 50px !important; top: 50px !important;
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin); border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin);
margin: 2px; margin: 2px;
margin-right: -14px !important; margin-right: -14px !important;
background-color: rgba(0, 0, 0, 0.8) !important; background-color: rgba(0, 0, 0, 0.8) !important;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
[class^="_bottomGradient"] { [class^="_bottomGradient"] {
visibility: hidden; visibility: hidden;
} }
[data-test="settings-page"] { [data-test="settings-page"] {
padding-bottom: 60px !important; padding-bottom: 60px !important;
} }
[data-test="query-suggestions"], [data-test="query-suggestions"],
[data-test="recent-searches-container"] { [data-test="recent-searches-container"] {
background-color: rgba(0, 0, 0, 0.6); background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
[data-test="contextmenu"] { [data-test="contextmenu"] {
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important; border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important;
} }
[class^="_dataContainer_"]::before { [class^="_dataContainer_"]::before {
background-image: var(--img); background-image: var(--img);
filter: blur(10px) brightness(0.4); filter: blur(10px) brightness(0.4);
} }
+1 -1
View File
@@ -8,4 +8,4 @@
}, },
"main": "./src/index.ts", "main": "./src/index.ts",
"type": "module" "type": "module"
} }
+181 -74
View File
@@ -3,43 +3,82 @@ import { LunaSettings, LunaSwitchSetting, LunaNumberSetting } from "@luna/ui";
import React from "react"; import React from "react";
export const settings = await ReactiveStore.getPluginStorage("RadiantLyrics", { export const settings = await ReactiveStore.getPluginStorage("RadiantLyrics", {
hideUIEnabled: true,
trackTitleGlow: false,
playerBarVisible: false,
lyricsGlowEnabled: true, lyricsGlowEnabled: true,
textGlow: 20, trackTitleGlow: false,
spinningCoverEverywhere: true, hideUIEnabled: true,
playerBarVisible: false,
CoverEverywhere: true,
performanceMode: false, performanceMode: false,
spinningArtEnabled: true, spinningArt: true,
textGlow: 20,
backgroundScale: 15,
backgroundRadius: 25,
backgroundContrast: 120, backgroundContrast: 120,
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 [CoverEverywhere, setCoverEverywhere] = React.useState(
const [performanceMode, setPerformanceMode] = React.useState(settings.performanceMode); settings.CoverEverywhere,
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 [spinningArt, setspinningArt] = React.useState(
settings.spinningArt,
);
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,
);
const [backgroundScale, setBackgroundScale] = React.useState(
settings.backgroundScale,
);
const [backgroundRadius, setBackgroundRadius] = React.useState(
settings.backgroundRadius,
);
// Derive props and override onChange to accept a broader first param type
type BaseSwitchProps = React.ComponentProps<typeof LunaSwitchSetting>;
type AnySwitchProps = Omit<BaseSwitchProps, "onChange"> & {
onChange: (_: unknown, checked: boolean) => void;
checked: boolean;
};
const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType<
AnySwitchProps
>;
return ( return (
<LunaSettings> <LunaSettings>
<LunaSwitchSetting <AnySwitch
title="Lyrics Glow Effect" title="Lyrics Glow Effect"
desc="Enable glowing effect for lyrics & Font Stytling Changes" desc="Enable glowing effect for lyrics & Font Styling Changes"
checked={lyricsGlowEnabled} checked={lyricsGlowEnabled}
onChange={(_, checked: boolean) => { onChange={(_: unknown, checked: boolean) => {
setLyricsGlowEnabled((settings.lyricsGlowEnabled = checked)); setLyricsGlowEnabled((settings.lyricsGlowEnabled = checked));
// Update styles immediately when setting changes // Update styles immediately when setting changes
if ((window as any).updateRadiantLyricsStyles) { if ((window as any).updateRadiantLyricsStyles) {
@@ -47,30 +86,30 @@ export const Settings = () => {
} }
}} }}
/> />
<LunaSwitchSetting <AnySwitch
title="Track Title Glow" title="Track Title Glow"
desc="Apply glow to the track title" desc="Apply glow to the track title"
checked={trackTitleGlow} checked={trackTitleGlow}
onChange={(_: unknown, checked: boolean) => { onChange={(_: unknown, checked: boolean) => {
setTrackTitleGlow((settings.trackTitleGlow = checked)); setTrackTitleGlow((settings.trackTitleGlow = checked));
if ((window as any).updateRadiantLyricsStyles) { if ((window as any).updateRadiantLyricsStyles) {
(window as any).updateRadiantLyricsStyles(); (window as any).updateRadiantLyricsStyles();
} }
}} }}
/> />
<LunaSwitchSetting <AnySwitch
title="Hide UI Feature" title="Hide UI Feature"
desc="Enable hide/unhide UI functionality with toggle buttons" desc="Enable hide/unhide UI functionality with toggle buttons"
checked={hideUIEnabled} checked={hideUIEnabled}
onChange={(_, checked: boolean) => { onChange={(_: unknown, checked: boolean) => {
setHideUIEnabled((settings.hideUIEnabled = checked)); setHideUIEnabled((settings.hideUIEnabled = checked));
}} }}
/> />
<LunaSwitchSetting <AnySwitch
title="Player Bar Visibility in Hide UI Mode" title="Player Bar Visibility in Hide UI Mode"
desc="Keep player bar visible when UI is hidden" desc="Keep player bar visible when UI is hidden"
checked={playerBarVisible} checked={playerBarVisible}
onChange={(_, checked: boolean) => { onChange={(_: unknown, checked: boolean) => {
console.log("Player Bar Visibility:", checked ? "visible" : "hidden"); console.log("Player Bar Visibility:", checked ? "visible" : "hidden");
setPlayerBarVisible((settings.playerBarVisible = checked)); setPlayerBarVisible((settings.playerBarVisible = checked));
// Update styles immediately when setting changes // Update styles immediately when setting changes
@@ -79,24 +118,29 @@ export const Settings = () => {
} }
}} }}
/> />
<LunaSwitchSetting <AnySwitch
title="Cover Everywhere" title="Cover Everywhere"
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={CoverEverywhere}
onChange={(_, checked: boolean) => { onChange={(_: unknown, checked: boolean) => {
console.log("Spinning Cover Everywhere:", checked ? "enabled" : "disabled"); console.log(
setSpinningCoverEverywhere((settings.spinningCoverEverywhere = checked)); "Spinning Cover Everywhere:",
checked ? "enabled" : "disabled",
);
setCoverEverywhere(
(settings.CoverEverywhere = 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();
} }
}} }}
/> />
<LunaSwitchSetting <AnySwitch
title="Performance Mode | Experimental" title="Performance Mode | Experimental"
desc="Performance mode: Reduces blur effects & uses smaller image sizes, to optimize GPU usage" desc="Performance mode: Reduces blur effects & uses smaller image sizes, to optimize GPU usage"
checked={performanceMode} checked={performanceMode}
onChange={(_, checked: boolean) => { onChange={(_: unknown, checked: boolean) => {
console.log("Performance Mode:", checked ? "enabled" : "disabled"); console.log("Performance Mode:", checked ? "enabled" : "disabled");
setPerformanceMode((settings.performanceMode = checked)); setPerformanceMode((settings.performanceMode = checked));
// Update background animations immediately when setting changes // Update background animations immediately when setting changes
@@ -108,36 +152,82 @@ export const Settings = () => {
} }
}} }}
/> />
<LunaSwitchSetting <AnySwitch
title="Background Cover Spin" // Cheers @Max/n0201 for the idea <3 title="Background Cover Spin" // Cheers @Max/n0201 for the idea <3
desc="Enable the spinning cover art background animation" desc="Enable the spinning cover art background animation"
checked={spinningArtEnabled} checked={spinningArt}
onChange={(_, checked: boolean) => { onChange={(_: unknown, checked: boolean) => {
console.log("Background Cover Spin:", checked ? "enabled" : "disabled"); console.log(
setSpinningArtEnabled((settings.spinningArtEnabled = checked)); "Background Cover Spin:",
checked ? "enabled" : "disabled",
);
setspinningArt((settings.spinningArt = 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();
} }
}} }}
/> />
<LunaNumberSetting <LunaNumberSetting
title="Text Glow" title="Text Glow"
desc="Adjust the glow size of lyrics (0-100, default: 20)" desc="Adjust the glow size of lyrics (0-100, default: 20)"
min={0} min={0}
max={100} max={100}
step={1} step={1}
value={textGlow} value={textGlow}
onNumber={(value: number) => { onNumber={(value: number) => {
setTextGlow((settings.textGlow = value)); setTextGlow((settings.textGlow = value));
// Update variables immediately when setting changes // Update variables immediately when setting changes
if ((window as any).updateRadiantLyricsTextGlow) { if ((window as any).updateRadiantLyricsTextGlow) {
(window as any).updateRadiantLyricsTextGlow(); (window as any).updateRadiantLyricsTextGlow();
} }
}} }}
/> />
<LunaNumberSetting
title="Background Scale"
desc="Adjust the scale of the background cover (1=10% - 50=500%)"
min={1}
max={50}
step={1}
value={backgroundScale}
onNumber={(value: number) => {
setBackgroundScale((settings.backgroundScale = value));
if ((window as any).updateRadiantLyricsGlobalBackground) {
(window as any).updateRadiantLyricsGlobalBackground();
}
if (
settings.settingsAffectNowPlaying &&
(window as any).updateRadiantLyricsNowPlayingBackground
) {
(window as any).updateRadiantLyricsNowPlayingBackground();
}
}}
/>
<LunaNumberSetting
title="Background Radius"
desc="Adjust the cover art corner radius (0-100%, 100% = circle)"
min={0}
max={100}
step={1}
value={backgroundRadius}
onNumber={(value: number) => {
setBackgroundRadius((settings.backgroundRadius = value));
if ((window as any).updateRadiantLyricsGlobalBackground) {
(window as any).updateRadiantLyricsGlobalBackground();
}
if (
settings.settingsAffectNowPlaying &&
(window as any).updateRadiantLyricsNowPlayingBackground
) {
(window as any).updateRadiantLyricsNowPlayingBackground();
}
}}
/>
<LunaNumberSetting <LunaNumberSetting
title="Background Contrast" title="Background Contrast"
desc="Adjust the contrast of the spinning background (0-200, default: 120)" desc="Adjust the contrast of the spinning background (0-200, default: 120)"
@@ -150,7 +240,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 +261,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 +282,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,18 +303,26 @@ 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();
} }
}} }}
/> />
<LunaSwitchSetting <AnySwitch
title="Settings Affect Now Playing" title="Settings Affect Now Playing"
desc="Apply background settings to Now Playing view" desc="Apply background settings to Now Playing view"
checked={settingsAffectNowPlaying} checked={settingsAffectNowPlaying}
onChange={(_, checked: boolean) => { onChange={(_: unknown, 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();
@@ -224,4 +331,4 @@ export const Settings = () => {
/> />
</LunaSettings> </LunaSettings>
); );
}; };
@@ -1,105 +1,114 @@
/* Global Spinning Background Styles - PERFORMANCE OPTIMIZED */ /* Global Spinning Background Styles - PERFORMANCE OPTIMIZED */
.global-background-container { .global-background-container {
position: fixed; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
z-index: -3; z-index: -3;
pointer-events: none; pointer-events: none;
overflow: hidden; overflow: hidden;
/* Hardware acceleration */ /* Hardware acceleration */
transform: translateZ(0); transform: translateZ(0);
backface-visibility: hidden; backface-visibility: hidden;
} }
.global-spinning-black-bg { .global-spinning-black-bg {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: #000; background: #000;
z-index: -2; z-index: -2;
pointer-events: none; pointer-events: none;
} }
.global-spinning-image { .global-spinning-image {
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 150vw; width: 150vw;
height: 150vh; height: 150vh;
object-fit: cover; object-fit: cover;
z-index: -1; z-index: -1;
filter: blur(80px) brightness(0.4) contrast(1.2) saturate(1); filter: blur(80px) brightness(0.4) contrast(1.2) saturate(1);
opacity: 1; opacity: 1;
animation: spinGlobal 45s linear infinite; animation: spinGlobal 45s linear infinite;
will-change: transform; will-change: transform;
/* Hardware acceleration */ /* Hardware acceleration */
transform-origin: center center; transform-origin: center center;
backface-visibility: hidden; backface-visibility: hidden;
} }
/* Performance mode optimizations - keep spinning but optimize other aspects */ /* Performance mode optimizations - keep spinning but optimize other aspects */
.global-spinning-image.performance-mode-static { .global-spinning-image.performance-mode-static {
/* Keep animation enabled in performance mode */ /* Keep animation enabled in performance mode */
/* Lighter blur for performance */ /* Lighter blur for performance */
filter: blur(20px) brightness(0.4) contrast(1.2) saturate(1) !important; /* biome-ignore lint: Required to override app styles in performance mode */
/* Smaller size for performance */ filter: blur(20px) brightness(0.4) contrast(1.2) saturate(1) !important;
width: 120vw !important; /* Smaller size for performance */
height: 120vh !important; /* biome-ignore lint: Required to override app layout sizes */
width: 120vw !important;
/* biome-ignore lint: Required to override app layout sizes */
height: 120vh !important;
} }
.now-playing-background-image.performance-mode-static { .now-playing-background-image.performance-mode-static {
/* Keep animation enabled in performance mode */ /* Keep animation enabled in performance mode */
/* Optimized size and effects for performance */ /* Optimized size and effects for performance */
width: 80vw !important; /* biome-ignore lint: Required to override inline sizes in performance mode */
height: 80vh !important; width: 80vw !important;
/* biome-ignore lint: Required to override inline sizes in performance mode */
height: 80vh !important;
} }
/* Now Playing Background Container Optimization */ /* Now Playing Background Container Optimization */
.now-playing-background-container { .now-playing-background-container {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: -3; z-index: -3;
pointer-events: none; pointer-events: none;
overflow: hidden; overflow: hidden;
/* Hardware acceleration */ /* Hardware acceleration */
transform: translateZ(0); transform: translateZ(0);
backface-visibility: hidden; backface-visibility: hidden;
} }
/* Optimized keyframe animations with GPU acceleration */ /* Optimized keyframe animations with GPU acceleration */
@keyframes spinGlobal { @keyframes spinGlobal {
from { from {
transform: translate(-50%, -50%) rotate(0deg); transform: translate(-50%, -50%) rotate(0deg);
} }
to { to {
transform: translate(-50%, -50%) rotate(360deg); transform: translate(-50%, -50%) rotate(360deg);
} }
} }
/* Reduced motion for users who prefer it */ /* Reduced motion for users who prefer it */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.global-spinning-image, .global-spinning-image,
.now-playing-background-image { .now-playing-background-image {
animation: none !important; /* biome-ignore lint: Accessibility override needs priority */
transform: translate(-50%, -50%) !important; animation: none !important;
will-change: auto !important; /* biome-ignore lint: Accessibility override needs priority */
} transform: translate(-50%, -50%) !important;
/* biome-ignore lint: Accessibility override needs priority */
will-change: auto !important;
}
} }
/* Performance mode: optimize effects but keep spinning */ /* Performance mode: optimize effects but keep spinning */
.performance-mode .global-spinning-image, .performance-mode .global-spinning-image,
.performance-mode .now-playing-background-image { .performance-mode .now-playing-background-image {
/* Keep animations but optimize filter effects */ /* Keep animations but optimize filter effects */
filter: blur(10px) brightness(0.4) contrast(1.1) !important; /* biome-ignore lint: Intentional override of runtime styles */
filter: blur(10px) brightness(0.4) contrast(1.1) !important;
} }
/* Make Notification Feed sidebar transparent */ /* Make Notification Feed sidebar transparent */
@@ -115,13 +124,14 @@ 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"],
[class^="_cellContainer"], [class^="_cellContainer"],
[class^="_cellTextContainer"] { [class^="_cellTextContainer"] {
background: unset !important; /* biome-ignore lint: Ensure background is fully cleared under theme CSS */
background: unset !important;
} }
/* Make sidebar and player bar semi-transparent with optimized backdrop-filter */ /* Make sidebar and player bar semi-transparent with optimized backdrop-filter */
@@ -129,9 +139,12 @@ main,
[data-test="main-layout-sidebar-wrapper"], [data-test="main-layout-sidebar-wrapper"],
[class^="_bar"], [class^="_bar"],
[class^="_sidebarItem"]:hover { [class^="_sidebarItem"]:hover {
background-color: rgba(0, 0, 0, 0.3) !important; /* biome-ignore lint: Must beat app inline styles for translucency */
backdrop-filter: blur(10px) !important; background-color: rgba(0, 0, 0, 0.3) !important;
-webkit-backdrop-filter: blur(10px) !important; /* biome-ignore lint: Must beat app inline styles for translucency */
backdrop-filter: blur(10px) !important;
/* biome-ignore lint: Must beat app inline styles for translucency */
-webkit-backdrop-filter: blur(10px) !important;
} }
/* Performance mode: reduce backdrop blur */ /* Performance mode: reduce backdrop blur */
@@ -139,21 +152,28 @@ main,
.performance-mode [data-test="main-layout-sidebar-wrapper"], .performance-mode [data-test="main-layout-sidebar-wrapper"],
.performance-mode [class^="_bar"], .performance-mode [class^="_bar"],
.performance-mode [class^="_sidebarItem"]:hover { .performance-mode [class^="_sidebarItem"]:hover {
backdrop-filter: blur(5px) !important; /* biome-ignore lint: Performance mode style requires priority */
-webkit-backdrop-filter: blur(5px) !important; backdrop-filter: blur(5px) !important;
/* biome-ignore lint: Performance mode style requires priority */
-webkit-backdrop-filter: blur(5px) !important;
} }
/* Feed sidebar panel - black tint background for readability */ /* Feed sidebar panel - black tint background for readability */
[data-test="feed-sidebar"] { [data-test="feed-sidebar"] {
background-color: rgba(0, 0, 0, 0.5) !important; /* biome-ignore lint: Ensure readability over media */
backdrop-filter: blur(10px) !important; background-color: rgba(0, 0, 0, 0.5) !important;
-webkit-backdrop-filter: blur(10px) !important; /* biome-ignore lint: Ensure readability over media */
backdrop-filter: blur(10px) !important;
/* biome-ignore lint: Ensure readability over media */
-webkit-backdrop-filter: blur(10px) !important;
} }
/* Performance mode: reduce sidebar backdrop blur */ /* Performance mode: reduce sidebar backdrop blur */
.performance-mode [data-test="feed-sidebar"] { .performance-mode [data-test="feed-sidebar"] {
backdrop-filter: blur(5px) !important; /* biome-ignore lint: Performance mode style requires priority */
-webkit-backdrop-filter: blur(5px) !important; backdrop-filter: blur(5px) !important;
/* biome-ignore lint: Performance mode style requires priority */
-webkit-backdrop-filter: blur(5px) !important;
} }
/* Feed sidebar items - transparent */ /* Feed sidebar items - transparent */
@@ -162,10 +182,12 @@ main,
[class*="_cellContainer"], [class*="_cellContainer"],
[data-test="feed-interval"], [data-test="feed-interval"],
[data-test="feed-item"] { [data-test="feed-item"] {
background-color: transparent !important; /* biome-ignore lint: Match theme transparency */
background-color: transparent !important;
} }
/* Remove bottom gradient */ /* Remove bottom gradient */
[class^="_bottomGradient"] { [class^="_bottomGradient"] {
display: none !important; /* biome-ignore lint: Explicitly remove conflicting gradient */
} display: none !important;
}
File diff suppressed because it is too large Load Diff
+91 -52
View File
@@ -1,82 +1,112 @@
/* Font imports for lyrics */ /* Font imports for lyrics */
@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:
padding-left: 20px; 0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #fff),
transition-duration: 0.7s; /* biome-ignore lint: Required to override app glow strength */
font-size: 55px; 0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #fff) !important;
color: white !important; padding-left: 20px;
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; transition-duration: 0.7s;
font-weight: 700; font-size: 55px;
/* biome-ignore lint: Needs priority for active lyric color */
color: white !important;
font-family:
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 700;
} }
[class*="_lyricsText"] > div > span { [class*="_lyricsText"] > div > span {
text-shadow: 0 0 0px transparent, 0 0 0px transparent; text-shadow:
transition-duration: 0.25s; 0 0 0px transparent,
color: rgba(128, 128, 128, 0.4); 0 0 0px transparent;
font-size: 40px; transition-duration: 0.25s;
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; color: rgba(128, 128, 128, 0.4);
font-weight: 700; font-size: 40px;
font-family:
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 700;
} }
[class*="_lyricsText"] > div > span:hover { [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:
color: lightgray !important; 0 0 var(--rl-glow-inner, 2px) lightgray,
padding-left: 20px; /* biome-ignore lint: Hover glow should override defaults */
transition-duration: 0.7s; 0 0 var(--rl-glow-outer, 20px) lightgray !important;
/* biome-ignore lint: Hover color override */
color: lightgray !important;
padding-left: 20px;
transition-duration: 0.7s;
} }
/* 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:
-webkit-background-clip: initial !important; 0 0 var(--rl-glow-inner, 1px) var(--cl-glow1, #fff),
background-clip: initial !important; /* biome-ignore lint: Title glow needs priority */
-webkit-text-fill-color: initial !important; 0 0 var(--rl-glow-outer, 30px) #fff !important;
color: inherit !important; /* biome-ignore lint: Reset vendor background clip */
-webkit-background-clip: initial !important;
/* biome-ignore lint: Reset background clip */
background-clip: initial !important;
/* biome-ignore lint: Reset vendor text fill */
-webkit-text-fill-color: initial !important;
/* biome-ignore lint: Ensure inherited color takes precedence */
color: inherit !important;
} }
/* When track title glow setting is disabled, remove glow regardless of Colorama */ /* When track title glow setting is disabled, remove glow regardless of Colorama */
.rl-title-glow-disabled[data-test="now-playing-track-title"] { .rl-title-glow-disabled[data-test="now-playing-track-title"] {
text-shadow: none !important; /* biome-ignore lint: Full reset required */
text-shadow: none !important;
} }
/* 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,
/* biome-ignore lint: Transition priority needed */
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:
font-weight: 700; "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
font-size: 38px !important; Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 700;
/* biome-ignore lint: Typography override for readability */
font-size: 38px !important;
} }
/* Reset all lyrics styling when disabled */ /* Reset all lyrics styling when disabled */
@@ -85,13 +115,22 @@
.lyrics-glow-disabled [class*="_lyricsText"] > div > span:hover, .lyrics-glow-disabled [class*="_lyricsText"] > div > span:hover,
.lyrics-glow-disabled [data-test="now-playing-track-title"], .lyrics-glow-disabled [data-test="now-playing-track-title"],
.lyrics-glow-disabled [class^="_lyricsContainer"] > div > div > span { .lyrics-glow-disabled [class^="_lyricsContainer"] > div > div > span {
text-shadow: none !important; /* biome-ignore lint: Hard reset when disabled */
padding-left: 0 !important; text-shadow: none !important;
transition: none !important; /* biome-ignore lint: Hard reset when disabled */
font-size: inherit !important; padding-left: 0 !important;
color: inherit !important; /* biome-ignore lint: Hard reset when disabled */
font-family: inherit !important; transition: none !important;
font-weight: inherit !important; /* biome-ignore lint: Hard reset when disabled */
margin-bottom: inherit !important; font-size: inherit !important;
opacity: inherit !important; /* biome-ignore lint: Hard reset when disabled */
} color: inherit !important;
/* biome-ignore lint: Hard reset when disabled */
font-family: inherit !important;
/* biome-ignore lint: Hard reset when disabled */
font-weight: inherit !important;
/* biome-ignore lint: Hard reset when disabled */
margin-bottom: inherit !important;
/* biome-ignore lint: Hard reset when disabled */
opacity: inherit !important;
}
@@ -1,15 +1,15 @@
/* Hide player bar when setting is disabled, but show on hover - only when UI is hidden */ /* Hide player bar when setting is disabled, but show on hover - only when UI is hidden */
.radiant-lyrics-ui-hidden [data-test="footer-player"] { .radiant-lyrics-ui-hidden [data-test="footer-player"] {
opacity: 0 !important; opacity: 0 !important;
transition: opacity 0.5s ease-in-out !important; transition: opacity 0.5s ease-in-out !important;
} }
.radiant-lyrics-ui-hidden [data-test="footer-player"]:hover { .radiant-lyrics-ui-hidden [data-test="footer-player"]:hover {
opacity: 1 !important; opacity: 1 !important;
} }
/* Also show player bar when hovering over the bottom area - only when UI is hidden */ /* Also show player bar when hovering over the bottom area - only when UI is hidden */
.radiant-lyrics-ui-hidden body.rl-footer-hover [data-test="footer-player"], .radiant-lyrics-ui-hidden body.rl-footer-hover [data-test="footer-player"],
.radiant-lyrics-ui-hidden [data-test="footer-player"]:hover { .radiant-lyrics-ui-hidden [data-test="footer-player"]:hover {
opacity: 1 !important; opacity: 1 !important;
} }
+124 -76
View File
@@ -1,30 +1,32 @@
/* Only apply styles when UI is hidden */ /* Only apply styles when UI is hidden */
.radiant-lyrics-ui-hidden [class*="tabItems"] { .radiant-lyrics-ui-hidden [class*="tabItems"] {
opacity: 0 !important; opacity: 0 !important;
transition: opacity 0.4s ease-in-out; transition: opacity 0.4s ease-in-out;
} }
/* Default state - visible */ /* Default state - visible */
[class*="tabItems"] { [class*="tabItems"] {
transition: opacity 0.4s ease-in-out; transition: opacity 0.4s ease-in-out;
} }
/* 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
opacity: 0 !important; [data-test="header-container"]:not(.rl-header-has-hide-btn) {
transition: opacity 0.4s ease-in-out; opacity: 0 !important;
transition: opacity 0.4s ease-in-out;
} }
/* Keep header visible if it contains the Hide UI button, but hide its other children */ /* 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
opacity: 0 !important; > *:not(.hide-ui-button) {
transition: opacity 0.4s ease-in-out; opacity: 0 !important;
transition: opacity 0.4s ease-in-out;
} }
/* Default state for header */ /* Default state for header */
[data-test="header-container"] { [data-test="header-container"] {
transition: opacity 0.4s ease-in-out; transition: opacity 0.4s ease-in-out;
} }
/* Only prevent specific text elements in player bar from being affected by margin adjustments */ /* Only prevent specific text elements in player bar from being affected by margin adjustments */
@@ -32,30 +34,30 @@
[data-test="footer-player"] [class*="_artistName"], [data-test="footer-player"] [class*="_artistName"],
[data-test="footer-player"] [class*="_trackInfo"], [data-test="footer-player"] [class*="_trackInfo"],
[data-test="footer-player"] [class*="_trackContainer"] { [data-test="footer-player"] [class*="_trackContainer"] {
margin-top: 0 !important; margin-top: 0 !important;
transform: none !important; transform: none !important;
} }
/* Immediate hide class for unhide button with smooth transition (had issues with the fade out.. so I removed it) */ /* Immediate hide class for unhide button with smooth transition (had issues with the fade out.. so I removed it) */
.hide-immediately { .hide-immediately {
opacity: 0 !important; opacity: 0 !important;
visibility: hidden !important; visibility: hidden !important;
pointer-events: none !important; pointer-events: none !important;
} }
[class^="_bar"] { [class^="_bar"] {
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;
} }
/* Hide search box and make it non-interactive */ /* Hide search box and make it non-interactive */
@@ -73,54 +75,81 @@
.radiant-lyrics-ui-hidden [data-test="main-layout-header"] [class*="input"], .radiant-lyrics-ui-hidden [data-test="main-layout-header"] [class*="input"],
.radiant-lyrics-ui-hidden header input, .radiant-lyrics-ui-hidden header input,
.radiant-lyrics-ui-hidden nav input { .radiant-lyrics-ui-hidden nav input {
pointer-events: none !important; pointer-events: none !important;
cursor: default !important; cursor: default !important;
user-select: none !important; user-select: none !important;
} }
/* 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
opacity: 0 !important; > div:first-child
pointer-events: none !important; button:not(.unhide-ui-button),
transition: opacity 0.5s ease-in-out !important; .radiant-lyrics-ui-hidden
[class*="_nowPlayingContainer"]
> div:first-child
button:not(.unhide-ui-button) {
opacity: 0 !important;
pointer-events: none !important;
transition: opacity 0.5s ease-in-out !important;
} }
/* No hover functionality in Hide UI Mode - buttons stay hidden.. yea thats right, you heard me */ /* No hover functionality in Hide UI Mode - buttons stay hidden.. yea thats right, you heard me */
/* Default state for control buttons */ /* Default state for control buttons */
[data-test="add-to-playlist"], [data-test="add-to-playlist"],
[data-test="remove-from-playlist"], [data-test="remove-from-playlist"],
[data-test="like-toggle"], [data-test="like-toggle"],
[data-test="dislike-toggle"], [data-test="dislike-toggle"],
[data-test="favorite-toggle"], [data-test="favorite-toggle"],
@@ -146,33 +175,38 @@ 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
transition: opacity 0.5s ease-in-out; > 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;
} }
/* Smooth cover art movement when UI is hidden */ /* Smooth cover art movement when UI is hidden */
[class*="_albumImage"], [class*="_albumImage"],
[class*="_coverArt"], [class*="_coverArt"],
figure[class*="_albumImage"] { figure[class*="_albumImage"] {
transition: transform 0.6s ease-in-out; transition: transform 0.6s ease-in-out;
} }
.radiant-lyrics-ui-hidden [class*="_albumImage"], .radiant-lyrics-ui-hidden [class*="_albumImage"],
.radiant-lyrics-ui-hidden [class*="_coverArt"], .radiant-lyrics-ui-hidden [class*="_coverArt"],
.radiant-lyrics-ui-hidden figure[class*="_albumImage"] { .radiant-lyrics-ui-hidden figure[class*="_albumImage"] {
transform: translateX(80px) !important; transform: translateX(80px) !important;
} }
/* Smooth track info wrapper movement when UI is hidden (Arists & Track Title) */ /* Smooth track info wrapper movement when UI is hidden (Arists & Track Title) */
[class*="_infoWrapper"], [class*="_infoWrapper"],
[class*="_textContainer"] { [class*="_textContainer"] {
transition: transform 0.6s ease-in-out; transition: transform 0.6s ease-in-out;
} }
.radiant-lyrics-ui-hidden [class*="_infoWrapper"], .radiant-lyrics-ui-hidden [class*="_infoWrapper"],
.radiant-lyrics-ui-hidden [class*="_textContainer"] { .radiant-lyrics-ui-hidden [class*="_textContainer"] {
transform: translateX(40px) !important; transform: translateX(40px) !important;
} }
/* Move parent containers instead of lyrics container directly to preserve gradient fade */ /* Move parent containers instead of lyrics container directly to preserve gradient fade */
@@ -183,7 +217,7 @@ figure[class*="_albumImage"] {
[class*="_sidePanel"], [class*="_sidePanel"],
[class*="_lyricsSection"], [class*="_lyricsSection"],
[class*="_lyricsWrapper"] { [class*="_lyricsWrapper"] {
transition: transform 0.6s ease-in-out; transition: transform 0.6s ease-in-out;
} }
.radiant-lyrics-ui-hidden [data-test="stream-metadata"], .radiant-lyrics-ui-hidden [data-test="stream-metadata"],
@@ -193,32 +227,46 @@ figure[class*="_albumImage"] {
.radiant-lyrics-ui-hidden [class*="_sidePanel"], .radiant-lyrics-ui-hidden [class*="_sidePanel"],
.radiant-lyrics-ui-hidden [class*="_lyricsSection"], .radiant-lyrics-ui-hidden [class*="_lyricsSection"],
.radiant-lyrics-ui-hidden [class*="_lyricsWrapper"] { .radiant-lyrics-ui-hidden [class*="_lyricsWrapper"] {
transform: translateX(60px) translateY(-70px) !important; transform: translateX(60px) translateY(-70px) !important;
} }
/* 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*/
.unhide-ui-button.auto-faded { .unhide-ui-button.auto-faded {
background-color: transparent !important; background-color: transparent !important;
border-color: transparent !important; border-color: transparent !important;
box-shadow: none !important; box-shadow: none !important;
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 */
.unhide-ui-button.auto-faded:hover { .unhide-ui-button.auto-faded:hover {
background-color: rgba(255, 255, 255, 0.2) !important; background-color: rgba(255, 255, 255, 0.2) !important;
border-color: rgba(255, 255, 255, 0.3) !important; border-color: rgba(255, 255, 255, 0.3) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
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;
}