From 99661096d5c3780fc11fe0f7b592e91303658c3e Mon Sep 17 00:00:00 2001 From: meowarex Date: Tue, 9 Sep 2025 17:59:47 +1000 Subject: [PATCH 01/10] BIOME Formating --- plugins/audio-visualizer-luna/package.json | 2 +- .../audio-visualizer-luna/src/Settings.tsx | 761 +++++----- plugins/audio-visualizer-luna/src/index.ts | 709 +++++----- plugins/audio-visualizer-luna/src/styles.css | 56 +- plugins/colorama-lyrics-luna/package.json | 19 +- plugins/colorama-lyrics-luna/src/Settings.tsx | 1238 +++++++++------- plugins/colorama-lyrics-luna/src/index.ts | 342 ++--- plugins/colorama-lyrics-luna/src/styles.css | 146 +- plugins/copy-lyrics-luna/package.json | 2 +- plugins/copy-lyrics-luna/src/index.ts | 161 ++- plugins/copy-lyrics-luna/src/styles.css | 12 +- plugins/element-hider-luna/package.json | 2 +- plugins/element-hider-luna/src/Settings.tsx | 4 +- plugins/element-hider-luna/src/index.ts | 412 +++--- plugins/element-hider-luna/src/styles.css | 72 +- plugins/oled-theme-luna/package.json | 2 +- plugins/oled-theme-luna/src/Settings.tsx | 25 +- plugins/oled-theme-luna/src/dark-theme.css | 269 ++-- plugins/oled-theme-luna/src/index.ts | 177 +-- plugins/oled-theme-luna/src/light-theme.css | 329 ++--- plugins/oled-theme-luna/src/oled-friendly.css | 186 +-- plugins/radiant-lyrics-luna/package.json | 2 +- plugins/radiant-lyrics-luna/src/Settings.tsx | 151 +- .../src/cover-everywhere.css | 168 +-- plugins/radiant-lyrics-luna/src/index.ts | 1253 +++++++++-------- .../radiant-lyrics-luna/src/lyrics-glow.css | 125 +- .../src/player-bar-hidden.css | 10 +- plugins/radiant-lyrics-luna/src/styles.css | 200 ++- 28 files changed, 3781 insertions(+), 3054 deletions(-) diff --git a/plugins/audio-visualizer-luna/package.json b/plugins/audio-visualizer-luna/package.json index d932cf7..d117625 100644 --- a/plugins/audio-visualizer-luna/package.json +++ b/plugins/audio-visualizer-luna/package.json @@ -8,4 +8,4 @@ }, "main": "./src/index.ts", "type": "module" -} \ No newline at end of file +} diff --git a/plugins/audio-visualizer-luna/src/Settings.tsx b/plugins/audio-visualizer-luna/src/Settings.tsx index 8855144..3d4fce0 100644 --- a/plugins/audio-visualizer-luna/src/Settings.tsx +++ b/plugins/audio-visualizer-luna/src/Settings.tsx @@ -1,355 +1,436 @@ import { ReactiveStore } from "@luna/core"; -import { LunaSettings, LunaNumberSetting, LunaSwitchSetting, LunaTextSetting } from "@luna/ui"; +import { + LunaSettings, + LunaNumberSetting, + LunaSwitchSetting, + LunaTextSetting, +} from "@luna/ui"; import React from "react"; -export const settings = await ReactiveStore.getPluginStorage("AudioVisualizer", { - barCount: 32, - barColor: "#ffffff", - barRounding: true, - customColors: [] as string[] -}); +export const settings = await ReactiveStore.getPluginStorage( + "AudioVisualizer", + { + barCount: 32, + barColor: "#ffffff", + barRounding: true, + customColors: [] as string[], + }, +); export const Settings = () => { - const [barCount, setBarCount] = React.useState(settings.barCount); - const [barColor, setBarColor] = React.useState(settings.barColor); - const [barRounding, setBarRounding] = React.useState(settings.barRounding); - const [showColorPicker, setShowColorPicker] = React.useState(false); - const [isAnimatingIn, setIsAnimatingIn] = React.useState(false); - const [shouldRender, setShouldRender] = React.useState(false); - const [customInput, setCustomInput] = React.useState(settings.barColor); - const [customColors, setCustomColors] = React.useState(settings.customColors); - const [hoveredColorIndex, setHoveredColorIndex] = React.useState(null); + const [barCount, setBarCount] = React.useState(settings.barCount); + const [barColor, setBarColor] = React.useState(settings.barColor); + const [barRounding, setBarRounding] = React.useState(settings.barRounding); + const [showColorPicker, setShowColorPicker] = React.useState(false); + const [isAnimatingIn, setIsAnimatingIn] = React.useState(false); + const [shouldRender, setShouldRender] = React.useState(false); + const [customInput, setCustomInput] = React.useState(settings.barColor); + const [customColors, setCustomColors] = React.useState(settings.customColors); + const [hoveredColorIndex, setHoveredColorIndex] = React.useState< + number | null + >(null); - const closeColorPicker = () => { - setIsAnimatingIn(false); - setTimeout(() => { - setShowColorPicker(false); - setShouldRender(false); - }, 200); // Wait for animation to complete because i need to - }; + const closeColorPicker = () => { + setIsAnimatingIn(false); + setTimeout(() => { + setShowColorPicker(false); + setShouldRender(false); + }, 200); // Wait for animation to complete because i need to + }; - const openColorPicker = () => { - setShowColorPicker(true); - setShouldRender(true); - setTimeout(() => setIsAnimatingIn(true), 10); - }; + const openColorPicker = () => { + setShowColorPicker(true); + setShouldRender(true); + setTimeout(() => setIsAnimatingIn(true), 10); + }; - React.useEffect(() => { - if (showColorPicker) { - setShouldRender(true); - setTimeout(() => setIsAnimatingIn(true), 10); - } - }, [showColorPicker]); + React.useEffect(() => { + if (showColorPicker) { + setShouldRender(true); + setTimeout(() => setIsAnimatingIn(true), 10); + } + }, [showColorPicker]); - // Common color presets for cool points :D - const colorPresets = [ - "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", - "#ff8800", "#8800ff", "#0088ff", "#88ff00", "#ff0088", "#00ff88", - "#444444", "#888888", "#cccccc", "#1db954", "#e22134", "#1976d2" - ]; + // Common color presets for cool points :D + const colorPresets = [ + "#ffffff", + "#ff0000", + "#00ff00", + "#0000ff", + "#ffff00", + "#ff00ff", + "#00ffff", + "#ff8800", + "#8800ff", + "#0088ff", + "#88ff00", + "#ff0088", + "#00ff88", + "#444444", + "#888888", + "#cccccc", + "#1db954", + "#e22134", + "#1976d2", + ]; - const updateColor = (color: string) => { - setBarColor(color); - setCustomInput(color); - settings.barColor = color; - (window as any).updateAudioVisualizer?.(); - }; + const updateColor = (color: string) => { + setBarColor(color); + setCustomInput(color); + settings.barColor = color; + (window as any).updateAudioVisualizer?.(); + }; - const addCustomColor = () => { - if (customInput) { - // Trim whitespace and convert to lowercase - 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 addCustomColor = () => { + if (customInput) { + // Trim whitespace and convert to lowercase + const trimmedInput = customInput.trim().toLowerCase(); - const removeCustomColor = (colorToRemove: string) => { - const newCustomColors = customColors.filter(color => color !== colorToRemove); - setCustomColors(newCustomColors); - settings.customColors = newCustomColors; - - // If the removed color was the selected color (reset to white) - if (barColor === colorToRemove) { - updateColor("#ffffff"); - } - }; + // Validate hex color format + const hexColorRegex = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i; - const allColors = [...colorPresets, ...customColors]; + if ( + hexColorRegex.test(trimmedInput) && + !colorPresets.includes(trimmedInput) && + !customColors.includes(trimmedInput) + ) { + const newCustomColors = [...customColors, trimmedInput]; + setCustomColors(newCustomColors); + settings.customColors = newCustomColors; + } + } + }; - return ( - - { - setBarRounding(checked); - settings.barRounding = checked; - (window as any).updateAudioVisualizer?.(); - }} - /> - - { - 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 */} - -
-
-
Bar Color
-
Color of the visualizer bars
-
-
- - - {/* Custom Color Picker Modal */} - {shouldRender && ( - <> - {/* Backdrop */} -
- - {/* Color Picker Panel */} -
-
- Choose Color -
- - {/* Color Grid */} -
- {allColors.map((color, index) => { - const isCustomColor = customColors.includes(color); - const isHovered = hoveredColorIndex === index; - return ( -
setHoveredColorIndex(index)} - onMouseLeave={() => setHoveredColorIndex(null)} - > - - )} -
- ); - })} -
- - {/* Custom Hex Input */} -
-
- Add Custom Color -
-
- 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" - }} - /> - -
-
- - {/* Close Button (Done) - Also runs when color chosen*/} - -
- - )} -
-
- + const removeCustomColor = (colorToRemove: string) => { + const newCustomColors = customColors.filter( + (color) => color !== colorToRemove, + ); + setCustomColors(newCustomColors); + settings.customColors = newCustomColors; - - ); -}; \ No newline at end of file + // If the removed color was the selected color (reset to white) + if (barColor === colorToRemove) { + updateColor("#ffffff"); + } + }; + + const allColors = [...colorPresets, ...customColors]; + + return ( + + { + setBarRounding(checked); + settings.barRounding = checked; + (window as any).updateAudioVisualizer?.(); + }} + /> + + { + 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 */} + +
+
+
+ Bar Color +
+
+ Color of the visualizer bars +
+
+
+ + + {/* Custom Color Picker Modal */} + {shouldRender && ( + <> + {/* Backdrop */} +
+ + {/* Color Picker Panel */} +
+
+ Choose Color +
+ + {/* Color Grid */} +
+ {allColors.map((color, index) => { + const isCustomColor = customColors.includes(color); + const isHovered = hoveredColorIndex === index; + return ( +
setHoveredColorIndex(index)} + onMouseLeave={() => setHoveredColorIndex(null)} + > + + )} +
+ ); + })} +
+ + {/* Custom Hex Input */} +
+
+ Add Custom Color +
+
+ 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", + }} + /> + +
+
+ + {/* Close Button (Done) - Also runs when color chosen*/} + +
+ + )} +
+
+ + ); +}; diff --git a/plugins/audio-visualizer-luna/src/index.ts b/plugins/audio-visualizer-luna/src/index.ts index e912aed..0a83bb3 100644 --- a/plugins/audio-visualizer-luna/src/index.ts +++ b/plugins/audio-visualizer-luna/src/index.ts @@ -10,21 +10,28 @@ export const { trace } = Tracer("[Audio Visualizer]"); // Helper function for consistent logging const log = (message: string) => console.log(`[Audio Visualizer] ${message}`); const warn = (message: string) => console.warn(`[Audio Visualizer] ${message}`); -const error = (message: string) => console.error(`[Audio Visualizer] ${message}`); +const error = (message: string) => + console.error(`[Audio Visualizer] ${message}`); export { Settings }; // Basic config with settings const config = { - enabled: true, - position: 'left' as 'left' | 'right', - width: 200, - height: 40, - get barCount() { return settings.barCount; }, - get color() { return settings.barColor; }, - get barRounding() { return settings.barRounding; }, - sensitivity: 1.5, - smoothing: 0.8, - visualizerType: 'bars' as 'bars' | 'waveform' | 'circular' + enabled: true, + position: "left" as "left" | "right", + width: 200, + height: 40, + get barCount() { + return settings.barCount; + }, + get color() { + return settings.barColor; + }, + get barRounding() { + return settings.barRounding; + }, + sensitivity: 1.5, + smoothing: 0.8, + visualizerType: "bars" as "bars" | "waveform" | "circular", }; // 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 const findAudioElement = (): HTMLAudioElement | null => { - // Try main selectors first - const selectors = [ - 'audio', - 'video', - 'audio[data-test]', - '[data-test="audio-player"] audio' - ]; + // Try main selectors first + const selectors = [ + "audio", + "video", + "audio[data-test]", + '[data-test="audio-player"] audio', + ]; - for (const selector of selectors) { - const element = document.querySelector(selector) as HTMLAudioElement; - if (element && (element.tagName === 'AUDIO' || element.tagName === 'VIDEO')) { - return element; - } - } + for (const selector of selectors) { + const element = document.querySelector(selector) as HTMLAudioElement; + if ( + element && + (element.tagName === "AUDIO" || element.tagName === "VIDEO") + ) { + return element; + } + } - // Quick scan for any audio elements - const audioElements = document.querySelectorAll('audio, video'); - for (const element of audioElements) { - const audioEl = element as HTMLAudioElement; - if (audioEl.src || audioEl.currentSrc) { - return audioEl; - } - } + // Quick scan for any audio elements + const audioElements = document.querySelectorAll("audio, video"); + for (const element of audioElements) { + const audioEl = element as HTMLAudioElement; + if (audioEl.src || audioEl.currentSrc) { + return audioEl; + } + } - return null; + return null; }; // Initialize audio visualization const initializeAudioVisualizer = async (): Promise => { - try { - // Find the audio element - const audioElement = findAudioElement(); - if (!audioElement) { - return; - } + try { + // Find the audio element + const audioElement = findAudioElement(); + if (!audioElement) { + return; + } - // create audio context - if (!audioContext) { - audioContext = new 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; - } - } + // create audio context + if (!audioContext) { + audioContext = new AudioContext(); + log("Created AudioContext"); + } - // 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 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"); + } - // 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); - } + // 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 + // (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 const createVisualizerUI = (): void => { - // Remove existing visualizer if it exists - removeVisualizerUI(); - - if (!config.enabled) return; + // Remove existing visualizer if it exists + removeVisualizerUI(); - // Find the search bar - const searchField = document.querySelector('input[class*="_searchField"]') as HTMLInputElement; - if (!searchField) { - warn("Search field not found"); - return; - } + if (!config.enabled) return; - const searchContainer = searchField.parentElement; - if (!searchContainer) { - warn("Search container not found"); - return; - } + // Find the search bar + const searchField = document.querySelector( + 'input[class*="_searchField"]', + ) as HTMLInputElement; + if (!searchField) { + warn("Search field not found"); + return; + } - // Create visualizer container - visualizerContainer = document.createElement('div'); - visualizerContainer.id = 'audio-visualizer-container'; - visualizerContainer.style.cssText = ` + const searchContainer = searchField.parentElement; + if (!searchContainer) { + warn("Search container not found"); + return; + } + + // Create visualizer container + visualizerContainer = document.createElement("div"); + visualizerContainer.id = "audio-visualizer-container"; + visualizerContainer.style.cssText = ` display: flex; align-items: center; justify-content: center; - margin-${config.position === 'left' ? 'right' : 'left'}: 12px; + margin-${config.position === "left" ? "right" : "left"}: 12px; background: rgba(0, 0, 0, 0.2); border-radius: 8px; padding: 4px; @@ -178,154 +192,168 @@ const createVisualizerUI = (): void => { -webkit-backdrop-filter: blur(10px); `; - // Create canvas - canvas = document.createElement('canvas'); - canvas.width = config.width; - canvas.height = config.height; - canvas.style.cssText = ` + // Create canvas + canvas = document.createElement("canvas"); + canvas.width = config.width; + canvas.height = config.height; + canvas.style.cssText = ` width: ${config.width}px; height: ${config.height}px; border-radius: 4px; `; - visualizerContainer.appendChild(canvas); - canvasContext = canvas.getContext('2d'); + visualizerContainer.appendChild(canvas); + canvasContext = canvas.getContext("2d"); - // Insert visualizer next to search bar - if (config.position === 'left') { - searchContainer.parentElement?.insertBefore(visualizerContainer, searchContainer); - } else { - searchContainer.parentElement?.insertBefore(visualizerContainer, searchContainer.nextSibling); - } + // Insert visualizer next to search bar + if (config.position === "left") { + searchContainer.parentElement?.insertBefore( + visualizerContainer, + searchContainer, + ); + } else { + searchContainer.parentElement?.insertBefore( + visualizerContainer, + searchContainer.nextSibling, + ); + } }; // Remove visualizer UI const removeVisualizerUI = (): void => { - if (visualizerContainer) { - visualizerContainer.remove(); - visualizerContainer = null; - canvas = null; - canvasContext = null; - } + if (visualizerContainer) { + visualizerContainer.remove(); + visualizerContainer = null; + canvas = null; + canvasContext = null; + } }; // Animation loop for rendering visualizer const animate = (): void => { - if (!canvasContext || !canvas) { - animationId = null; - return; - } + if (!canvasContext || !canvas) { + animationId = null; + return; + } - // Update canvas color in case it changed - canvasContext.fillStyle = config.color; - canvasContext.strokeStyle = config.color; + // Update canvas color in case it changed + canvasContext.fillStyle = config.color; + canvasContext.strokeStyle = config.color; - // Check if we have real audio data - this might not be needed but its a good idea - let hasRealAudio = false; - if (analyser && dataArray) { - analyser.getByteFrequencyData(dataArray); - // Check if there's actual audio signal (not just silence) - const avgVolume = dataArray.reduce((sum, val) => sum + val, 0) / dataArray.length; - hasRealAudio = avgVolume > 5; // Threshold for detecting actual audio - } + // Check if we have real audio data - this might not be needed but its a good idea + let hasRealAudio = false; + if (analyser && dataArray) { + analyser.getByteFrequencyData(dataArray); + // Check if there's actual audio signal (not just silence) + const avgVolume = + dataArray.reduce((sum, val) => sum + val, 0) / dataArray.length; + hasRealAudio = avgVolume > 5; // Threshold for detecting actual audio + } - // Clear canvas - canvasContext.clearRect(0, 0, canvas.width, canvas.height); + // Clear canvas + canvasContext.clearRect(0, 0, canvas.width, canvas.height); - if (hasRealAudio && analyser && dataArray) { - // Draw real audio visualization - switch (config.visualizerType) { - case 'bars': // Is implemented YAYYY (default) - drawBars(); - break; - case 'waveform': // Not implemented yet - drawWaveform(); - break; - case 'circular': // Not implemented yet - drawCircular(); - break; - } - } else { - // Draw cool scrolling wave effect when no audio - drawScrollingWave(); - } + if (hasRealAudio && analyser && dataArray) { + // Draw real audio visualization + switch (config.visualizerType) { + case "bars": // Is implemented YAYYY (default) + drawBars(); + break; + case "waveform": // Not implemented yet + drawWaveform(); + break; + case "circular": // Not implemented yet + drawCircular(); + break; + } + } else { + // Draw cool scrolling wave effect when no audio + drawScrollingWave(); + } - animationId = requestAnimationFrame(animate); + animationId = requestAnimationFrame(animate); }; // Global wave animation state let waveTime = 0; // Helper function to draw rounded rectangles -const drawRoundedRect = (ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number): void => { - ctx.beginPath(); - ctx.roundRect(x, y, width, height, radius); - ctx.fill(); +const drawRoundedRect = ( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + width: number, + height: number, + radius: number, +): void => { + ctx.beginPath(); + ctx.roundRect(x, y, width, height, radius); + ctx.fill(); }; // Draw scrolling wave effect when no audio is detected const drawScrollingWave = (): void => { - if (!canvasContext || !canvas) return; + if (!canvasContext || !canvas) return; - waveTime += 0.05; // Speed of wave animation - - const barCount = config.barCount; - const barWidth = canvas.width / barCount; - const maxHeight = canvas.height * 0.6; - - canvasContext.fillStyle = config.color; - - for (let i = 0; i < barCount; i++) { - // Create a sine wave that scrolls back and forth - const x = i / barCount; - 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 wave3 = Math.sin(x * Math.PI * 6 + waveTime * 0.7) * 0.1; - - // Combine waves for complex pattern - const combinedWave = (wave1 + wave2 + wave3 + 1) / 2; // Normalize to 0-1 - - // Add a traveling wave effect - const travelWave = Math.sin(x * Math.PI * 3 - waveTime * 2) * 0.5 + 0.5; - - // Final height calculation - const barHeight = maxHeight * combinedWave * travelWave * 0.8 + 2; // Minimum height of 2px - - const xPos = i * barWidth; - const yPos = (canvas.height - barHeight) / 2; - - // Draw rounded or square bars based on setting - if (config.barRounding) { - drawRoundedRect(canvasContext, xPos, yPos, barWidth - 1, barHeight, 2); - } else { - canvasContext.fillRect(xPos, yPos, barWidth - 1, barHeight); - } - } + waveTime += 0.05; // Speed of wave animation + + const barCount = config.barCount; + const barWidth = canvas.width / barCount; + const maxHeight = canvas.height * 0.6; + + canvasContext.fillStyle = config.color; + + for (let i = 0; i < barCount; i++) { + // Create a sine wave that scrolls back and forth + const x = i / barCount; + 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 wave3 = Math.sin(x * Math.PI * 6 + waveTime * 0.7) * 0.1; + + // Combine waves for complex pattern + const combinedWave = (wave1 + wave2 + wave3 + 1) / 2; // Normalize to 0-1 + + // Add a traveling wave effect + const travelWave = Math.sin(x * Math.PI * 3 - waveTime * 2) * 0.5 + 0.5; + + // Final height calculation + const barHeight = maxHeight * combinedWave * travelWave * 0.8 + 2; // Minimum height of 2px + + const xPos = i * barWidth; + const yPos = (canvas.height - barHeight) / 2; + + // Draw rounded or square bars based on setting + if (config.barRounding) { + drawRoundedRect(canvasContext, xPos, yPos, barWidth - 1, barHeight, 2); + } else { + canvasContext.fillRect(xPos, yPos, barWidth - 1, barHeight); + } + } }; // Draw frequency bars - default const drawBars = (): void => { - if (!canvasContext || !dataArray || !canvas) return; + if (!canvasContext || !dataArray || !canvas) return; - const barWidth = canvas.width / config.barCount; - const heightScale = canvas.height / 255; + const barWidth = canvas.width / config.barCount; + const heightScale = canvas.height / 255; - canvasContext.fillStyle = config.color; + canvasContext.fillStyle = config.color; - for (let i = 0; i < config.barCount; i++) { - const dataIndex = Math.floor(i * (dataArray.length / config.barCount)); - const barHeight = (dataArray[dataIndex] * config.sensitivity * heightScale); - - const x = i * barWidth; - const y = canvas.height - barHeight; - - // Draw rounded or square bars based on setting - if (config.barRounding) { - drawRoundedRect(canvasContext, x, y, barWidth - 1, barHeight, 2); - } else { - canvasContext.fillRect(x, y, barWidth - 1, barHeight); - } - } + for (let i = 0; i < config.barCount; i++) { + const dataIndex = Math.floor(i * (dataArray.length / config.barCount)); + const barHeight = dataArray[dataIndex] * config.sensitivity * heightScale; + + const x = i * barWidth; + const y = canvas.height - barHeight; + + // Draw rounded or square bars based on setting + if (config.barRounding) { + drawRoundedRect(canvasContext, x, y, barWidth - 1, barHeight, 2); + } else { + canvasContext.fillRect(x, y, barWidth - 1, barHeight); + } + } }; // Draw waveform visualization - NOT IMPLEMENTED YET @@ -342,10 +370,10 @@ const drawBars = (): void => { // for (let i = 0; i < config.barCount; i++) { // const dataIndex = Math.floor(i * (dataArray.length / config.barCount)); // const amplitude = (dataArray[dataIndex] - 128) * config.sensitivity * amplitudeScale; - + // const x = (i / config.barCount) * canvas.width; // const y = centerY + amplitude; - + // if (i === 0) { // canvasContext.moveTo(x, y); // } else { @@ -370,13 +398,13 @@ const drawBars = (): void => { // for (let i = 0; i < config.barCount; i++) { // const dataIndex = Math.floor(i * (dataArray.length / config.barCount)); // const amplitude = (dataArray[dataIndex] * config.sensitivity) / 255; - + // const angle = (i / config.barCount) * Math.PI * 2; // const startX = centerX + Math.cos(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 endY = centerY + Math.sin(angle) * radius * (0.7 + amplitude * 0.3); - + // canvasContext.beginPath(); // canvasContext.moveTo(startX, startY); // canvasContext.lineTo(endX, endY); @@ -386,22 +414,22 @@ const drawBars = (): void => { // Update visualizer settings const updateAudioVisualizer = (): void => { - if (analyser) { - // use a fixed size that provides enough frequency bins - analyser.fftSize = 512; // Fixed power of 2 - important - analyser.smoothingTimeConstant = config.smoothing; - dataArray = new Uint8Array(analyser.frequencyBinCount); - } + if (analyser) { + // use a fixed size that provides enough frequency bins + analyser.fftSize = 512; // Fixed power of 2 - important + analyser.smoothingTimeConstant = config.smoothing; + dataArray = new Uint8Array(analyser.frequencyBinCount); + } - if (canvas) { - canvas.width = config.width; - canvas.height = config.height; - canvas.style.width = `${config.width}px`; - canvas.style.height = `${config.height}px`; - } + if (canvas) { + canvas.width = config.width; + canvas.height = config.height; + canvas.style.width = `${config.width}px`; + canvas.style.height = `${config.height}px`; + } - // Recreate UI if position changed - createVisualizerUI(); + // Recreate UI if position changed + createVisualizerUI(); }; // Make updateAudioVisualizer available globally for settings @@ -409,120 +437,121 @@ const updateAudioVisualizer = (): void => { // Clean up function const cleanupAudioVisualizer = (): void => { - // stop animation and hide UI - don't touch audio connections (otherwise it will reconnect) - if (animationId) { - cancelAnimationFrame(animationId); - animationId = null; - } - - removeVisualizerUI(); - - // i was killing audio connections - But it was reconnecting and being a pain - // so i just left it alone - it works fine + // stop animation and hide UI - don't touch audio connections (otherwise it will reconnect) + if (animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } + + removeVisualizerUI(); + + // i was killing audio connections - But it was reconnecting and being a pain + // so i just left it alone - it works fine }; // Initialize when DOM is ready and track is playing const observePlayState = (): void => { - let hasTriedInitialization = false; - 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(); - } - }; + let hasTriedInitialization = false; + let checkCount = 0; - // 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(); + 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 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 const initialize = (): void => { - log("Audio Visualizer plugin initializing..."); - - // Start immediately - DOM should be ready by plugin load - setTimeout(() => { - log("Starting visualizer..."); - // Create UI immediately so wave effect shows - createVisualizerUI(); - // Start animation loop immediately - animate(); - // Also observe play state for audio detection - observePlayState(); - }, 100); // Minimal delay to ensure DOM is ready + log("Audio Visualizer plugin initializing..."); + + // Start immediately - DOM should be ready by plugin load + setTimeout(() => { + log("Starting visualizer..."); + // Create UI immediately so wave effect shows + createVisualizerUI(); + // Start animation loop immediately + animate(); + // Also observe play state for audio detection + observePlayState(); + }, 100); // Minimal delay to ensure DOM is ready }; // Complete cleanup function for plugin unload const completeCleanup = (): void => { - log("Complete cleanup - plugin unloading"); - - if (animationId) { - cancelAnimationFrame(animationId); - animationId = null; - } - - removeVisualizerUI(); - - // Fully disconnect and reset everything - if (audioSource) { - try { - audioSource.disconnect(); - log("Disconnected audio source completely"); - } catch (e) { - log("Audio source already disconnected"); - } - } - - // Close audio context completely on plugin unload - if (audioContext && audioContext.state !== 'closed') { - audioContext.close(); - log("Closed AudioContext"); - } - - // Reset all references - audioContext = null; - analyser = null; - audioSource = null; - dataArray = null; - currentAudioElement = null; - isSourceConnected = false; + log("Complete cleanup - plugin unloading"); + + if (animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } + + removeVisualizerUI(); + + // Fully disconnect and reset everything + if (audioSource) { + try { + audioSource.disconnect(); + log("Disconnected audio source completely"); + } catch (e) { + log("Audio source already disconnected"); + } + } + + // Close audio context completely on plugin unload + if (audioContext && audioContext.state !== "closed") { + audioContext.close(); + log("Closed AudioContext"); + } + + // Reset all references + audioContext = null; + analyser = null; + audioSource = null; + dataArray = null; + currentAudioElement = null; + isSourceConnected = false; }; // Register cleanup unloads.add(completeCleanup); // Start initialization -initialize(); \ No newline at end of file +initialize(); diff --git a/plugins/audio-visualizer-luna/src/styles.css b/plugins/audio-visualizer-luna/src/styles.css index 0e2a26d..967a6a2 100644 --- a/plugins/audio-visualizer-luna/src/styles.css +++ b/plugins/audio-visualizer-luna/src/styles.css @@ -1,56 +1,56 @@ /* Audio Visualizer CSS - Only applies to the Visualizer */ #audio-visualizer-container { - transition: all 0.3s ease-in-out; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); - border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease-in-out; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); } #audio-visualizer-container:hover { - transform: scale(1.02); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + transform: scale(1.02); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); } #audio-visualizer-container canvas { - display: block; - transition: all 0.3s ease-in-out; + display: block; + transition: all 0.3s ease-in-out; } /* Responsive adjustments */ @media (max-width: 768px) { - #audio-visualizer-container { - margin: 4px; - padding: 2px; - } - - #audio-visualizer-container canvas { - max-width: 150px; - max-height: 30px; - } + #audio-visualizer-container { + margin: 4px; + padding: 2px; + } + + #audio-visualizer-container canvas { + max-width: 150px; + max-height: 30px; + } } /* Where to put the thingy */ [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 */ #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 */ @keyframes fadeIn { - from { - opacity: 0; - transform: scale(0.8); - } - to { - opacity: 1; - transform: scale(1); - } + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } } #audio-visualizer-container { - animation: fadeIn 0.5s ease-out; -} \ No newline at end of file + animation: fadeIn 0.5s ease-out; +} diff --git a/plugins/colorama-lyrics-luna/package.json b/plugins/colorama-lyrics-luna/package.json index 122a98e..3b292f8 100644 --- a/plugins/colorama-lyrics-luna/package.json +++ b/plugins/colorama-lyrics-luna/package.json @@ -1,12 +1,11 @@ { - "name": "@meowarex/colorama-lyrics", - "description": "Customize lyrics colors: single, gradient & auto from cover art", - "author": { - "name": "meowarex", - "url": "https://github.com/meowarex", - "avatarUrl": "https://avatars.githubusercontent.com/u/90243579" - }, - "main": "./src/index.ts", - "type": "module" + "name": "@meowarex/colorama-lyrics", + "description": "Customize lyrics colors: single, gradient & auto from cover art", + "author": { + "name": "meowarex", + "url": "https://github.com/meowarex", + "avatarUrl": "https://avatars.githubusercontent.com/u/90243579" + }, + "main": "./src/index.ts", + "type": "module" } - diff --git a/plugins/colorama-lyrics-luna/src/Settings.tsx b/plugins/colorama-lyrics-luna/src/Settings.tsx index eb01980..c46521d 100644 --- a/plugins/colorama-lyrics-luna/src/Settings.tsx +++ b/plugins/colorama-lyrics-luna/src/Settings.tsx @@ -2,515 +2,775 @@ import { ReactiveStore } from "@luna/core"; import { LunaSettings, LunaSwitchSetting } from "@luna/ui"; import React from "react"; -export type ColoramaMode = "single" | "gradient-experimental" | "cover" | "cover-gradient"; +export type ColoramaMode = + | "single" + | "gradient-experimental" + | "cover" + | "cover-gradient"; export const settings = await ReactiveStore.getPluginStorage("ColoramaLyrics", { - enabled: true, - mode: "single" as ColoramaMode, - // Store colors as RGB hex (#RRGGBB) and opacity separately (0-100) - singleColor: "#FFFFFF", - singleAlpha: 100, - gradientStart: "#FFFFFF", - gradientStartAlpha: 100, - gradientEnd: "#AAFFFF", - gradientEndAlpha: 100, - gradientAngle: 0, - customColors: [] as string[], - excludeInactive: false + enabled: true, + mode: "single" as ColoramaMode, + // Store colors as RGB hex (#RRGGBB) and opacity separately (0-100) + singleColor: "#FFFFFF", + singleAlpha: 100, + gradientStart: "#FFFFFF", + gradientStartAlpha: 100, + gradientEnd: "#AAFFFF", + gradientEndAlpha: 100, + gradientAngle: 0, + customColors: [] as string[], + excludeInactive: false, }); export const Settings = () => { - const [enabled, setEnabled] = React.useState(settings.enabled); - const [mode, setMode] = React.useState(settings.mode); - const [singleColor, setSingleColor] = React.useState(settings.singleColor); - const [singleAlpha, setSingleAlpha] = React.useState(settings.singleAlpha ?? 100); - const [gradientStart, setGradientStart] = React.useState(settings.gradientStart); - const [gradientStartAlpha, setGradientStartAlpha] = React.useState(settings.gradientStartAlpha ?? 100); - const [gradientEnd, setGradientEnd] = React.useState(settings.gradientEnd); - const [gradientEndAlpha, setGradientEndAlpha] = React.useState(settings.gradientEndAlpha ?? 100); - const [gradientAngle, setGradientAngle] = React.useState(settings.gradientAngle); - const [customInput, setCustomInput] = React.useState(settings.singleColor); - const [customColors, setCustomColors] = React.useState(settings.customColors); - const [showPicker, setShowPicker] = React.useState(false); - const [isAnimatingIn, setIsAnimatingIn] = React.useState(false); - const [shouldRender, setShouldRender] = React.useState(false); - const [excludeInactive, setExcludeInactive] = React.useState(settings.excludeInactive); - const [activeEndpoint, setActiveEndpoint] = React.useState<'single' | 'start' | 'end'>('single'); - const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType; + const [enabled, setEnabled] = React.useState(settings.enabled); + const [mode, setMode] = React.useState(settings.mode); + const [singleColor, setSingleColor] = React.useState(settings.singleColor); + const [singleAlpha, setSingleAlpha] = React.useState( + settings.singleAlpha ?? 100, + ); + const [gradientStart, setGradientStart] = React.useState( + settings.gradientStart, + ); + const [gradientStartAlpha, setGradientStartAlpha] = React.useState( + settings.gradientStartAlpha ?? 100, + ); + const [gradientEnd, setGradientEnd] = React.useState(settings.gradientEnd); + const [gradientEndAlpha, setGradientEndAlpha] = React.useState( + settings.gradientEndAlpha ?? 100, + ); + const [gradientAngle, setGradientAngle] = React.useState( + settings.gradientAngle, + ); + const [customInput, setCustomInput] = React.useState(settings.singleColor); + const [customColors, setCustomColors] = React.useState(settings.customColors); + const [showPicker, setShowPicker] = React.useState(false); + const [isAnimatingIn, setIsAnimatingIn] = React.useState(false); + const [shouldRender, setShouldRender] = React.useState(false); + const [excludeInactive, setExcludeInactive] = React.useState( + settings.excludeInactive, + ); + const [activeEndpoint, setActiveEndpoint] = React.useState< + "single" | "start" | "end" + >("single"); + const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType; - // Helper for HEX normalization - const normalizeToRGB = (hex: string, fallback: string = "#FFFFFF"): string => { - let v = hex.trim().toLowerCase(); - if (!v.startsWith('#')) v = `#${v}`; - // #rgb or #rgba -> expand - if (/^#([0-9a-f]{3,4})$/.test(v)) { - const m = v.slice(1); - const r = m[0]; - const g = m[1]; - const b = m[2]; - // ignore alpha if provided (#rgba) - return `#${r}${r}${g}${g}${b}${b}`.toUpperCase(); - } - // #aarrggbb -> strip alpha - if (/^#([0-9a-f]{8})$/.test(v)) { - const rrggbb = v.slice(3); - return `#${rrggbb}`.toUpperCase(); - } - // #rrggbb - if (/^#([0-9a-f]{6})$/.test(v)) return v.toUpperCase(); - return fallback; - }; + // Helper for HEX normalization + const normalizeToRGB = ( + hex: string, + fallback: string = "#FFFFFF", + ): string => { + let v = hex.trim().toLowerCase(); + if (!v.startsWith("#")) v = `#${v}`; + // #rgb or #rgba -> expand + if (/^#([0-9a-f]{3,4})$/.test(v)) { + const m = v.slice(1); + const r = m[0]; + const g = m[1]; + const b = m[2]; + // ignore alpha if provided (#rgba) + return `#${r}${r}${g}${g}${b}${b}`.toUpperCase(); + } + // #aarrggbb -> strip alpha + if (/^#([0-9a-f]{8})$/.test(v)) { + const rrggbb = v.slice(3); + return `#${rrggbb}`.toUpperCase(); + } + // #rrggbb + if (/^#([0-9a-f]{6})$/.test(v)) return v.toUpperCase(); + return fallback; + }; - const colorPresets = [ - "#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", - "#FF8800", "#8800FF", "#0088FF", "#88FF00", "#FF0088", "#00FF88", - "#444444", "#888888", "#CCCCCC", "#1DB954", "#E22134", "#1976D2" - ]; + const colorPresets = [ + "#FFFFFF", + "#FF0000", + "#00FF00", + "#0000FF", + "#FFFF00", + "#FF00FF", + "#00FFFF", + "#FF8800", + "#8800FF", + "#0088FF", + "#88FF00", + "#FF0088", + "#00FF88", + "#444444", + "#888888", + "#CCCCCC", + "#1DB954", + "#E22134", + "#1976D2", + ]; - const openPicker = (endpoint: 'single' | 'start' | 'end' = 'single') => { - setActiveEndpoint(endpoint); - setShowPicker(true); - setShouldRender(true); - setTimeout(() => setIsAnimatingIn(true), 10); - }; - const closePicker = () => { - setIsAnimatingIn(false); - setTimeout(() => { - setShowPicker(false); - setShouldRender(false); - }, 200); - }; + const openPicker = (endpoint: "single" | "start" | "end" = "single") => { + setActiveEndpoint(endpoint); + setShowPicker(true); + setShouldRender(true); + setTimeout(() => setIsAnimatingIn(true), 10); + }; + const closePicker = () => { + setIsAnimatingIn(false); + setTimeout(() => { + setShowPicker(false); + setShouldRender(false); + }, 200); + }; - const hexColorRegex = /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3,4})$/i; + const hexColorRegex = /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3,4})$/i; - const applyCustomInputColor = (raw: string, updateInput: boolean): void => { - const trimmed = raw.trim(); - if (!hexColorRegex.test(trimmed)) return; - if (mode === "single") { - const next = normalizeToRGB(trimmed); - setSingleColor((settings.singleColor = next)); - if (updateInput) setCustomInput(next); - } else if (mode === "gradient-experimental") { - const norm = normalizeToRGB(trimmed); - if (activeEndpoint === 'end') { - setGradientEnd((settings.gradientEnd = norm)); - } else { - setGradientStart((settings.gradientStart = norm)); - } - if (updateInput) setCustomInput(norm); - } - requestApply(); - }; + const applyCustomInputColor = (raw: string, updateInput: boolean): void => { + const trimmed = raw.trim(); + if (!hexColorRegex.test(trimmed)) return; + if (mode === "single") { + const next = normalizeToRGB(trimmed); + setSingleColor((settings.singleColor = next)); + if (updateInput) setCustomInput(next); + } else if (mode === "gradient-experimental") { + const norm = normalizeToRGB(trimmed); + if (activeEndpoint === "end") { + setGradientEnd((settings.gradientEnd = norm)); + } else { + setGradientStart((settings.gradientStart = norm)); + } + if (updateInput) setCustomInput(norm); + } + requestApply(); + }; - const addCustomColor = () => { - const trimmed = customInput.trim(); - if ( - hexColorRegex.test(trimmed) && - !colorPresets.includes(trimmed) && - !customColors.includes(normalizeToRGB(trimmed)) - ) { - const updated = [...customColors, normalizeToRGB(trimmed)]; - setCustomColors(updated); - settings.customColors = updated; - } - }; + const addCustomColor = () => { + const trimmed = customInput.trim(); + if ( + hexColorRegex.test(trimmed) && + !colorPresets.includes(trimmed) && + !customColors.includes(normalizeToRGB(trimmed)) + ) { + const updated = [...customColors, normalizeToRGB(trimmed)]; + setCustomColors(updated); + settings.customColors = updated; + } + }; - const removeCustomColor = (color: string) => { - const updated = customColors.filter(c => c !== color); - setCustomColors(updated); - settings.customColors = updated; - }; + const removeCustomColor = (color: string) => { + const updated = customColors.filter((c) => c !== color); + setCustomColors(updated); + settings.customColors = updated; + }; - const allColors = [...colorPresets, ...customColors]; + const allColors = [...colorPresets, ...customColors]; - const requestApply = () => { - (window as any).applyColoramaLyrics?.(); - }; + const requestApply = () => { + (window as any).applyColoramaLyrics?.(); + }; - return ( - + return ( + + {/* Mode selection via dropdown (aligned right) */} +
+
+
Mode
+
+ Choose how lyrics are colored +
+
+ +
- {/* Mode selection via dropdown (aligned right) */} -
-
-
Mode
-
Choose how lyrics are colored
-
- -
+ {/* Single color */} +
+
+
+ Lyrics Color +
+
Set lyrics color
+
+
+
+
- {/* Single color */} -
-
-
Lyrics Color
-
Set lyrics color
-
-
-
-
- + {/* Gradient controls (open picker) */} +
+
+
+ Gradient (Experimental) +
+
Set colors & angle
+
+ +
- {/* Gradient controls (open picker) */} -
-
-
Gradient (Experimental)
-
Set colors & angle
-
- -
+ {/* Cover gradient controls (open picker for angle) */} +
+
+
+ Cover (Gradient) - Experimental +
+
Set angle
+
+ +
- {/* Cover gradient controls (open picker for angle) */} -
-
-
Cover (Gradient) - Experimental
-
Set angle
-
- -
+ {/* Modal for picking and managing colors (reused) */} + {shouldRender && ( + <> +
+
+
+ {mode === "single" ? "Single Color" : "Gradient Colors"} +
+ {mode === "gradient-experimental" && ( +
+
+ Editing +
+ + +
+ )} + {mode !== "cover-gradient" && ( +
+ {allColors.map((color, index) => ( +
+ )} + {mode !== "cover-gradient" && ( +
+
+ Custom Hex (#RRGGBB) +
+
+ setCustomInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + applyCustomInputColor(customInput, true); + addCustomColor(); + } + }} + placeholder="#RRGGBB" + style={{ + flex: 1, + padding: "8px 12px", + borderRadius: 6, + border: "1px solid rgba(255,255,255,0.2)", + background: "rgba(255,255,255,0.1)", + color: "#fff", + fontSize: 14, + fontFamily: "monospace", + boxSizing: "border-box", + }} + /> + +
+
+ )} + {/* Sliders inside picker based on mode */} + {mode === "single" && ( +
+
+ Alpha +
+ { + const value = Number(e.target.value); + setSingleAlpha((settings.singleAlpha = value)); + requestApply(); + }} + style={{ width: "100%" }} + /> +
+ )} - {/* Modal for picking and managing colors (reused) */} - {shouldRender && ( - <> -
-
-
- {mode === 'single' ? 'Single Color' : 'Gradient Colors'} -
- {mode === 'gradient-experimental' && ( -
-
Editing
- - -
- )} - {mode !== 'cover-gradient' && ( -
- {allColors.map((color, index) => ( -
- )} - {mode !== 'cover-gradient' && ( -
-
Custom Hex (#RRGGBB)
-
- setCustomInput(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - applyCustomInputColor(customInput, true); - addCustomColor(); - } - }} - placeholder="#RRGGBB" - style={{ - flex: 1, - padding: "8px 12px", - borderRadius: 6, - border: "1px solid rgba(255,255,255,0.2)", - background: "rgba(255,255,255,0.1)", - color: "#fff", - fontSize: 14, - fontFamily: "monospace", - boxSizing: "border-box" - }} - /> - -
-
- )} - {/* Sliders inside picker based on mode */} - {mode === 'single' && ( -
-
Alpha
- { - const value = Number(e.target.value); - setSingleAlpha((settings.singleAlpha = value)); - requestApply(); - }} - style={{ width: '100%' }} - /> -
- )} + {mode === "gradient-experimental" && ( +
+
+
+
+
+ Start Alpha +
+
+ { + const value = Number(e.target.value); + setGradientStartAlpha( + (settings.gradientStartAlpha = value), + ); + requestApply(); + }} + style={{ width: "100%" }} + /> +
+
+
+
+
+ End Alpha +
+
+ { + const value = Number(e.target.value); + setGradientEndAlpha((settings.gradientEndAlpha = value)); + requestApply(); + }} + style={{ width: "100%" }} + /> +
+
+
+
+ Angle +
+
+ {gradientAngle}° +
+
+ { + const value = Number(e.target.value); + setGradientAngle((settings.gradientAngle = value)); + requestApply(); + }} + style={{ width: "100%" }} + /> +
+
+ )} - {mode === 'gradient-experimental' && ( -
-
-
-
-
Start Alpha
-
- { - const value = Number(e.target.value); - setGradientStartAlpha((settings.gradientStartAlpha = value)); - requestApply(); - }} - style={{ width: '100%' }} - /> -
-
-
-
-
End Alpha
-
- { - const value = Number(e.target.value); - setGradientEndAlpha((settings.gradientEndAlpha = value)); - requestApply(); - }} - style={{ width: '100%' }} - /> -
-
-
-
Angle
-
{gradientAngle}°
-
- { - const value = Number(e.target.value); - setGradientAngle((settings.gradientAngle = value)); - requestApply(); - }} - style={{ width: '100%' }} - /> -
-
- )} + {mode === "cover-gradient" && ( +
+
+
+ Angle +
+
+ {gradientAngle}° +
+
+ { + const value = Number(e.target.value); + setGradientAngle((settings.gradientAngle = value)); + requestApply(); + }} + style={{ width: "100%" }} + /> +
+ )} - {mode === 'cover-gradient' && ( -
-
-
Angle
-
{gradientAngle}°
-
- { - const value = Number(e.target.value); - setGradientAngle((settings.gradientAngle = value)); - requestApply(); - }} - style={{ width: '100%' }} - /> -
- )} - - -
- - )} - { - setExcludeInactive((settings.excludeInactive = checked)); - requestApply(); - }} - /> - - ); + +
+ + )} + { + setExcludeInactive((settings.excludeInactive = checked)); + requestApply(); + }} + /> + + ); }; - - diff --git a/plugins/colorama-lyrics-luna/src/index.ts b/plugins/colorama-lyrics-luna/src/index.ts index b7c83d1..7750d3f 100644 --- a/plugins/colorama-lyrics-luna/src/index.ts +++ b/plugins/colorama-lyrics-luna/src/index.ts @@ -13,181 +13,195 @@ new StyleTag("ColoramaLyrics", unloads, styles); // Simple dominant color extraction from current cover art async function getCoverArtElement(): Promise { - const img = document.querySelector('figure[class*="_albumImage"] > div > div > div > img') as HTMLImageElement | null; - if (img) return img; - const video = document.querySelector('figure[class*="_albumImage"] > div > div > div > video') as HTMLVideoElement | null; - if (video) { - const poster = video.getAttribute("poster"); - if (!poster) return null; - const tempImg = new Image(); - tempImg.crossOrigin = "anonymous"; - tempImg.src = poster; - await new Promise((resolve) => { - tempImg.onload = () => resolve(); - tempImg.onerror = () => resolve(); - }); - return tempImg as unknown as HTMLImageElement; - } - return null; + const img = document.querySelector( + 'figure[class*="_albumImage"] > div > div > div > img', + ) as HTMLImageElement | null; + if (img) return img; + const video = document.querySelector( + 'figure[class*="_albumImage"] > div > div > div > video', + ) as HTMLVideoElement | null; + if (video) { + const poster = video.getAttribute("poster"); + if (!poster) return null; + const tempImg = new Image(); + tempImg.crossOrigin = "anonymous"; + tempImg.src = poster; + await new Promise((resolve) => { + tempImg.onload = () => resolve(); + tempImg.onerror = () => resolve(); + }); + return tempImg as unknown as HTMLImageElement; + } + return null; } -function getDominantColorsFromImage(img: HTMLImageElement, count: number = 2): string[] { - try { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - if (!ctx) return ["#ffffff", "#88aaff"]; // fallback - const w = 64; - const h = 64; - canvas.width = w; - canvas.height = h; - ctx.drawImage(img, 0, 0, w, h); - const data = ctx.getImageData(0, 0, w, h).data; +function getDominantColorsFromImage( + img: HTMLImageElement, + count: number = 2, +): string[] { + try { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + if (!ctx) return ["#ffffff", "#88aaff"]; // fallback + const w = 64; + const h = 64; + canvas.width = w; + canvas.height = h; + ctx.drawImage(img, 0, 0, w, h); + const data = ctx.getImageData(0, 0, w, h).data; - // Simple k-means-ish binning into 16 buckets per channel - const buckets = new Map(); - for (let i = 0; i < data.length; i += 4) { - const r = data[i]; - const g = data[i + 1]; - const b = data[i + 2]; - const key = `${Math.round(r/16)},${Math.round(g/16)},${Math.round(b/16)}`; - buckets.set(key, (buckets.get(key) ?? 0) + 1); - } - const sorted = [...buckets.entries()].sort((a, b) => b[1] - a[1]); - const picked = sorted.slice(0, Math.max(1, count)).map(([key]) => { - const [r, g, b] = key.split(',').map(v => parseInt(v, 10) * 16); - return `#${[r, g, b].map(v => Math.max(0, Math.min(255, v)).toString(16).padStart(2, '0')).join('')}`; - }); - return picked; - } catch { - return ["#ffffff", "#88aaff"]; // fallback - } + // Simple k-means-ish binning into 16 buckets per channel + const buckets = new Map(); + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const key = `${Math.round(r / 16)},${Math.round(g / 16)},${Math.round(b / 16)}`; + buckets.set(key, (buckets.get(key) ?? 0) + 1); + } + const sorted = [...buckets.entries()].sort((a, b) => b[1] - a[1]); + const picked = sorted.slice(0, Math.max(1, count)).map(([key]) => { + const [r, g, b] = key.split(",").map((v) => parseInt(v, 10) * 16); + return `#${[r, g, b].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")}`; + }); + return picked; + } catch { + return ["#ffffff", "#88aaff"]; // fallback + } } // build rgba() from hex + alpha percentage function hexToRgb(hex: string): { r: number; g: number; b: number } | null { - let v = hex.trim(); - if (!v.startsWith('#')) v = `#${v}`; - if (/^#([0-9a-fA-F]{3})$/.test(v)) { - const r = parseInt(v[1] + v[1], 16); - const g = parseInt(v[2] + v[2], 16); - const b = parseInt(v[3] + v[3], 16); - return { r, g, b }; - } - if (/^#([0-9a-fA-F]{6})$/.test(v)) { - const r = parseInt(v.slice(1, 3), 16); - const g = parseInt(v.slice(3, 5), 16); - const b = parseInt(v.slice(5, 7), 16); - return { r, g, b }; - } - // 8-digit hex expects #AARRGGBB. Indices 1-3 are the alpha byte (ignored here), - // so r/g/b are extracted from v.slice(3,5), v.slice(5,7), v.slice(7,9) respectively. - if (/^#([0-9a-fA-F]{8})$/.test(v)) { - const r = parseInt(v.slice(3, 5), 16); - const g = parseInt(v.slice(5, 7), 16); - const b = parseInt(v.slice(7, 9), 16); - return { r, g, b }; - } - return null; + let v = hex.trim(); + if (!v.startsWith("#")) v = `#${v}`; + if (/^#([0-9a-fA-F]{3})$/.test(v)) { + const r = parseInt(v[1] + v[1], 16); + const g = parseInt(v[2] + v[2], 16); + const b = parseInt(v[3] + v[3], 16); + return { r, g, b }; + } + if (/^#([0-9a-fA-F]{6})$/.test(v)) { + const r = parseInt(v.slice(1, 3), 16); + const g = parseInt(v.slice(3, 5), 16); + const b = parseInt(v.slice(5, 7), 16); + return { r, g, b }; + } + // 8-digit hex expects #AARRGGBB. Indices 1-3 are the alpha byte (ignored here), + // so r/g/b are extracted from v.slice(3,5), v.slice(5,7), v.slice(7,9) respectively. + if (/^#([0-9a-fA-F]{8})$/.test(v)) { + const r = parseInt(v.slice(3, 5), 16); + const g = parseInt(v.slice(5, 7), 16); + const b = parseInt(v.slice(7, 9), 16); + return { r, g, b }; + } + return null; } -function rgbaFromHexAndAlpha(hex: string, alphaPercent: number | undefined): string { - const rgb = hexToRgb(hex); - const a = Math.max(0.05, Math.min(100, alphaPercent ?? 100)) / 100; - if (!rgb) return `rgba(255,255,255,${a})`; - return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`; +function rgbaFromHexAndAlpha( + hex: string, + alphaPercent: number | undefined, +): string { + const rgb = hexToRgb(hex); + const a = Math.max(0.05, Math.min(100, alphaPercent ?? 100)) / 100; + if (!rgb) return `rgba(255,255,255,${a})`; + return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`; } function applySingleColor(color: string) { - const alpha = (settings as any).singleAlpha ?? 100; - const rgba = rgbaFromHexAndAlpha(color, alpha); - document.documentElement.style.setProperty('--cl-lyrics-color', rgba); - document.documentElement.style.setProperty('--cl-glow1', rgba); - document.documentElement.style.setProperty('--cl-glow2', rgba); - document.documentElement.style.removeProperty('--cl-grad-start'); - document.documentElement.style.removeProperty('--cl-grad-end'); - document.documentElement.style.removeProperty('--cl-grad-angle'); - document.body.classList.remove('colorama-gradient'); - document.body.classList.add('colorama-single'); + const alpha = (settings as any).singleAlpha ?? 100; + const rgba = rgbaFromHexAndAlpha(color, alpha); + document.documentElement.style.setProperty("--cl-lyrics-color", rgba); + document.documentElement.style.setProperty("--cl-glow1", rgba); + document.documentElement.style.setProperty("--cl-glow2", rgba); + document.documentElement.style.removeProperty("--cl-grad-start"); + document.documentElement.style.removeProperty("--cl-grad-end"); + document.documentElement.style.removeProperty("--cl-grad-angle"); + document.body.classList.remove("colorama-gradient"); + document.body.classList.add("colorama-single"); } function applyGradient(start: string, end: string, angle: number) { - const startAlpha = (settings as any).gradientStartAlpha ?? 100; - const endAlpha = (settings as any).gradientEndAlpha ?? 100; - const startRgba = rgbaFromHexAndAlpha(start, startAlpha); - const endRgba = rgbaFromHexAndAlpha(end, endAlpha); - document.documentElement.style.setProperty('--cl-grad-start', startRgba); - document.documentElement.style.setProperty('--cl-grad-end', endRgba); - document.documentElement.style.setProperty('--cl-grad-angle', `${angle}deg`); - document.documentElement.style.setProperty('--cl-glow1', startRgba); - document.documentElement.style.setProperty('--cl-glow2', endRgba); - document.body.classList.remove('colorama-single'); - document.body.classList.add('colorama-gradient'); + const startAlpha = (settings as any).gradientStartAlpha ?? 100; + const endAlpha = (settings as any).gradientEndAlpha ?? 100; + const startRgba = rgbaFromHexAndAlpha(start, startAlpha); + const endRgba = rgbaFromHexAndAlpha(end, endAlpha); + document.documentElement.style.setProperty("--cl-grad-start", startRgba); + document.documentElement.style.setProperty("--cl-grad-end", endRgba); + document.documentElement.style.setProperty("--cl-grad-angle", `${angle}deg`); + document.documentElement.style.setProperty("--cl-glow1", startRgba); + document.documentElement.style.setProperty("--cl-glow2", endRgba); + document.body.classList.remove("colorama-single"); + document.body.classList.add("colorama-gradient"); } function resetModeClasses(): void { - document.body.classList.remove('colorama-single', 'colorama-gradient'); + document.body.classList.remove("colorama-single", "colorama-gradient"); } async function applyCoverColors(gradient: boolean) { - const img = await getCoverArtElement(); - if (!img) return; - const colors = getDominantColorsFromImage(img, gradient ? 2 : 1); - if (gradient) { - const start = colors[0] ?? settings.gradientStart; - const end = colors[1] ?? settings.gradientEnd; - applyGradient(start, end, settings.gradientAngle); - } else { - const color = colors[0] ?? settings.singleColor; - applySingleColor(color); - } + const img = await getCoverArtElement(); + if (!img) return; + const colors = getDominantColorsFromImage(img, gradient ? 2 : 1); + if (gradient) { + const start = colors[0] ?? settings.gradientStart; + const end = colors[1] ?? settings.gradientEnd; + applyGradient(start, end, settings.gradientAngle); + } else { + const color = colors[0] ?? settings.singleColor; + applySingleColor(color); + } } function applyColoramaLyrics(): void { - if (!settings.enabled) { - document.body.classList.remove('colorama-single', 'colorama-gradient'); - return; - } + if (!settings.enabled) { + document.body.classList.remove("colorama-single", "colorama-gradient"); + return; + } - // Toggle only-active-line mode class - if (settings.excludeInactive) { - document.body.classList.add('colorama-only-active'); - } else { - document.body.classList.remove('colorama-only-active'); - } - resetModeClasses(); - switch (settings.mode) { - case "single": - applySingleColor(settings.singleColor); - break; - case "gradient-experimental": - applyGradient(settings.gradientStart, settings.gradientEnd, settings.gradientAngle); - break; - case "cover": - applyCoverColors(false); - break; - case "cover-gradient": - applyCoverColors(true); - break; - } + // Toggle only-active-line mode class + if (settings.excludeInactive) { + document.body.classList.add("colorama-only-active"); + } else { + document.body.classList.remove("colorama-only-active"); + } + resetModeClasses(); + switch (settings.mode) { + case "single": + applySingleColor(settings.singleColor); + break; + case "gradient-experimental": + applyGradient( + settings.gradientStart, + settings.gradientEnd, + settings.gradientAngle, + ); + break; + case "cover": + applyCoverColors(false); + break; + case "cover-gradient": + applyCoverColors(true); + break; + } } (window as any).applyColoramaLyrics = applyColoramaLyrics; // Re-apply on track changes (for auto modes) function observeTrackChanges(): void { - let lastTrackId: string | null = null; - const check = () => { - const currentTrackId = PlayState.playbackContext?.actualProductId; - if (currentTrackId && currentTrackId !== lastTrackId) { - lastTrackId = currentTrackId; - if (settings.mode === 'cover' || settings.mode === 'cover-gradient') { - setTimeout(() => applyColoramaLyrics(), 200); - } - } - }; - const interval = setInterval(check, 500); - unloads.add(() => clearInterval(interval)); - check(); + let lastTrackId: string | null = null; + const check = () => { + const currentTrackId = PlayState.playbackContext?.actualProductId; + if (currentTrackId && currentTrackId !== lastTrackId) { + lastTrackId = currentTrackId; + if (settings.mode === "cover" || settings.mode === "cover-gradient") { + setTimeout(() => applyColoramaLyrics(), 200); + } + } + }; + const interval = setInterval(check, 500); + unloads.add(() => clearInterval(interval)); + check(); } // Initial apply and observers @@ -196,26 +210,26 @@ observeTrackChanges(); // for some reason, re-apply after Radiant updates its styles/backgrounds function hookRadiantUpdates(): void { - const w = window as any; - const wrap = (name: string) => { - const fn = w[name]; - if (typeof fn === 'function' && !fn.__coloramaPatched) { - const orig = fn.bind(w); - const patched = (...args: unknown[]) => { - const result = orig(...args); - try { applyColoramaLyrics(); } catch {} - return result; - }; - (patched as any).__coloramaPatched = true; - w[name] = patched; - } - }; - wrap('updateRadiantLyricsStyles'); - wrap('updateRadiantLyricsNowPlayingBackground'); - wrap('updateRadiantLyricsGlobalBackground'); - wrap('updateRadiantLyricsTextGlow'); + const w = window as any; + const wrap = (name: string) => { + const fn = w[name]; + if (typeof fn === "function" && !fn.__coloramaPatched) { + const orig = fn.bind(w); + const patched = (...args: unknown[]) => { + const result = orig(...args); + try { + applyColoramaLyrics(); + } catch {} + return result; + }; + (patched as any).__coloramaPatched = true; + w[name] = patched; + } + }; + wrap("updateRadiantLyricsStyles"); + wrap("updateRadiantLyricsNowPlayingBackground"); + wrap("updateRadiantLyricsGlobalBackground"); + wrap("updateRadiantLyricsTextGlow"); } setTimeout(() => hookRadiantUpdates(), 0); - - diff --git a/plugins/colorama-lyrics-luna/src/styles.css b/plugins/colorama-lyrics-luna/src/styles.css index 494acf1..6c6596a 100644 --- a/plugins/colorama-lyrics-luna/src/styles.css +++ b/plugins/colorama-lyrics-luna/src/styles.css @@ -1,94 +1,138 @@ /* Variables used by Colorama Lyrics */ :root { - --cl-lyrics-color: #ffffff; - --cl-grad-start: #ffffff; - --cl-grad-end: #88aaff; - --cl-grad-angle: 0deg; - --cl-glow1: #ffffff; - --cl-glow2: #ffffff; + --cl-lyrics-color: #ffffff; + --cl-grad-start: #ffffff; + --cl-grad-end: #88aaff; + --cl-grad-angle: 0deg; + --cl-glow1: #ffffff; + --cl-glow2: #ffffff; } /* Apply solid color to lyrics text */ .colorama-single [class*="_lyricsText"] > div > span, .colorama-single [class*="_lyricsText"] > div > span[data-current="true"], .colorama-single [class^="_lyricsContainer"] > div > div > span, -.colorama-single [class^="_lyricsContainer"] > div > div > span[data-current="true"] { - color: var(--cl-lyrics-color) !important; - background: none !important; - -webkit-background-clip: initial !important; - background-clip: initial !important; - -webkit-text-fill-color: initial !important; +.colorama-single + [class^="_lyricsContainer"] + > div + > div + > span[data-current="true"] { + 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 */ .colorama-gradient [class*="_lyricsText"] > div > span, .colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"], .colorama-gradient [class^="_lyricsContainer"] > div > div > span, -.colorama-gradient [class^="_lyricsContainer"] > div > div > span[data-current="true"] { - background: linear-gradient(var(--cl-grad-angle), var(--cl-grad-start), var(--cl-grad-end)) !important; - -webkit-background-clip: text !important; - background-clip: text !important; - color: transparent !important; - -webkit-text-fill-color: transparent !important; +.colorama-gradient + [class^="_lyricsContainer"] + > div + > div + > span[data-current="true"] { + background: linear-gradient( + var(--cl-grad-angle), + var(--cl-grad-start), + var(--cl-grad-end) + ) !important; + -webkit-background-clip: text !important; + background-clip: text !important; + color: transparent !important; + -webkit-text-fill-color: transparent !important; } /* Only-active: apply container class only on the active line via JS */ /* Slight emphasis on current line (uniform to single mode) */ .colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"], -.colorama-gradient [class^="_lyricsContainer"] > div > div > span[data-current="true"] { - filter: brightness(1.1) !important; +.colorama-gradient + [class^="_lyricsContainer"] + > div + > div + > span[data-current="true"] { + filter: brightness(1.1) !important; } /* Keep song title color unchanged; its glow is controlled in Radiant CSS */ /* Color Radiant glow shadows using Colorama colors (respect RL sizes) */ .colorama-single [class*="_lyricsText"] > div > span[data-current="true"], -.colorama-single [class^="_lyricsContainer"] > div > div > span[data-current="true"], +.colorama-single + [class^="_lyricsContainer"] + > div + > div + > span[data-current="true"], .colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"], -.colorama-gradient [class^="_lyricsContainer"] > div > div > span[data-current="true"], -.colorama-gradient [class^="_lyricsContainer"] > div > div > span[data-current="true"] { - text-shadow: 0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff), 0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important; +.colorama-gradient + [class^="_lyricsContainer"] + > div + > div + > span[data-current="true"], +.colorama-gradient + [class^="_lyricsContainer"] + > div + > div + > span[data-current="true"] { + text-shadow: + 0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff), + 0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important; } /* Hover: force glow color to match Colorama settings for inactive lines */ .colorama-single [class*="_lyricsText"] > div > span:hover, .colorama-single [class^="_lyricsContainer"] > div > div > span:hover { - color: var(--cl-lyrics-color) !important; - text-shadow: 0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff), 0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important; + 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; } .colorama-gradient [class*="_lyricsText"] > div > span:hover, .colorama-gradient [class^="_lyricsContainer"] > div > div > span:hover { - background: linear-gradient(var(--cl-grad-angle), var(--cl-grad-start), var(--cl-grad-end)) !important; - -webkit-background-clip: text !important; - background-clip: text !important; - color: transparent !important; - -webkit-text-fill-color: transparent !important; - /* Do not increase glow strength on hover for gradients */ + background: linear-gradient( + var(--cl-grad-angle), + var(--cl-grad-start), + var(--cl-grad-end) + ) !important; + -webkit-background-clip: text !important; + background-clip: text !important; + color: transparent !important; + -webkit-text-fill-color: transparent !important; + /* Do not increase glow strength on hover for gradients */ } /* Only color active line mode */ -body.colorama-only-active.colorama-single [class*="_lyricsText"] > div > span:not([data-current="true"]), -body.colorama-only-active.colorama-gradient [class*="_lyricsText"] > div > span:not([data-current="true"]) { - /* Match Radiant inactive styling */ - color: rgba(128, 128, 128, 0.4) !important; - background: none !important; - -webkit-background-clip: initial !important; - background-clip: initial !important; - -webkit-text-fill-color: initial !important; - text-shadow: initial !important; +body.colorama-only-active.colorama-single [class*="_lyricsText"] + > div + > span:not([data-current="true"]), +body.colorama-only-active.colorama-gradient + [class*="_lyricsText"] + > div + > span:not([data-current="true"]) { + /* Match Radiant inactive styling */ + color: rgba(128, 128, 128, 0.4) !important; + background: none !important; + -webkit-background-clip: initial !important; + 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 */ -body.colorama-only-active.colorama-single [class*="_lyricsText"] > div > span:not([data-current="true"]):hover, -body.colorama-only-active.colorama-gradient [class*="_lyricsText"] > div > span:not([data-current="true"]):hover { - color: lightgray !important; - background: none !important; - -webkit-background-clip: initial !important; - background-clip: initial !important; - -webkit-text-fill-color: initial !important; - text-shadow: initial !important; +body.colorama-only-active.colorama-single [class*="_lyricsText"] + > div + > span:not([data-current="true"]):hover, +body.colorama-only-active.colorama-gradient + [class*="_lyricsText"] + > div + > span:not([data-current="true"]):hover { + color: lightgray !important; + background: none !important; + -webkit-background-clip: initial !important; + background-clip: initial !important; + -webkit-text-fill-color: initial !important; + text-shadow: initial !important; } - - diff --git a/plugins/copy-lyrics-luna/package.json b/plugins/copy-lyrics-luna/package.json index 76210fa..e20f18e 100644 --- a/plugins/copy-lyrics-luna/package.json +++ b/plugins/copy-lyrics-luna/package.json @@ -8,4 +8,4 @@ }, "main": "./src/index.ts", "type": "module" -} \ No newline at end of file +} diff --git a/plugins/copy-lyrics-luna/src/index.ts b/plugins/copy-lyrics-luna/src/index.ts index 4c8cc13..f03d40e 100644 --- a/plugins/copy-lyrics-luna/src/index.ts +++ b/plugins/copy-lyrics-luna/src/index.ts @@ -13,95 +13,102 @@ export const unloads = new Set(); const lyricsStyleTag = new StyleTag("Copy-Lyrics", unloads, unlockSelection); function SetClipboard(text: string): void { - const textarea = document.createElement("textarea"); - textarea.value = text; - textarea.style.position = "fixed"; // Avoid scrolling to bottom - document.body.appendChild(textarea); - textarea.select(); + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; // Avoid scrolling to bottom + document.body.appendChild(textarea); + textarea.select(); - try { - const success = document.execCommand("copy"); - if (!success) throw new Error("Failed to copy text."); - } catch (err) { - trace.msg.err(err instanceof Error ? err.message : String(err)); - } finally { - document.body.removeChild(textarea); - } + try { + const success = document.execCommand("copy"); + if (!success) throw new Error("Failed to copy text."); + } catch (err) { + trace.msg.err(err instanceof Error ? err.message : String(err)); + } finally { + document.body.removeChild(textarea); + } } let isSelecting = false; const onMouseDown = function (): void { - isSelecting = true; + isSelecting = true; }; const onMouseUp = function (event: MouseEvent): void { - if (isSelecting) { - const selection = window.getSelection(); - if (selection && selection.toString().length > 0) { - const selectedSpans: HTMLSpanElement[] = []; - const range = selection.getRangeAt(0); - let container = 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; - } - } + if (isSelecting) { + const selection = window.getSelection(); + if (selection && selection.toString().length > 0) { + const selectedSpans: HTMLSpanElement[] = []; + const range = selection.getRangeAt(0); + let container = range.commonAncestorContainer; - // Get all the spans inside the container. - const spans = (container as Element).getElementsByTagName("span"); - for (let span of spans) { - if (selection.containsNode(span, true)) { - selectedSpans.push(span as HTMLSpanElement); - } - } + // 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; + } + } - // 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"; - } - } - }); + // Get all the spans inside the container. + const spans = (container as Element).getElementsByTagName("span"); + for (let span of spans) { + if (selection.containsNode(span, true)) { + selectedSpans.push(span as HTMLSpanElement); + } + } - text = text.trim(); + // 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"; + } + } + }); - if (hasCorrectAttribute) { - SetClipboard(text); - trace.msg.log("Copied to clipboard!"); - selection.removeAllRanges(); - } - } - isSelecting = false; - } + text = text.trim(); + + if (hasCorrectAttribute) { + SetClipboard(text); + trace.msg.log("Copied to clipboard!"); + selection.removeAllRanges(); + } + } + isSelecting = false; + } }; const onClickHooked = function (event: MouseEvent): boolean | void { - if (!isSelecting) return; + if (!isSelecting) return; - const target = event.target as HTMLElement; - if (target.tagName.toLowerCase() === "span" && target.hasAttribute("data-current")) { - // Prevent default behavior and stop event propagation - event.preventDefault(); - event.stopPropagation(); - event.stopImmediatePropagation(); - return false; - } + const target = event.target as HTMLElement; + if ( + target.tagName.toLowerCase() === "span" && + target.hasAttribute("data-current") + ) { + // Prevent default behavior and stop event propagation + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + return false; + } }; // Add event listener with capture phase to intercept events before they reach other handlers @@ -111,8 +118,8 @@ document.addEventListener("mouseup", onMouseUp); // Add cleanup to unloads unloads.add(() => { - // Remove event listeners - document.removeEventListener("click", onClickHooked, true); - document.removeEventListener("mousedown", onMouseDown); - document.removeEventListener("mouseup", onMouseUp); -}); \ No newline at end of file + // Remove event listeners + document.removeEventListener("click", onClickHooked, true); + document.removeEventListener("mousedown", onMouseDown); + document.removeEventListener("mouseup", onMouseUp); +}); diff --git a/plugins/copy-lyrics-luna/src/styles.css b/plugins/copy-lyrics-luna/src/styles.css index deb70db..c84ab74 100644 --- a/plugins/copy-lyrics-luna/src/styles.css +++ b/plugins/copy-lyrics-luna/src/styles.css @@ -1,9 +1,9 @@ -[class^="_lyricsText"]>div>span { - user-select: text; - cursor: text; +[class^="_lyricsText"] > div > span { + user-select: text; + cursor: text; } ::selection { - background: rgb(72, 0, 60); - color: rgb(255, 255, 255); -} \ No newline at end of file + background: rgb(72, 0, 60); + color: rgb(255, 255, 255); +} diff --git a/plugins/element-hider-luna/package.json b/plugins/element-hider-luna/package.json index 8c2ba51..14975ca 100644 --- a/plugins/element-hider-luna/package.json +++ b/plugins/element-hider-luna/package.json @@ -8,4 +8,4 @@ }, "main": "./src/index.ts", "type": "module" -} \ No newline at end of file +} diff --git a/plugins/element-hider-luna/src/Settings.tsx b/plugins/element-hider-luna/src/Settings.tsx index dc0f2cc..fa4342a 100644 --- a/plugins/element-hider-luna/src/Settings.tsx +++ b/plugins/element-hider-luna/src/Settings.tsx @@ -9,9 +9,9 @@ export const settings = await ReactiveStore.getPluginStorage("ElementHider", { className: string; textContent: string; timestamp: number; - }> + }>, }); export const Settings = () => { return null; -}; \ No newline at end of file +}; diff --git a/plugins/element-hider-luna/src/index.ts b/plugins/element-hider-luna/src/index.ts index 1ac08e5..98252be 100644 --- a/plugins/element-hider-luna/src/index.ts +++ b/plugins/element-hider-luna/src/index.ts @@ -30,39 +30,54 @@ function generateElementSelector(element: HTMLElement): string { if (element.id) { return `#${element.id}`; } - + // Priority 2: data-test attribute (very specific for Tidal <3) - const dataTest = element.getAttribute('data-test'); + const dataTest = element.getAttribute("data-test"); if (dataTest) { return `[data-test="${dataTest}"]`; } - + // Priority 3: Combination of tag + specific classes + position let selector = element.tagName.toLowerCase(); - + // Get filtered classes (exclude our temporary classes) - const classes = element.className ? element.className.trim().split(/\s+/).filter(cls => { - return cls.length > 0 && - !cls.startsWith('element-hider-') && - cls !== 'element-hider-target' && - cls !== 'element-hider-hiding' && - cls !== 'element-hider-hidden'; - }) : []; - + const classes = element.className + ? element.className + .trim() + .split(/\s+/) + .filter((cls) => { + return ( + cls.length > 0 && + !cls.startsWith("element-hider-") && + cls !== "element-hider-target" && + cls !== "element-hider-hiding" && + cls !== "element-hider-hidden" + ); + }) + : []; + // Only use classes if we have them and they're not generic and dumb if (classes.length > 0) { // Use ALL classes to be very specific - selector += '.' + classes.join('.'); - + selector += "." + classes.join("."); + // Add parent context for extra specificity (for when the element is inside another element) const parent = element.parentElement; - if (parent && parent.tagName !== 'BODY' && parent.tagName !== 'HTML') { - const parentClasses = parent.className ? parent.className.trim().split(/\s+/).filter(cls => { - return cls.length > 0 && !cls.startsWith('element-hider-'); - }) : []; - + if (parent && parent.tagName !== "BODY" && parent.tagName !== "HTML") { + const parentClasses = parent.className + ? parent.className + .trim() + .split(/\s+/) + .filter((cls) => { + return cls.length > 0 && !cls.startsWith("element-hider-"); + }) + : []; + if (parentClasses.length > 0) { - const parentSelector = parent.tagName.toLowerCase() + '.' + parentClasses.slice(0, 2).join('.'); + const parentSelector = + parent.tagName.toLowerCase() + + "." + + parentClasses.slice(0, 2).join("."); selector = `${parentSelector} > ${selector}`; } } @@ -70,26 +85,36 @@ function generateElementSelector(element: HTMLElement): string { // If no useful classes, use position-based selector with parent context const parent = element.parentElement; if (parent) { - const siblings = Array.from(parent.children).filter(child => child.tagName === element.tagName); + const siblings = Array.from(parent.children).filter( + (child) => child.tagName === element.tagName, + ); const index = siblings.indexOf(element); if (index >= 0) { selector += `:nth-of-type(${index + 1})`; - + // Add parent context - if (parent.tagName !== 'BODY' && parent.tagName !== 'HTML') { - const parentClasses = parent.className ? parent.className.trim().split(/\s+/).filter(cls => { - return cls.length > 0 && !cls.startsWith('element-hider-'); - }) : []; - + if (parent.tagName !== "BODY" && parent.tagName !== "HTML") { + const parentClasses = parent.className + ? parent.className + .trim() + .split(/\s+/) + .filter((cls) => { + return cls.length > 0 && !cls.startsWith("element-hider-"); + }) + : []; + if (parentClasses.length > 0) { - const parentSelector = parent.tagName.toLowerCase() + '.' + parentClasses.slice(0, 2).join('.'); + const parentSelector = + parent.tagName.toLowerCase() + + "." + + parentClasses.slice(0, 2).join("."); selector = `${parentSelector} > ${selector}`; } } } } } - + trace.log(`Generated specific selector: ${selector}`); return selector; } @@ -100,16 +125,16 @@ function saveHiddenElement(element: HTMLElement): void { const elementInfo = { selector: selector, tagName: element.tagName, - className: element.className || '', - textContent: element.textContent?.substring(0, 100) || '', - timestamp: Date.now() + className: element.className || "", + textContent: element.textContent?.substring(0, 100) || "", + timestamp: Date.now(), }; - + // Check if element is already saved const existingIndex = settings.hiddenElements.findIndex( - stored => stored.selector === elementInfo.selector + (stored) => stored.selector === elementInfo.selector, ); - + if (existingIndex === -1) { settings.hiddenElements.push(elementInfo); trace.log(`Saved element: ${elementInfo.selector}`); @@ -122,8 +147,10 @@ function saveHiddenElement(element: HTMLElement): void { // Remove hidden element from persistent storage (for unhiding) function removeSavedElement(element: HTMLElement): void { const selector = generateElementSelector(element); - const index = settings.hiddenElements.findIndex(stored => stored.selector === selector); - + const index = settings.hiddenElements.findIndex( + (stored) => stored.selector === selector, + ); + if (index !== -1) { settings.hiddenElements.splice(index, 1); trace.log(`Permanently removed: ${selector}`); @@ -143,58 +170,67 @@ function matchesStoredSelector(element: HTMLElement): boolean { trace.warn(`Invalid selector: ${storedElement.selector}`, error); } } - + return false; } // Hide element directly without animation function hideElementDirectly(element: HTMLElement): void { if (hiddenElements.has(element)) return; - + element.classList.add("element-hider-hidden"); hiddenElements.add(element); hiddenElementsArray.push(element); - trace.log(`Hidden element: ${element.tagName}${element.className ? '.' + element.className.split(' ')[0] : ''}`); + trace.log( + `Hidden element: ${element.tagName}${element.className ? "." + element.className.split(" ")[0] : ""}`, + ); } // Hide the target element with animation function hideTargetElement(): void { if (!targetElement) return; - - trace.log(`Hiding with animation: ${targetElement.tagName}${targetElement.className ? '.' + targetElement.className.split(' ')[0] : ''}`); - + + trace.log( + `Hiding with animation: ${targetElement.tagName}${targetElement.className ? "." + targetElement.className.split(" ")[0] : ""}`, + ); + // Add hiding animation class targetElement.classList.add("element-hider-hiding"); - + // Store reference to the element const elementToHide = targetElement; - + // Save to persistent storage saveHiddenElement(elementToHide); - + // Wait for animation to complete, then hide setTimeout(() => { elementToHide.classList.add("element-hider-hidden"); - elementToHide.classList.remove("element-hider-hiding", "element-hider-target"); + elementToHide.classList.remove( + "element-hider-hiding", + "element-hider-target", + ); hiddenElements.add(elementToHide); hiddenElementsArray.push(elementToHide); }, 300); - + // Clear target reference targetElement = null; } // Unhide all elements permanently (remove from storage) function unhideAllElements(): void { - trace.log(`Permanently unhiding ${settings.hiddenElements.length} saved elements`); - + trace.log( + `Permanently unhiding ${settings.hiddenElements.length} saved elements`, + ); + // Show all currently hidden elements - hiddenElementsArray.forEach(element => { + hiddenElementsArray.forEach((element) => { if (document.body.contains(element)) { element.classList.remove("element-hider-hidden", "element-hider-hiding"); } }); - + // Clear both storage and runtime collections settings.hiddenElements = []; hiddenElements = new WeakSet(); @@ -204,36 +240,42 @@ function unhideAllElements(): void { // Process all elements in the document to hide matching ones (with strict matching) function processAllElements(): void { if (settings.hiddenElements.length === 0) return; - - trace.log(`Scanning document for ${settings.hiddenElements.length} stored selectors`); + + trace.log( + `Scanning document for ${settings.hiddenElements.length} stored selectors`, + ); let hiddenCount = 0; - + // Use querySelectorAll for each stored selector with validation settings.hiddenElements.forEach((storedElement, index) => { try { trace.log(`Searching for: ${storedElement.selector}`); const elements = document.querySelectorAll(storedElement.selector); trace.log(`Found ${elements.length} matches for selector ${index + 1}`); - + // Limit to prevent over-hiding (safety check) if (elements.length > 10) { - trace.warn(`Selector too broad (${elements.length} matches), skipping: ${storedElement.selector}`); + trace.warn( + `Selector too broad (${elements.length} matches), skipping: ${storedElement.selector}`, + ); return; } - + elements.forEach((element, elemIndex) => { const htmlElement = element as HTMLElement; if (!hiddenElements.has(htmlElement)) { hideElementDirectly(htmlElement); hiddenCount++; - trace.log(`Hid element ${elemIndex + 1}/${elements.length} for selector ${index + 1}`); + trace.log( + `Hid element ${elemIndex + 1}/${elements.length} for selector ${index + 1}`, + ); } }); } catch (error) { trace.warn(`Invalid selector: ${storedElement.selector}`, error); } }); - + if (hiddenCount > 0) { trace.log(`Total elements hidden: ${hiddenCount}`); } @@ -241,19 +283,19 @@ function processAllElements(): void { // Process new elements that are added to the DOM function processNewElements(addedNodes: NodeList): void { - addedNodes.forEach(node => { + addedNodes.forEach((node) => { if (node.nodeType !== Node.ELEMENT_NODE) return; - + const element = node as HTMLElement; - + // Check the element itself if (matchesStoredSelector(element)) { hideElementDirectly(element); } - + // Check all descendant elements - const descendants = element.querySelectorAll('*'); - descendants.forEach(descendant => { + const descendants = element.querySelectorAll("*"); + descendants.forEach((descendant) => { if (matchesStoredSelector(descendant as HTMLElement)) { hideElementDirectly(descendant as HTMLElement); } @@ -264,20 +306,20 @@ function processNewElements(addedNodes: NodeList): void { // Set up reactive element observer function setupElementObserver(): void { if (elementObserver) return; - + elementObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { - if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { + if (mutation.type === "childList" && mutation.addedNodes.length > 0) { processNewElements(mutation.addedNodes); } }); }); - + elementObserver.observe(document.body, { childList: true, - subtree: true + subtree: true, }); - + trace.log(`Set up reactive element observer`); } @@ -297,19 +339,19 @@ function setupElementObserver(): void { // Handle highlighting target element function highlightElement(element: HTMLElement): void { // Remove previous highlights - document.querySelectorAll('.element-hider-target').forEach(el => { - el.classList.remove('element-hider-target'); + document.querySelectorAll(".element-hider-target").forEach((el) => { + el.classList.remove("element-hider-target"); }); - + // Highlight current element - element.classList.add('element-hider-target'); + element.classList.add("element-hider-target"); targetElement = element; } // Remove highlight function removeHighlight(): void { if (targetElement) { - targetElement.classList.remove('element-hider-target'); + targetElement.classList.remove("element-hider-target"); targetElement = null; } } @@ -321,59 +363,71 @@ let contextMenuTimeout: number | null = null; let waitingForBuiltInMenu = false; // Listen for right-click events to capture the target for context menu -document.addEventListener('contextmenu', (event: MouseEvent) => { - const target = event.target as HTMLElement; - - // Don't interfere with native context menus on inputs, textareas, etc. - if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) { - currentContextElement = null; - return; - } - - // Don't show menu on our own custom menu - if (target.closest(".element-hider-custom-menu")) { - 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); +document.addEventListener( + "contextmenu", + (event: MouseEvent) => { + const target = event.target as HTMLElement; + + // Don't interfere with native context menus on inputs, textareas, etc. + if ( + target.tagName === "INPUT" || + target.tagName === "TEXTAREA" || + target.isContentEditable + ) { + currentContextElement = null; + return; } - waitingForBuiltInMenu = false; - }, 150); // Wait 150ms for built-in menu - - // Don't prevent default initially - let Luna try to handle the context menu -}, true); + + // Don't show menu on our own custom menu + if (target.closest(".element-hider-custom-menu")) { + 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 prevent default initially - let Luna try to handle the context menu + }, + true, +); // Listen for clicks to close custom menu -document.addEventListener('click', (event: MouseEvent) => { - const target = event.target as HTMLElement; - - // If clicking outside our custom menu, close it - if (customMenu && !target.closest(".element-hider-custom-menu")) { - closeCustomMenu(); - removeHighlight(); - } -}, true); +document.addEventListener( + "click", + (event: MouseEvent) => { + const target = event.target as HTMLElement; + + // If clicking outside our custom menu, close it + if (customMenu && !target.closest(".element-hider-custom-menu")) { + closeCustomMenu(); + removeHighlight(); + } + }, + true, +); // Handle escape key to close custom menu and remove highlights -document.addEventListener('keydown', (event: KeyboardEvent) => { +document.addEventListener("keydown", (event: KeyboardEvent) => { if (event.key === "Escape") { if (customMenu) { closeCustomMenu(); @@ -386,7 +440,7 @@ document.addEventListener('keydown', (event: KeyboardEvent) => { function createCustomMenu(): HTMLElement { const menu = document.createElement("div"); menu.className = "element-hider-custom-menu"; - + // Hide Element option const hideItem = document.createElement("button"); hideItem.className = "element-hider-menu-item"; @@ -398,18 +452,18 @@ function createCustomMenu(): HTMLElement { closeCustomMenu(); } }); - + // Add hover effects for highlighting hideItem.addEventListener("mouseenter", () => { if (currentContextElement) { highlightElement(currentContextElement); } }); - + hideItem.addEventListener("mouseleave", () => { removeHighlight(); }); - + // Unhide All Elements option const unhideAllItem = document.createElement("button"); unhideAllItem.className = "element-hider-menu-item"; @@ -418,28 +472,28 @@ function createCustomMenu(): HTMLElement { unhideAllElements(); closeCustomMenu(); }); - + menu.appendChild(hideItem); menu.appendChild(unhideAllItem); - + return menu; } // Show custom context menu function showCustomMenu(x: number, y: number): void { closeCustomMenu(); - + customMenu = createCustomMenu(); document.body.appendChild(customMenu); - + // Position the menu const rect = customMenu.getBoundingClientRect(); const finalX = Math.min(x, window.innerWidth - rect.width - 10); const finalY = Math.min(y, window.innerHeight - rect.height - 10); - + customMenu.style.left = `${finalX}px`; customMenu.style.top = `${finalY}px`; - + trace.log(`Context menu opened for: ${currentContextElement?.tagName}`); } @@ -449,7 +503,7 @@ function closeCustomMenu(): void { customMenu.remove(); customMenu = null; } - + if (contextMenuTimeout) { clearTimeout(contextMenuTimeout); contextMenuTimeout = null; @@ -462,11 +516,18 @@ const contextMenuObserver = new MutationObserver((mutations) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { const element = node as HTMLElement; - + // Look for Tidal's context menu - if (element.matches('[data-test="contextmenu"]') || element.querySelector('[data-test="contextmenu"]')) { - const contextMenu = element.matches('[data-test="contextmenu"]') ? element : element.querySelector('[data-test="contextmenu"]') as HTMLElement; - + if ( + element.matches('[data-test="contextmenu"]') || + element.querySelector('[data-test="contextmenu"]') + ) { + const contextMenu = element.matches('[data-test="contextmenu"]') + ? element + : (element.querySelector( + '[data-test="contextmenu"]', + ) as HTMLElement); + if (contextMenu && currentContextElement && waitingForBuiltInMenu) { // Built-in menu appeared, cancel custom menu timeout waitingForBuiltInMenu = false; @@ -485,8 +546,8 @@ const contextMenuObserver = new MutationObserver((mutations) => { // Add our options to the existing context menu function addElementHiderOptions(contextMenu: HTMLElement): void { // Create hide element button - const hideButton = document.createElement('button'); - hideButton.className = 'element-hider-menu-item'; + const hideButton = document.createElement("button"); + hideButton.className = "element-hider-menu-item"; hideButton.style.cssText = ` display: flex; align-items: center; @@ -502,46 +563,47 @@ function addElementHiderOptions(contextMenu: HTMLElement): void { font-size: 14px; `; hideButton.innerHTML = `Hide This Element`; - - hideButton.addEventListener('click', () => { + + hideButton.addEventListener("click", () => { if (currentContextElement) { targetElement = currentContextElement; hideTargetElement(); } }); - + // Add hover effects for highlighting - hideButton.addEventListener('mouseenter', () => { - hideButton.style.background = 'var(--wave-color-background-hover, #3a3a3a)'; + hideButton.addEventListener("mouseenter", () => { + hideButton.style.background = "var(--wave-color-background-hover, #3a3a3a)"; if (currentContextElement) { highlightElement(currentContextElement); } }); - - hideButton.addEventListener('mouseleave', () => { - hideButton.style.background = 'transparent'; + + hideButton.addEventListener("mouseleave", () => { + hideButton.style.background = "transparent"; removeHighlight(); }); - + // Create unhide all button - const unhideAllButton = document.createElement('button'); - unhideAllButton.className = 'element-hider-menu-item'; + const unhideAllButton = document.createElement("button"); + unhideAllButton.className = "element-hider-menu-item"; unhideAllButton.style.cssText = hideButton.style.cssText; unhideAllButton.innerHTML = `Unhide All Elements (${hiddenElementsArray.length})`; - - unhideAllButton.addEventListener('click', unhideAllElements); - + + unhideAllButton.addEventListener("click", unhideAllElements); + // Add hover effects for unhide all button - unhideAllButton.addEventListener('mouseenter', () => { - unhideAllButton.style.background = 'var(--wave-color-background-hover, #3a3a3a)'; + unhideAllButton.addEventListener("mouseenter", () => { + unhideAllButton.style.background = + "var(--wave-color-background-hover, #3a3a3a)"; }); - unhideAllButton.addEventListener('mouseleave', () => { - unhideAllButton.style.background = 'transparent'; + unhideAllButton.addEventListener("mouseleave", () => { + unhideAllButton.style.background = "transparent"; }); - + // Add a separator if the menu has other items if (contextMenu.children.length > 0) { - const separator = document.createElement('div'); + const separator = document.createElement("div"); separator.style.cssText = ` height: 1px; background: var(--wave-color-border, #444); @@ -549,7 +611,7 @@ function addElementHiderOptions(contextMenu: HTMLElement): void { `; contextMenu.appendChild(separator); } - + // Add our buttons contextMenu.appendChild(hideButton); contextMenu.appendChild(unhideAllButton); @@ -558,28 +620,28 @@ function addElementHiderOptions(contextMenu: HTMLElement): void { // Start observing for context menus contextMenuObserver.observe(document.body, { childList: true, - subtree: true + subtree: true, }); - // Initialize plugin +// Initialize plugin function initializePlugin() { trace.log("Initializing plugin..."); - + // Process immediately when DOM is ready trace.log("Starting element processing..."); - + // Process existing elements processAllElements(); - + // Set up reactive observer for new elements setupElementObserver(); - + trace.log("Plugin fully initialized"); } // Run initialization when DOM is ready -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializePlugin); +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initializePlugin); } else { initializePlugin(); } @@ -592,18 +654,18 @@ unloads.add(() => { elementObserver = null; } contextMenuObserver.disconnect(); - + // Close any open custom menu closeCustomMenu(); - + // Remove highlights removeHighlight(); - + // Clean up global functions (window as any).showAllElementsFromSettings = undefined; (window as any).debugElementHider = undefined; - + trace.log("Plugin unloaded"); }); -trace.log("Plugin loaded - Right-click any element to hide it!"); \ No newline at end of file +trace.log("Plugin loaded - Right-click any element to hide it!"); diff --git a/plugins/element-hider-luna/src/styles.css b/plugins/element-hider-luna/src/styles.css index 4efb832..3924f25 100644 --- a/plugins/element-hider-luna/src/styles.css +++ b/plugins/element-hider-luna/src/styles.css @@ -2,62 +2,64 @@ /* Custom context menu for elements without built-in menu */ .element-hider-custom-menu { - position: fixed; - background: var(--wave-color-background-elevated, #2a2a2a); - border: 1px solid var(--wave-color-border, #444); - border-radius: 8px; - padding: 8px 0; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); - z-index: 999999; - min-width: 180px; - font-family: inherit; - font-size: 14px; + position: fixed; + background: var(--wave-color-background-elevated, #2a2a2a); + border: 1px solid var(--wave-color-border, #444); + border-radius: 8px; + padding: 8px 0; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + z-index: 999999; + min-width: 180px; + font-family: inherit; + font-size: 14px; } .element-hider-menu-item { - display: flex; - align-items: center; - padding: 8px 16px; - cursor: pointer; - color: var(--wave-color-text, #ffffff); - background: transparent; - border: none; - width: 100%; - text-align: left; - transition: background-color 0.15s ease; - font-family: inherit; - font-size: 14px; + display: flex; + align-items: center; + padding: 8px 16px; + cursor: pointer; + color: var(--wave-color-text, #ffffff); + background: transparent; + border: none; + width: 100%; + text-align: left; + transition: background-color 0.15s ease; + font-family: inherit; + font-size: 14px; } .element-hider-menu-item:hover { - background: var(--wave-color-background-hover, #3a3a3a); + background: var(--wave-color-background-hover, #3a3a3a); } .element-hider-menu-item:active { - background: var(--wave-color-background-active, #4a4a4a); + background: var(--wave-color-background-active, #4a4a4a); } .element-hider-menu-icon { - margin-right: 8px; - width: 16px; - height: 16px; + margin-right: 8px; + width: 16px; + height: 16px; } /* Highlight the target element */ .element-hider-target { - outline: 2px solid #ff6b6b !important; - outline-offset: 2px !important; - box-shadow: 0 0 10px rgba(255, 107, 107, 0.6) !important; + outline: 2px solid #ff6b6b !important; + outline-offset: 2px !important; + box-shadow: 0 0 10px rgba(255, 107, 107, 0.6) !important; } /* Hidden elements */ .element-hider-hidden { - display: none !important; + display: none !important; } /* Animation for hiding */ .element-hider-hiding { - transition: opacity 0.3s ease, transform 0.3s ease; - opacity: 0; - transform: scale(0.95); -} \ No newline at end of file + transition: + opacity 0.3s ease, + transform 0.3s ease; + opacity: 0; + transform: scale(0.95); +} diff --git a/plugins/oled-theme-luna/package.json b/plugins/oled-theme-luna/package.json index d15b12b..084d801 100644 --- a/plugins/oled-theme-luna/package.json +++ b/plugins/oled-theme-luna/package.json @@ -8,4 +8,4 @@ }, "main": "./src/index.ts", "type": "module" -} \ No newline at end of file +} diff --git a/plugins/oled-theme-luna/src/Settings.tsx b/plugins/oled-theme-luna/src/Settings.tsx index 845f999..49e7f94 100644 --- a/plugins/oled-theme-luna/src/Settings.tsx +++ b/plugins/oled-theme-luna/src/Settings.tsx @@ -9,10 +9,13 @@ export const settings = await ReactiveStore.getPluginStorage("OLEDTheme", { }); export const Settings = () => { - const [qualityColorMatchedSeekBar, setQualityColorMatchedSeekBar] = React.useState(settings.qualityColorMatchedSeekBar); - const [oledFriendlyButtons, setOledFriendlyButtons] = React.useState(settings.oledFriendlyButtons); + const [qualityColorMatchedSeekBar, setQualityColorMatchedSeekBar] = + React.useState(settings.qualityColorMatchedSeekBar); + const [oledFriendlyButtons, setOledFriendlyButtons] = React.useState( + settings.oledFriendlyButtons, + ); const [lightMode, setLightMode] = React.useState(settings.lightMode); - + return ( { desc="Color the Seek/Progress Bar based on audio quality" checked={qualityColorMatchedSeekBar} onChange={(_, checked) => { - console.log("Quality Color Matched Seek Bar:", checked ? "enabled" : "disabled"); - setQualityColorMatchedSeekBar((settings.qualityColorMatchedSeekBar = checked)); + console.log( + "Quality Color Matched Seek Bar:", + checked ? "enabled" : "disabled", + ); + setQualityColorMatchedSeekBar( + (settings.qualityColorMatchedSeekBar = checked), + ); // Update styles immediately when setting changes if ((window as any).updateOLEDThemeStyles) { (window as any).updateOLEDThemeStyles(); @@ -33,7 +41,10 @@ export const Settings = () => { desc="Remove button styling from OLED theme to keep buttons with original Tidal appearance" checked={oledFriendlyButtons} onChange={(_, checked) => { - console.log("OLED Friendly Buttons:", checked ? "enabled" : "disabled"); + console.log( + "OLED Friendly Buttons:", + checked ? "enabled" : "disabled", + ); setOledFriendlyButtons((settings.oledFriendlyButtons = checked)); // Update styles immediately when setting changes if ((window as any).updateOLEDThemeStyles) { @@ -56,4 +67,4 @@ export const Settings = () => { /> ); -}; \ No newline at end of file +}; diff --git a/plugins/oled-theme-luna/src/dark-theme.css b/plugins/oled-theme-luna/src/dark-theme.css index 909948e..dcb171c 100644 --- a/plugins/oled-theme-luna/src/dark-theme.css +++ b/plugins/oled-theme-luna/src/dark-theme.css @@ -7,295 +7,298 @@ */ ::-webkit-scrollbar { - display: none; + display: none; } :root { - --wave-color-solid-accent-fill: white; - --wave-color-solid-rainbow-yellow-fill: white; - --wave-color-solid-contrast-fill: white; - --wave-color-solid-base-brighter: black; - --wave-text-body-medium: white !important; - --track-vibrant-color: white !important; - --wave-color-opacity-contrast-fill-ultra-thin: #fffafa1a !important; - --wave-color-solid-rainbow-yellow-darkest: #fffafa1a !important; - --wave-color-solid-accent-dark: rgb(128, 128, 128); + --wave-color-solid-accent-fill: white; + --wave-color-solid-rainbow-yellow-fill: white; + --wave-color-solid-contrast-fill: white; + --wave-color-solid-base-brighter: black; + --wave-text-body-medium: white !important; + --track-vibrant-color: white !important; + --wave-color-opacity-contrast-fill-ultra-thin: #fffafa1a !important; + --wave-color-solid-rainbow-yellow-darkest: #fffafa1a !important; + --wave-color-solid-accent-dark: rgb(128, 128, 128); } /* Credits to https://github.com/surfbryce for the fonts */ @font-face { - font-family: "AbyssFont"; - font-weight: 400; - src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 400; + src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") + format("woff2"); } @font-face { - font-family: "AbyssFont"; - font-weight: 500; - src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 500; + src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") + format("woff2"); } @font-face { - font-family: "AbyssFont"; - font-weight: 600; - src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 600; + src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") + format("woff2"); } @font-face { - font-family: "AbyssFont"; - font-weight: 700; - src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 700; + src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") + format("woff2"); } [class^="followingButton"], [title="Unfollow"], [title="Follow"], -[title="Unfollow"]>span, -[title="Follow"]>span { - background-color: var(--wave-color-solid-rainbow-yellow-fill) !important; - color: var(--wave-color-solid-base-brighter); +[title="Unfollow"] > span, +[title="Follow"] > span { + background-color: var(--wave-color-solid-rainbow-yellow-fill) !important; + color: var(--wave-color-solid-base-brighter); } [class^="_wave-badge-color-max"] { - color: black !important; - background-color: var(--wave-color-solid-accent-fill); - border-radius: 3px; + color: black !important; + background-color: var(--wave-color-solid-accent-fill); + border-radius: 3px; } [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"] { - background-color: var(--wave-color-solid-accent-fill); - border-radius: 4px; - color: black; + background-color: var(--wave-color-solid-accent-fill); + border-radius: 4px; + color: black; } [class^="_progressBarWrapper"] { - color: var(--wave-color-solid-accent-fill) !important; + color: var(--wave-color-solid-accent-fill) !important; } -[class^="_sidebarItem"]>span { - color: var(--wave-color-solid-accent-dark); +[class^="_sidebarItem"] > span { + color: var(--wave-color-solid-accent-dark); } [data-test="main-layout-header"] { - border-left: 0 !important; + border-left: 0 !important; } [class^="_sidebarItem"]:hover span { - color: var(--wave-color-solid-contrast-fill); + color: var(--wave-color-solid-contrast-fill); } -[class^="_sidebarItem"] [class^="active"]>span { - color: var(--wave-color-solid-accent-dark) !important; +[class^="_sidebarItem"] [class^="active"] > span { + color: var(--wave-color-solid-accent-dark) !important; } [class^="_active"] { - color: var(--wave-color-solid-accent-fill) !important; + color: var(--wave-color-solid-accent-fill) !important; } [class^="ReactVirtualized__Grid"] { - border-radius: 10px; - margin: 5px; + border-radius: 10px; + margin: 5px; } -[data-test="media-table"]>div>div>div { - border: 1px solid rgb(25, 25, 25) !important; +[data-test="media-table"] > div > div > div { + border: 1px solid rgb(25, 25, 25) !important; } [class^="ReactVirtualized__Grid__innerScrollContainer"] { - border: none; - margin: 5px; + border: none; + margin: 5px; } -[class^="button"]>span { - color: black; +[class^="button"] > span { + color: black; } [class^="_explicitBadge"] { - color: var(--wave-color-solid-accent-fill); + color: var(--wave-color-solid-accent-fill); } [class^="viewAllButton"] { - border-radius: 4px; - display: grid; - place-items: center; + border-radius: 4px; + display: grid; + place-items: center; } [data-test="current-media-imagery"] { - border: 0 !important; - margin: none; + border: 0 !important; + margin: none; } [class^="_imageBorder"] { - display: none; + display: none; } -[class^="_headerButtons"]>button, -[class^="_headerButtons"]>button>span, +[class^="_headerButtons"] > button, +[class^="_headerButtons"] > button > span, [data-test="toggle-picture-in-picture"] { - background-color: var(--wave-color-solid-accent-fill) !important; - color: black; + background-color: var(--wave-color-solid-accent-fill) !important; + color: black; } -[class^="_container"]>[class^="_navigationArrows"] { - color: black; - background-color: var(--wave-color-solid-accent-fill) !important; - border-radius: 4px; +[class^="_container"] > [class^="_navigationArrows"] { + color: black; + background-color: var(--wave-color-solid-accent-fill) !important; + border-radius: 4px; } -[class^="_buttons"]>button>span { - color: black !important; +[class^="_buttons"] > button > span { + color: black !important; } -[class^="_container"]>button { - border: 0px none; +[class^="_container"] > button { + border: 0px none; } - [data-test="feed-sidebar"] { - margin-top: 10px; + margin-top: 10px; } [data-test="footer-player"] { - width: calc(100% - 20px); - bottom: 10px; - left: 10px; - border: 1px solid rgb(25, 25, 25); - border-radius: 4px !important; - position: absolute !important; + width: calc(100% - 20px); + bottom: 10px; + left: 10px; + border: 1px solid rgb(25, 25, 25); + border-radius: 4px !important; + position: absolute !important; } -[class^="_tooltipContainer"]>button { - background-color: var(--wave-color-solid-accent-fill); - color: black; +[class^="_tooltipContainer"] > button { + background-color: var(--wave-color-solid-accent-fill); + color: black; } -[class^="_tooltipContainer"]>button:hover { - background-color: lightgray !important; +[class^="_tooltipContainer"] > button:hover { + background-color: lightgray !important; } -[class^="_tableRow"]:hover>*, -[data-test-is-playing="true"]>* { - color: var(--wave-color-solid-accent-fill) !important; +[class^="_tableRow"]:hover > *, +[data-test-is-playing="true"] > * { + color: var(--wave-color-solid-accent-fill) !important; } -[class^="_tableRow"]>*, -[data-test-is-playing="false"]>* { - color: lightgray !important; +[class^="_tableRow"] > *, +[data-test-is-playing="false"] > * { + color: lightgray !important; } [class*="coverColumn"] { - padding-left: 5px !important; + padding-left: 5px !important; } [class^="actionList"] { - background-color: transparent; - margin: 0px; - border-radius: 5px; + background-color: transparent; + margin: 0px; + border-radius: 5px; } button[data-test="request-fullscreen"], button[data-test="close-now-playing"], button[data-test="play-all"], button[data-test="shuffle-all"] { - color: black; - background-color: var(--wave-color-solid-accent-fill); - border-radius: 12px; + color: black; + background-color: var(--wave-color-solid-accent-fill); + border-radius: 12px; } button[data-test="request-fullscreen"]:hover, button[data-test="close-now-playing"]:hover { - color: black; - background-color: lightgray !important; + color: black; + background-color: lightgray !important; } -.neptune-switch-checkbox:checked+.neptune-switch { - background-color: rgba(255, 255, 255, 0.1); +.neptune-switch-checkbox:checked + .neptune-switch { + background-color: rgba(255, 255, 255, 0.1); } -[data-test="navigation-arrows"]>button { - background-color: var(--wave-color-solid-accent-fill) !important; - color: black !important; - border-radius: 5px; +[data-test="navigation-arrows"] > button { + background-color: var(--wave-color-solid-accent-fill) !important; + color: black !important; + border-radius: 5px; } -[data-test="navigation-arrows"]>button:disabled { - background-color: lightgray !important; - opacity: 1; +[data-test="navigation-arrows"] > button:disabled { + background-color: lightgray !important; + opacity: 1; } [data-test="main-layout-header"], [data-test="feed-sidebar"], [data-test="stream-metadata"], [data-test="footer-player"] { - background-color: rgba(0, 0, 0, 0.8) !important; - backdrop-filter: blur(10px); - border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important; + background-color: rgba(0, 0, 0, 0.8) !important; + backdrop-filter: blur(10px); + border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important; } -[data-wave-color=textUrl] { - color: var(--wave-color-solid-accent-fill); +[data-wave-color="textUrl"] { + color: var(--wave-color-solid-accent-fill); } [class^="_smallHeader"] { - margin-top: 7.5px; + margin-top: 7.5px; } -[data-test="play-all"]>div>*, -[data-test="shuffle-all"]>div>*, +[data-test="play-all"] > div > *, +[data-test="shuffle-all"] > div > *, [data-test="play-all"], [data-test="shuffle-all"] { - color: var(--wave-color-solid-accent-fill) !important; - background-color: transparent !important; + color: var(--wave-color-solid-accent-fill) !important; + background-color: transparent !important; } [class^="__NEPTUNE_PAGE"], [data-test="main"] { - margin-top: 35px; + margin-top: 35px; } [data-test="button-desktop-release-notes"], [data-test="button-release-notes"] { - background-color: white; + background-color: white; } [data-test="button-desktop-release-notes"]:hover, [data-test="button-release-notes"]:hover { - background-color: lightgray !important; - transition: none !important; + background-color: lightgray !important; + transition: none !important; } #playQueueSidebar { - top: 50px !important; - border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin); - margin: 2px; - margin-right: -14px !important; - background-color: rgba(0, 0, 0, 0.8) !important; - backdrop-filter: blur(10px); + top: 50px !important; + border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin); + margin: 2px; + margin-right: -14px !important; + background-color: rgba(0, 0, 0, 0.8) !important; + backdrop-filter: blur(10px); } [class^="_bottomGradient"] { - visibility: hidden; + visibility: hidden; } [data-test="settings-page"] { - padding-bottom: 60px !important; + padding-bottom: 60px !important; } [data-test="query-suggestions"], [data-test="recent-searches-container"] { - background-color: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(10px); + background-color: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(10px); } [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 { - background-image: var(--img); - filter: blur(10px) brightness(0.4); -} \ No newline at end of file + background-image: var(--img); + filter: blur(10px) brightness(0.4); +} diff --git a/plugins/oled-theme-luna/src/index.ts b/plugins/oled-theme-luna/src/index.ts index c579263..9c47707 100644 --- a/plugins/oled-theme-luna/src/index.ts +++ b/plugins/oled-theme-luna/src/index.ts @@ -1,5 +1,11 @@ import { LunaUnload, Tracer } from "@luna/core"; -import { StyleTag, observePromise, PlayState, Quality, type MediaItem } from "@luna/lib"; +import { + StyleTag, + observePromise, + PlayState, + Quality, + type MediaItem, +} from "@luna/lib"; import { settings, Settings } from "./Settings"; // Import CSS files directly using Luna's file:// syntax - Took me a while to figure out <3 @@ -19,105 +25,119 @@ const themeStyleTag = new StyleTag("OLED-Theme", unloads); // Quality color mapping const QUALITY_COLORS = { - MAX: "#FED330", // Max/HiFi - HIGH: "#31FFEE", // High - LOW: "#FFFFFE" // Low + MAX: "#FED330", // Max/HiFi + HIGH: "#31FFEE", // High + LOW: "#FFFFFE", // Low }; // Function to get quality color based on audio quality const getQualityColor = (audioQuality: string): string => { - const quality = audioQuality?.toUpperCase(); - if (quality?.includes("HI_RES_LOSSLESS")) { - return QUALITY_COLORS.MAX; - } else if (quality?.includes("LOSSLESS")) { - return QUALITY_COLORS.HIGH; - } else { - return QUALITY_COLORS.LOW; - } + const quality = audioQuality?.toUpperCase(); + if (quality?.includes("HI_RES_LOSSLESS")) { + return QUALITY_COLORS.MAX; + } else if (quality?.includes("LOSSLESS")) { + return QUALITY_COLORS.HIGH; + } else { + return QUALITY_COLORS.LOW; + } }; // Function to Reset Seek Bar Color (if setting gets disabled while playing) const resetSeekBarColor = async (): Promise => { - try { - const progressBarWrapper = await observePromise(unloads, `[class^="_progressBarWrapper"]`); - if (!progressBarWrapper) return; - progressBarWrapper.style.removeProperty('color'); - progressBarWrapper.querySelectorAll('[class*="progress"], [class*="bar"]').forEach(el => { - if (el instanceof HTMLElement) el.style.removeProperty('color'); - }); - } catch (error) { - trace.msg.err(`Failed to reset seek bar color: ${error}`); - } + try { + const progressBarWrapper = await observePromise( + unloads, + `[class^="_progressBarWrapper"]`, + ); + if (!progressBarWrapper) return; + progressBarWrapper.style.removeProperty("color"); + progressBarWrapper + .querySelectorAll('[class*="progress"], [class*="bar"]') + .forEach((el) => { + if (el instanceof HTMLElement) el.style.removeProperty("color"); + }); + } catch (error) { + trace.msg.err(`Failed to reset seek bar color: ${error}`); + } }; // Function to apply quality-based seek bar coloring (if enabled) const applyQualityColors = async (): Promise => { - if (!settings.qualityColorMatchedSeekBar) return; - try { - const progressBarWrapper = await observePromise(unloads, `[class^="_progressBarWrapper"]`); - if (!progressBarWrapper) return; - const audioQuality = PlayState.playbackContext?.actualAudioQuality; - if (!audioQuality) return; - const qualityColor = getQualityColor(audioQuality); - progressBarWrapper.style.setProperty('color', qualityColor, 'important'); - progressBarWrapper.querySelectorAll('[class*="progress"], [class*="bar"]').forEach(el => { - if (el instanceof HTMLElement) el.style.setProperty('color', qualityColor, 'important'); - }); - //trace.msg.log(`Applied quality color ${qualityColor}`); - } catch (error) { - trace.msg.err(`Failed to apply quality colors: ${error}`); - } + if (!settings.qualityColorMatchedSeekBar) return; + try { + const progressBarWrapper = await observePromise( + unloads, + `[class^="_progressBarWrapper"]`, + ); + if (!progressBarWrapper) return; + const audioQuality = PlayState.playbackContext?.actualAudioQuality; + if (!audioQuality) return; + const qualityColor = getQualityColor(audioQuality); + progressBarWrapper.style.setProperty("color", qualityColor, "important"); + progressBarWrapper + .querySelectorAll('[class*="progress"], [class*="bar"]') + .forEach((el) => { + if (el instanceof HTMLElement) + el.style.setProperty("color", qualityColor, "important"); + }); + //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 const setupQualityMonitoring = (): void => { - let lastTrackId: string | null = null; - const interval = setInterval(() => { - if (!settings.qualityColorMatchedSeekBar) return; - const currentTrackId = PlayState.playbackContext?.actualProductId; - if (currentTrackId && currentTrackId !== lastTrackId) { - //trace.msg.log(`[OLED Theme] Track ID changed: ${lastTrackId} -> ${currentTrackId}`); - lastTrackId = currentTrackId; - applyQualityColors(); - } - }, 250); - unloads.add(() => clearInterval(interval)); + let lastTrackId: string | null = null; + const interval = setInterval(() => { + if (!settings.qualityColorMatchedSeekBar) return; + const currentTrackId = PlayState.playbackContext?.actualProductId; + if (currentTrackId && currentTrackId !== lastTrackId) { + //trace.msg.log(`[OLED Theme] Track ID changed: ${lastTrackId} -> ${currentTrackId}`); + lastTrackId = currentTrackId; + applyQualityColors(); + } + }, 250); + unloads.add(() => clearInterval(interval)); - // Initial color application (if a track is already loaded) - const currentTrackId = PlayState.playbackContext?.actualProductId; - if (settings.qualityColorMatchedSeekBar && currentTrackId) { - lastTrackId = currentTrackId; - applyQualityColors(); - } + // Initial color application (if a track is already loaded) + const currentTrackId = PlayState.playbackContext?.actualProductId; + if (settings.qualityColorMatchedSeekBar && currentTrackId) { + lastTrackId = currentTrackId; + applyQualityColors(); + } }; // Function to apply theme styles based on current settings -const applyThemeStyles = function(): void { - // Choose the appropriate CSS file based on settings - 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; +const applyThemeStyles = function (): void { + // Choose the appropriate CSS file based on settings + 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; }; // Make this function available globally so Settings can call it @@ -125,4 +145,3 @@ const applyThemeStyles = function(): void { // Apply the OLED theme initially applyThemeStyles(); - diff --git a/plugins/oled-theme-luna/src/light-theme.css b/plugins/oled-theme-luna/src/light-theme.css index 2fec709..630593f 100644 --- a/plugins/oled-theme-luna/src/light-theme.css +++ b/plugins/oled-theme-luna/src/light-theme.css @@ -7,90 +7,94 @@ */ ::-webkit-scrollbar { - display: none; + display: none; } :root { - --wave-color-solid-accent-fill: #666666; - --wave-color-solid-rainbow-yellow-fill: #666666; - --wave-color-solid-contrast-fill: #666666; - --wave-color-solid-base-brighter: #666666; - --wave-text-body-medium: #333333 !important; - --track-vibrant-color: #666666 !important; - --wave-color-opacity-contrast-fill-ultra-thin: #c0c0c0 !important; - --wave-color-solid-rainbow-yellow-darkest: #c0c0c0 !important; - --wave-color-solid-accent-dark: #555555; + --wave-color-solid-accent-fill: #666666; + --wave-color-solid-rainbow-yellow-fill: #666666; + --wave-color-solid-contrast-fill: #666666; + --wave-color-solid-base-brighter: #666666; + --wave-text-body-medium: #333333 !important; + --track-vibrant-color: #666666 !important; + --wave-color-opacity-contrast-fill-ultra-thin: #c0c0c0 !important; + --wave-color-solid-rainbow-yellow-darkest: #c0c0c0 !important; + --wave-color-solid-accent-dark: #555555; } /* Credits to https://github.com/surfbryce for the fonts */ @font-face { - font-family: "AbyssFont"; - font-weight: 400; - src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 400; + src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") + format("woff2"); } @font-face { - font-family: "AbyssFont"; - font-weight: 500; - src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 500; + src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") + format("woff2"); } @font-face { - font-family: "AbyssFont"; - font-weight: 600; - src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 600; + src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") + format("woff2"); } @font-face { - font-family: "AbyssFont"; - font-weight: 700; - src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 700; + src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") + format("woff2"); } [class^="followingButton"], [title="Unfollow"], [title="Follow"], -[title="Unfollow"]>span, -[title="Follow"]>span { - background-color: var(--wave-color-solid-rainbow-yellow-fill) !important; - color: var(--wave-color-solid-base-brighter); +[title="Unfollow"] > span, +[title="Follow"] > span { + background-color: var(--wave-color-solid-rainbow-yellow-fill) !important; + color: var(--wave-color-solid-base-brighter); } [class^="_wave-badge-color-max"] { - color: #333333 !important; - background-color: var(--wave-color-solid-accent-fill); - border-radius: 3px; + color: #333333 !important; + background-color: var(--wave-color-solid-accent-fill); + border-radius: 3px; } [data-test="main-layout-sidebar-wrapper"] { - border-right: rgb(230, 230, 230) 1px solid; - background-color: rgba(250, 250, 250, 0.95) !important; + border-right: rgb(230, 230, 230) 1px solid; + background-color: rgba(250, 250, 250, 0.95) !important; } [class^="_wave-badge"] { - background-color: var(--wave-color-solid-accent-fill); - border-radius: 4px; - color: #333333; + background-color: var(--wave-color-solid-accent-fill); + border-radius: 4px; + color: #333333; } [class^="_progressBarWrapper"] { - color: var(--wave-color-solid-accent-fill) !important; + color: var(--wave-color-solid-accent-fill) !important; } -[class^="_sidebarItem"]>span { - color: #666666; +[class^="_sidebarItem"] > span { + color: #666666; } [data-test="main-layout-header"] { - border-left: 0 !important; + border-left: 0 !important; } [class^="_sidebarItem"]:hover span { - color: #333333; + color: #333333; } -[class^="_sidebarItem"] [class^="active"]>span { - color: #333333 !important; +[class^="_sidebarItem"] [class^="active"] > span { + color: #333333 !important; } /* Sidebar icons and text - ensure grey colors */ @@ -98,110 +102,107 @@ [data-test="main-layout-sidebar-wrapper"] path, [class^="_sidebarItem"] svg, [class^="_sidebarItem"] path { - fill: #666666 !important; - color: #666666 !important; + fill: #666666 !important; + color: #666666 !important; } [data-test="main-layout-sidebar-wrapper"] span, [class^="_sidebarItem"] span { - color: #666666 !important; + color: #666666 !important; } [class^="_active"] { - color: var(--wave-color-solid-accent-fill) !important; + color: var(--wave-color-solid-accent-fill) !important; } [class^="ReactVirtualized__Grid"] { - border-radius: 10px; - margin: 5px; + border-radius: 10px; + margin: 5px; } -[data-test="media-table"]>div>div>div { - border: 1px solid rgb(230, 230, 230) !important; +[data-test="media-table"] > div > div > div { + border: 1px solid rgb(230, 230, 230) !important; } [class^="ReactVirtualized__Grid__innerScrollContainer"] { - border: none; - margin: 5px; + border: none; + margin: 5px; } -[class^="button"]>span { - color: #333333; +[class^="button"] > span { + color: #333333; } - - [class^="_explicitBadge"] { - color: var(--wave-color-solid-accent-fill); + color: var(--wave-color-solid-accent-fill); } [class^="viewAllButton"] { - border-radius: 4px; - display: grid; - place-items: center; + border-radius: 4px; + display: grid; + place-items: center; } [data-test="current-media-imagery"] { - border: 0 !important; - margin: none; + border: 0 !important; + margin: none; } [class^="_imageBorder"] { - display: none; + display: none; } -[class^="_headerButtons"]>button, -[class^="_headerButtons"]>button>span, +[class^="_headerButtons"] > button, +[class^="_headerButtons"] > button > span, [data-test="toggle-picture-in-picture"] { - background-color: var(--wave-color-solid-accent-fill) !important; - color: #333333; + background-color: var(--wave-color-solid-accent-fill) !important; + color: #333333; } -[class^="_container"]>[class^="_navigationArrows"] { - color: #333333; - background-color: var(--wave-color-solid-accent-fill) !important; - border-radius: 4px; +[class^="_container"] > [class^="_navigationArrows"] { + color: #333333; + background-color: var(--wave-color-solid-accent-fill) !important; + border-radius: 4px; } -[class^="_buttons"]>button>span { - color: #333333 !important; +[class^="_buttons"] > button > span { + color: #333333 !important; } -[class^="_container"]>button { - border: 0px none; +[class^="_container"] > button { + border: 0px none; } - [data-test="feed-sidebar"] { - margin-top: 10px; + margin-top: 10px; } [data-test="footer-player"] { - width: calc(100% - 20px); - bottom: 10px; - left: 10px; - border: 1px solid rgba(200, 200, 200, 0.7); - border-radius: 4px !important; - position: absolute !important; + width: calc(100% - 20px); + bottom: 10px; + left: 10px; + border: 1px solid rgba(200, 200, 200, 0.7); + border-radius: 4px !important; + position: absolute !important; } -[class^="_tooltipContainer"]>button { - background-color: var(--wave-color-solid-accent-fill); - color: #333333; +[class^="_tooltipContainer"] > button { + background-color: var(--wave-color-solid-accent-fill); + color: #333333; } -[class^="_tooltipContainer"]>button:hover { - background-color: #555555 !important; +[class^="_tooltipContainer"] > button:hover { + background-color: #555555 !important; } -[class^="_tableRow"]:hover>*, -[data-test-is-playing="true"]>* { - color: #333333 !important; +[class^="_tableRow"]:hover > *, +[data-test-is-playing="true"] > * { + color: #333333 !important; } -[class^="_tableRow"]>*, -[data-test-is-playing="false"]>* { - color: #333333 !important; +[class^="_tableRow"] > *, +[data-test-is-playing="false"] > * { + color: #333333 !important; } /* Track list text - ensure all text is dark */ @@ -211,200 +212,200 @@ [class^="_albumTitle"], [class^="_tableCell"] *, [class^="_tableCellContent"] * { - color: #333333 !important; + color: #333333 !important; } [class*="coverColumn"] { - padding-left: 5px !important; + padding-left: 5px !important; } [class^="actionList"] { - background-color: transparent; - margin: 0px; - border-radius: 5px; + background-color: transparent; + margin: 0px; + border-radius: 5px; } button[data-test="request-fullscreen"], button[data-test="close-now-playing"], button[data-test="play-all"], button[data-test="shuffle-all"] { - color: #333333; - background-color: var(--wave-color-solid-accent-fill); - border-radius: 12px; + color: #333333; + background-color: var(--wave-color-solid-accent-fill); + border-radius: 12px; } button[data-test="request-fullscreen"]:hover, button[data-test="close-now-playing"]:hover { - color: #333333; - background-color: #aaaaaa !important; + color: #333333; + background-color: #aaaaaa !important; } -.neptune-switch-checkbox:checked+.neptune-switch { - background-color: rgba(0, 0, 0, 0.1); +.neptune-switch-checkbox:checked + .neptune-switch { + background-color: rgba(0, 0, 0, 0.1); } -[data-test="navigation-arrows"]>button { - background-color: var(--wave-color-solid-accent-fill) !important; - color: #333333 !important; - border-radius: 5px; +[data-test="navigation-arrows"] > button { + background-color: var(--wave-color-solid-accent-fill) !important; + color: #333333 !important; + border-radius: 5px; } -[data-test="navigation-arrows"]>button:disabled { - background-color: #cccccc !important; - opacity: 1; +[data-test="navigation-arrows"] > button:disabled { + background-color: #cccccc !important; + opacity: 1; } [data-test="main-layout-header"] { - background-color: rgba(235, 235, 235, 0.95) !important; - backdrop-filter: blur(10px); - border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important; + background-color: rgba(235, 235, 235, 0.95) !important; + backdrop-filter: blur(10px); + border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important; } [data-test="feed-sidebar"] { - background-color: rgba(225, 225, 225, 0.9) !important; - backdrop-filter: blur(10px); - border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important; + background-color: rgba(225, 225, 225, 0.9) !important; + backdrop-filter: blur(10px); + border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important; } [data-test="stream-metadata"] { - background-color: rgba(230, 230, 230, 0.92) !important; - backdrop-filter: blur(10px); - border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important; + background-color: rgba(230, 230, 230, 0.92) !important; + backdrop-filter: blur(10px); + border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important; } [data-test="footer-player"] { - background-color: rgba(255, 255, 255, 0.6) !important; - backdrop-filter: blur(15px); - border: 1px solid rgba(200, 200, 200, 0.7) !important; + background-color: rgba(255, 255, 255, 0.6) !important; + backdrop-filter: blur(15px); + border: 1px solid rgba(200, 200, 200, 0.7) !important; } -[data-wave-color=textUrl] { - color: var(--wave-color-solid-accent-fill); +[data-wave-color="textUrl"] { + color: var(--wave-color-solid-accent-fill); } [class^="_smallHeader"] { - margin-top: 7.5px; + margin-top: 7.5px; } /* Button styling using proper light theme approach */ :root { - --button-light: #d9d9d9 !important; - --button-medium: #cbcbcb !important; + --button-light: #d9d9d9 !important; + --button-medium: #cbcbcb !important; } /*buttons*/ ._activeTab_f47dafa { - background: #0000001c; + background: #0000001c; } /*canvas nav buttons*/ .viewAllButton--Nb87U, .css-7l8ggf { - background: #e0e0e0; + background: #e0e0e0; } .viewAllButton--Nb87U:hover, .css-7l8ggf:hover { - background: #cbcbcb; + background: #cbcbcb; } /*tracks page*/ .variantPrimary--pjymy, ._button_3357ce6 { - background-color: var(--button-light); + background-color: var(--button-light); } ._button_f1c7fcb { - background: var(--wave-color-solid-base-brighter); + background: var(--wave-color-solid-base-brighter); } ._button_84b8ffe { - background-color: var(--wave-color-solid-base-brighter); + background-color: var(--wave-color-solid-base-brighter); } ._button_84b8ffe:hover { - background-color: var(--wave-color-solid-base-brightest); + background-color: var(--wave-color-solid-base-brightest); } .button--_0I_t { - background-color: var(--button-light); + background-color: var(--button-light); } .button--_0I_t:hover { - background-color: var(--wave-color-opacity-contrast-fill-regular); + background-color: var(--wave-color-opacity-contrast-fill-regular); } ._button_94c5125 { - background: var(--wave-color-solid-base-brighter); + background: var(--wave-color-solid-base-brighter); } .primary--NLSX4 { - background-color: #d5d5d5; + background-color: #d5d5d5; } .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 { - background-color: #e7e7e8; + background-color: #e7e7e8; } .primary--NLSX4:disabled:hover { - background-color: #e7e7e8; + background-color: #e7e7e8; } [class^="__NEPTUNE_PAGE"], [data-test="main"] { - margin-top: 35px; + margin-top: 35px; } [data-test="button-desktop-release-notes"], [data-test="button-release-notes"] { - background-color: #333333; + background-color: #333333; } [data-test="button-desktop-release-notes"]:hover, [data-test="button-release-notes"]:hover { - background-color: #555555 !important; - transition: none !important; + background-color: #555555 !important; + transition: none !important; } #playQueueSidebar { - top: 50px !important; - border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin); - margin: 2px; - margin-right: -14px !important; - background-color: rgba(220, 220, 220, 0.9) !important; - backdrop-filter: blur(10px); + top: 50px !important; + border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin); + margin: 2px; + margin-right: -14px !important; + background-color: rgba(220, 220, 220, 0.9) !important; + backdrop-filter: blur(10px); } [class^="_bottomGradient"] { - visibility: hidden; + visibility: hidden; } [data-test="settings-page"] { - padding-bottom: 60px !important; + padding-bottom: 60px !important; } [data-test="query-suggestions"], [data-test="recent-searches-container"] { - background-color: rgba(227, 227, 227, 0.85); - backdrop-filter: blur(10px); + background-color: rgba(227, 227, 227, 0.85); + backdrop-filter: blur(10px); } [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 { - background-image: var(--img); - filter: blur(10px) brightness(1.2); + background-image: var(--img); + filter: blur(10px) brightness(1.2); } /* Player bar text colors - fix white text issues */ [data-test="footer-player"] * { - color: #333333 !important; + color: #333333 !important; } [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*="time"], [data-test="footer-player"] [class*="timestamp"] { - color: #333333 !important; + color: #333333 !important; } /* Main page background */ body, [data-test="main"], [class^="__NEPTUNE_PAGE"] { - background-color: #f5f5f5 !important; -} \ No newline at end of file + background-color: #f5f5f5 !important; +} diff --git a/plugins/oled-theme-luna/src/oled-friendly.css b/plugins/oled-theme-luna/src/oled-friendly.css index e513a80..4437e77 100644 --- a/plugins/oled-theme-luna/src/oled-friendly.css +++ b/plugins/oled-theme-luna/src/oled-friendly.css @@ -7,209 +7,213 @@ */ ::-webkit-scrollbar { - display: none; + display: none; } :root { - --wave-color-solid-accent-fill: white; - --wave-color-solid-rainbow-yellow-fill: white; - --wave-color-solid-contrast-fill: white; - --wave-color-solid-base-brighter: black; - --wave-text-body-medium: white !important; - --track-vibrant-color: white !important; - --wave-color-opacity-contrast-fill-ultra-thin: #fffafa1a !important; - --wave-color-solid-rainbow-yellow-darkest: #fffafa1a !important; - --wave-color-solid-accent-dark: rgb(128, 128, 128); + --wave-color-solid-accent-fill: white; + --wave-color-solid-rainbow-yellow-fill: white; + --wave-color-solid-contrast-fill: white; + --wave-color-solid-base-brighter: black; + --wave-text-body-medium: white !important; + --track-vibrant-color: white !important; + --wave-color-opacity-contrast-fill-ultra-thin: #fffafa1a !important; + --wave-color-solid-rainbow-yellow-darkest: #fffafa1a !important; + --wave-color-solid-accent-dark: rgb(128, 128, 128); } /* Credits to https://github.com/surfbryce for the fonts */ @font-face { - font-family: "AbyssFont"; - font-weight: 400; - src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 400; + src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") + format("woff2"); } @font-face { - font-family: "AbyssFont"; - font-weight: 500; - src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 500; + src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") + format("woff2"); } @font-face { - font-family: "AbyssFont"; - font-weight: 600; - src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 600; + src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") + format("woff2"); } @font-face { - font-family: "AbyssFont"; - font-weight: 700; - src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 700; + src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") + format("woff2"); } [class^="followingButton"], [title="Unfollow"], [title="Follow"], -[title="Unfollow"]>span, -[title="Follow"]>span { - background-color: var(--wave-color-solid-rainbow-yellow-fill) !important; - color: var(--wave-color-solid-base-brighter); +[title="Unfollow"] > span, +[title="Follow"] > span { + background-color: var(--wave-color-solid-rainbow-yellow-fill) !important; + color: var(--wave-color-solid-base-brighter); } [class^="_wave-badge-color-max"] { - color: black !important; - background-color: var(--wave-color-solid-accent-fill); - border-radius: 3px; + color: black !important; + background-color: var(--wave-color-solid-accent-fill); + border-radius: 3px; } [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"] { - background-color: var(--wave-color-solid-accent-fill); - border-radius: 4px; - color: black; + background-color: var(--wave-color-solid-accent-fill); + border-radius: 4px; + color: black; } [class^="_progressBarWrapper"] { - color: var(--wave-color-solid-accent-fill) !important; + color: var(--wave-color-solid-accent-fill) !important; } -[class^="_sidebarItem"]>span { - color: var(--wave-color-solid-accent-dark); +[class^="_sidebarItem"] > span { + color: var(--wave-color-solid-accent-dark); } [data-test="main-layout-header"] { - border-left: 0 !important; + border-left: 0 !important; } [class^="_sidebarItem"]:hover span { - color: var(--wave-color-solid-contrast-fill); + color: var(--wave-color-solid-contrast-fill); } -[class^="_sidebarItem"] [class^="active"]>span { - color: var(--wave-color-solid-accent-dark) !important; +[class^="_sidebarItem"] [class^="active"] > span { + color: var(--wave-color-solid-accent-dark) !important; } [class^="_active"] { - color: var(--wave-color-solid-accent-fill) !important; + color: var(--wave-color-solid-accent-fill) !important; } [class^="ReactVirtualized__Grid"] { - border-radius: 10px; - margin: 5px; + border-radius: 10px; + margin: 5px; } -[data-test="media-table"]>div>div>div { - border: 1px solid rgb(25, 25, 25) !important; +[data-test="media-table"] > div > div > div { + border: 1px solid rgb(25, 25, 25) !important; } [class^="ReactVirtualized__Grid__innerScrollContainer"] { - border: none; - margin: 5px; + border: none; + margin: 5px; } [class^="_explicitBadge"] { - color: var(--wave-color-solid-accent-fill); + color: var(--wave-color-solid-accent-fill); } [data-test="current-media-imagery"] { - border: 0 !important; - margin: none; + border: 0 !important; + margin: none; } [class^="_imageBorder"] { - display: none; + display: none; } [data-test="feed-sidebar"] { - margin-top: 10px; + margin-top: 10px; } [data-test="footer-player"] { - width: calc(100% - 20px); - bottom: 10px; - left: 10px; - border: 1px solid rgb(25, 25, 25); - border-radius: 4px !important; - position: absolute !important; + width: calc(100% - 20px); + bottom: 10px; + left: 10px; + border: 1px solid rgb(25, 25, 25); + border-radius: 4px !important; + position: absolute !important; } -[class^="_tableRow"]:hover>*, -[data-test-is-playing="true"]>* { - color: var(--wave-color-solid-accent-fill) !important; +[class^="_tableRow"]:hover > *, +[data-test-is-playing="true"] > * { + color: var(--wave-color-solid-accent-fill) !important; } -[class^="_tableRow"]>*, -[data-test-is-playing="false"]>* { - color: lightgray !important; +[class^="_tableRow"] > *, +[data-test-is-playing="false"] > * { + color: lightgray !important; } [class*="coverColumn"] { - padding-left: 5px !important; + padding-left: 5px !important; } [class^="actionList"] { - background-color: transparent; - margin: 0px; - border-radius: 5px; + background-color: transparent; + margin: 0px; + border-radius: 5px; } -.neptune-switch-checkbox:checked+.neptune-switch { - background-color: rgba(255, 255, 255, 0.1); +.neptune-switch-checkbox:checked + .neptune-switch { + background-color: rgba(255, 255, 255, 0.1); } [data-test="main-layout-header"], [data-test="feed-sidebar"], [data-test="stream-metadata"], [data-test="footer-player"] { - background-color: rgba(0, 0, 0, 0.8) !important; - backdrop-filter: blur(10px); - border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important; + background-color: rgba(0, 0, 0, 0.8) !important; + backdrop-filter: blur(10px); + border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important; } -[data-wave-color=textUrl] { - color: var(--wave-color-solid-accent-fill); +[data-wave-color="textUrl"] { + color: var(--wave-color-solid-accent-fill); } [class^="_smallHeader"] { - margin-top: 7.5px; + margin-top: 7.5px; } [class^="__NEPTUNE_PAGE"], [data-test="main"] { - margin-top: 35px; + margin-top: 35px; } #playQueueSidebar { - top: 50px !important; - border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin); - margin: 2px; - margin-right: -14px !important; - background-color: rgba(0, 0, 0, 0.8) !important; - backdrop-filter: blur(10px); + top: 50px !important; + border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin); + margin: 2px; + margin-right: -14px !important; + background-color: rgba(0, 0, 0, 0.8) !important; + backdrop-filter: blur(10px); } [class^="_bottomGradient"] { - visibility: hidden; + visibility: hidden; } [data-test="settings-page"] { - padding-bottom: 60px !important; + padding-bottom: 60px !important; } [data-test="query-suggestions"], [data-test="recent-searches-container"] { - background-color: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(10px); + background-color: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(10px); } [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 { - background-image: var(--img); - filter: blur(10px) brightness(0.4); -} \ No newline at end of file + background-image: var(--img); + filter: blur(10px) brightness(0.4); +} diff --git a/plugins/radiant-lyrics-luna/package.json b/plugins/radiant-lyrics-luna/package.json index ab656f7..147affe 100644 --- a/plugins/radiant-lyrics-luna/package.json +++ b/plugins/radiant-lyrics-luna/package.json @@ -8,4 +8,4 @@ }, "main": "./src/index.ts", "type": "module" -} \ No newline at end of file +} diff --git a/plugins/radiant-lyrics-luna/src/Settings.tsx b/plugins/radiant-lyrics-luna/src/Settings.tsx index 9bda75f..ef17605 100644 --- a/plugins/radiant-lyrics-luna/src/Settings.tsx +++ b/plugins/radiant-lyrics-luna/src/Settings.tsx @@ -5,34 +5,55 @@ import React from "react"; export const settings = await ReactiveStore.getPluginStorage("RadiantLyrics", { hideUIEnabled: true, trackTitleGlow: false, - playerBarVisible: false, + playerBarVisible: false, lyricsGlowEnabled: true, textGlow: 20, - spinningCoverEverywhere: true, + spinningCoverEverywhere: true, performanceMode: false, spinningArtEnabled: true, backgroundContrast: 120, - backgroundBlur: 80, + backgroundBlur: 80, backgroundBrightness: 40, spinSpeed: 45, - settingsAffectNowPlaying: true + settingsAffectNowPlaying: true, }); export const Settings = () => { - const [hideUIEnabled, setHideUIEnabled] = React.useState(settings.hideUIEnabled); - const [playerBarVisible, setPlayerBarVisible] = React.useState(settings.playerBarVisible); - const [lyricsGlowEnabled, setLyricsGlowEnabled] = React.useState(settings.lyricsGlowEnabled); + const [hideUIEnabled, setHideUIEnabled] = React.useState( + settings.hideUIEnabled, + ); + const [playerBarVisible, setPlayerBarVisible] = React.useState( + settings.playerBarVisible, + ); + const [lyricsGlowEnabled, setLyricsGlowEnabled] = React.useState( + settings.lyricsGlowEnabled, + ); const [textGlow, setTextGlow] = React.useState(settings.textGlow); - const [spinningCoverEverywhere, setSpinningCoverEverywhere] = React.useState(settings.spinningCoverEverywhere); - const [performanceMode, setPerformanceMode] = React.useState(settings.performanceMode); - const [spinningArtEnabled, setSpinningArtEnabled] = React.useState(settings.spinningArtEnabled); - const [backgroundContrast, setBackgroundContrast] = React.useState(settings.backgroundContrast); - const [backgroundBlur, setBackgroundBlur] = React.useState(settings.backgroundBlur); - const [backgroundBrightness, setBackgroundBrightness] = React.useState(settings.backgroundBrightness); + const [spinningCoverEverywhere, setSpinningCoverEverywhere] = React.useState( + settings.spinningCoverEverywhere, + ); + const [performanceMode, setPerformanceMode] = React.useState( + settings.performanceMode, + ); + const [spinningArtEnabled, setSpinningArtEnabled] = React.useState( + settings.spinningArtEnabled, + ); + const [backgroundContrast, setBackgroundContrast] = React.useState( + settings.backgroundContrast, + ); + const [backgroundBlur, setBackgroundBlur] = React.useState( + settings.backgroundBlur, + ); + const [backgroundBrightness, setBackgroundBrightness] = React.useState( + settings.backgroundBrightness, + ); const [spinSpeed, setSpinSpeed] = React.useState(settings.spinSpeed); - const [settingsAffectNowPlaying, setSettingsAffectNowPlaying] = React.useState(settings.settingsAffectNowPlaying); - const [trackTitleGlow, setTrackTitleGlow] = React.useState(settings.trackTitleGlow); - + const [settingsAffectNowPlaying, setSettingsAffectNowPlaying] = + React.useState(settings.settingsAffectNowPlaying); + const [trackTitleGlow, setTrackTitleGlow] = React.useState( + settings.trackTitleGlow, + ); + return ( { }} /> { - setTrackTitleGlow((settings.trackTitleGlow = checked)); - if ((window as any).updateRadiantLyricsStyles) { - (window as any).updateRadiantLyricsStyles(); - } - }} - /> + title="Track Title Glow" + desc="Apply glow to the track title" + checked={trackTitleGlow} + onChange={(_: unknown, checked: boolean) => { + setTrackTitleGlow((settings.trackTitleGlow = checked)); + if ((window as any).updateRadiantLyricsStyles) { + (window as any).updateRadiantLyricsStyles(); + } + }} + /> { desc="Apply the spinning Cover Art background to the entire app, not just the Now Playing view, Heavily Inspired by Cover-Theme by @Inrixia" checked={spinningCoverEverywhere} onChange={(_, checked: boolean) => { - console.log("Spinning Cover Everywhere:", checked ? "enabled" : "disabled"); - setSpinningCoverEverywhere((settings.spinningCoverEverywhere = checked)); + console.log( + "Spinning Cover Everywhere:", + checked ? "enabled" : "disabled", + ); + setSpinningCoverEverywhere( + (settings.spinningCoverEverywhere = checked), + ); // Update styles immediately when setting changes if ((window as any).updateRadiantLyricsGlobalBackground) { (window as any).updateRadiantLyricsGlobalBackground(); @@ -113,31 +139,37 @@ export const Settings = () => { desc="Enable the spinning cover art background animation" checked={spinningArtEnabled} onChange={(_, checked: boolean) => { - console.log("Background Cover Spin:", checked ? "enabled" : "disabled"); + console.log( + "Background Cover Spin:", + checked ? "enabled" : "disabled", + ); setSpinningArtEnabled((settings.spinningArtEnabled = checked)); if ((window as any).updateRadiantLyricsGlobalBackground) { (window as any).updateRadiantLyricsGlobalBackground(); } - if (settings.settingsAffectNowPlaying && (window as any).updateRadiantLyricsNowPlayingBackground) { + if ( + settings.settingsAffectNowPlaying && + (window as any).updateRadiantLyricsNowPlayingBackground + ) { (window as any).updateRadiantLyricsNowPlayingBackground(); } }} /> { - setTextGlow((settings.textGlow = value)); - // Update variables immediately when setting changes - if ((window as any).updateRadiantLyricsTextGlow) { - (window as any).updateRadiantLyricsTextGlow(); - } - }} - /> + title="Text Glow" + desc="Adjust the glow size of lyrics (0-100, default: 20)" + min={0} + max={100} + step={1} + value={textGlow} + onNumber={(value: number) => { + setTextGlow((settings.textGlow = value)); + // Update variables immediately when setting changes + if ((window as any).updateRadiantLyricsTextGlow) { + (window as any).updateRadiantLyricsTextGlow(); + } + }} + /> { if ((window as any).updateRadiantLyricsGlobalBackground) { (window as any).updateRadiantLyricsGlobalBackground(); } - if (settings.settingsAffectNowPlaying && (window as any).updateRadiantLyricsNowPlayingBackground) { + if ( + settings.settingsAffectNowPlaying && + (window as any).updateRadiantLyricsNowPlayingBackground + ) { (window as any).updateRadiantLyricsNowPlayingBackground(); } }} @@ -168,7 +203,10 @@ export const Settings = () => { if ((window as any).updateRadiantLyricsGlobalBackground) { (window as any).updateRadiantLyricsGlobalBackground(); } - if (settings.settingsAffectNowPlaying && (window as any).updateRadiantLyricsNowPlayingBackground) { + if ( + settings.settingsAffectNowPlaying && + (window as any).updateRadiantLyricsNowPlayingBackground + ) { (window as any).updateRadiantLyricsNowPlayingBackground(); } }} @@ -186,7 +224,10 @@ export const Settings = () => { if ((window as any).updateRadiantLyricsGlobalBackground) { (window as any).updateRadiantLyricsGlobalBackground(); } - if (settings.settingsAffectNowPlaying && (window as any).updateRadiantLyricsNowPlayingBackground) { + if ( + settings.settingsAffectNowPlaying && + (window as any).updateRadiantLyricsNowPlayingBackground + ) { (window as any).updateRadiantLyricsNowPlayingBackground(); } }} @@ -204,7 +245,10 @@ export const Settings = () => { if ((window as any).updateRadiantLyricsGlobalBackground) { (window as any).updateRadiantLyricsGlobalBackground(); } - if (settings.settingsAffectNowPlaying && (window as any).updateRadiantLyricsNowPlayingBackground) { + if ( + settings.settingsAffectNowPlaying && + (window as any).updateRadiantLyricsNowPlayingBackground + ) { (window as any).updateRadiantLyricsNowPlayingBackground(); } }} @@ -214,8 +258,13 @@ export const Settings = () => { desc="Apply background settings to Now Playing view" checked={settingsAffectNowPlaying} onChange={(_, checked: boolean) => { - console.log("Settings Affect Now Playing:", checked ? "enabled" : "disabled"); - setSettingsAffectNowPlaying((settings.settingsAffectNowPlaying = checked)); + console.log( + "Settings Affect Now Playing:", + checked ? "enabled" : "disabled", + ); + setSettingsAffectNowPlaying( + (settings.settingsAffectNowPlaying = checked), + ); // Update Now Playing background immediately when setting changes if ((window as any).updateRadiantLyricsNowPlayingBackground) { (window as any).updateRadiantLyricsNowPlayingBackground(); @@ -224,4 +273,4 @@ export const Settings = () => { /> ); -}; \ No newline at end of file +}; diff --git a/plugins/radiant-lyrics-luna/src/cover-everywhere.css b/plugins/radiant-lyrics-luna/src/cover-everywhere.css index a22d42a..dad629a 100644 --- a/plugins/radiant-lyrics-luna/src/cover-everywhere.css +++ b/plugins/radiant-lyrics-luna/src/cover-everywhere.css @@ -1,105 +1,105 @@ /* Global Spinning Background Styles - PERFORMANCE OPTIMIZED */ .global-background-container { - position: fixed; - left: 0; - top: 0; - width: 100vw; - height: 100vh; - z-index: -3; - pointer-events: none; - overflow: hidden; - /* Hardware acceleration */ - transform: translateZ(0); - backface-visibility: hidden; + position: fixed; + left: 0; + top: 0; + width: 100vw; + height: 100vh; + z-index: -3; + pointer-events: none; + overflow: hidden; + /* Hardware acceleration */ + transform: translateZ(0); + backface-visibility: hidden; } .global-spinning-black-bg { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - background: #000; - z-index: -2; - pointer-events: none; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: #000; + z-index: -2; + pointer-events: none; } .global-spinning-image { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - width: 150vw; - height: 150vh; - object-fit: cover; - z-index: -1; - filter: blur(80px) brightness(0.4) contrast(1.2) saturate(1); - opacity: 1; - animation: spinGlobal 45s linear infinite; - will-change: transform; - /* Hardware acceleration */ - transform-origin: center center; - backface-visibility: hidden; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 150vw; + height: 150vh; + object-fit: cover; + z-index: -1; + filter: blur(80px) brightness(0.4) contrast(1.2) saturate(1); + opacity: 1; + animation: spinGlobal 45s linear infinite; + will-change: transform; + /* Hardware acceleration */ + transform-origin: center center; + backface-visibility: hidden; } /* Performance mode optimizations - keep spinning but optimize other aspects */ .global-spinning-image.performance-mode-static { - /* Keep animation enabled in performance mode */ - /* Lighter blur for performance */ - filter: blur(20px) brightness(0.4) contrast(1.2) saturate(1) !important; - /* Smaller size for performance */ - width: 120vw !important; - height: 120vh !important; + /* Keep animation enabled in performance mode */ + /* Lighter blur for performance */ + filter: blur(20px) brightness(0.4) contrast(1.2) saturate(1) !important; + /* Smaller size for performance */ + width: 120vw !important; + height: 120vh !important; } .now-playing-background-image.performance-mode-static { - /* Keep animation enabled in performance mode */ - /* Optimized size and effects for performance */ - width: 80vw !important; - height: 80vh !important; + /* Keep animation enabled in performance mode */ + /* Optimized size and effects for performance */ + width: 80vw !important; + height: 80vh !important; } /* Now Playing Background Container Optimization */ .now-playing-background-container { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - z-index: -3; - pointer-events: none; - overflow: hidden; - /* Hardware acceleration */ - transform: translateZ(0); - backface-visibility: hidden; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: -3; + pointer-events: none; + overflow: hidden; + /* Hardware acceleration */ + transform: translateZ(0); + backface-visibility: hidden; } /* Optimized keyframe animations with GPU acceleration */ @keyframes spinGlobal { - from { - transform: translate(-50%, -50%) rotate(0deg); - } - to { - transform: translate(-50%, -50%) rotate(360deg); - } + from { + transform: translate(-50%, -50%) rotate(0deg); + } + to { + transform: translate(-50%, -50%) rotate(360deg); + } } /* Reduced motion for users who prefer it */ @media (prefers-reduced-motion: reduce) { - .global-spinning-image, - .now-playing-background-image { - animation: none !important; - transform: translate(-50%, -50%) !important; - will-change: auto !important; - } + .global-spinning-image, + .now-playing-background-image { + animation: none !important; + transform: translate(-50%, -50%) !important; + will-change: auto !important; + } } /* Performance mode: optimize effects but keep spinning */ .performance-mode .global-spinning-image, .performance-mode .now-playing-background-image { - /* Keep animations but optimize filter effects */ - filter: blur(10px) brightness(0.4) contrast(1.1) !important; + /* Keep animations but optimize filter effects */ + filter: blur(10px) brightness(0.4) contrast(1.1) !important; } /* Make Notification Feed sidebar transparent */ @@ -115,13 +115,13 @@ main, [data-test="stream-metadata"], [data-test="footer-player"], /* Notification Feed sidebar specific container */ -[class^="_feedSidebarVStack"], + [class^="_feedSidebarVStack"], [class^="_feedSidebarSpacer"], [class^="_feedSidebarItem"], [class^="_feedSidebarItemDiv"], [class^="_cellContainer"], [class^="_cellTextContainer"] { - background: unset !important; + background: unset !important; } /* Make sidebar and player bar semi-transparent with optimized backdrop-filter */ @@ -129,9 +129,9 @@ main, [data-test="main-layout-sidebar-wrapper"], [class^="_bar"], [class^="_sidebarItem"]:hover { - background-color: rgba(0, 0, 0, 0.3) !important; - backdrop-filter: blur(10px) !important; - -webkit-backdrop-filter: blur(10px) !important; + background-color: rgba(0, 0, 0, 0.3) !important; + backdrop-filter: blur(10px) !important; + -webkit-backdrop-filter: blur(10px) !important; } /* Performance mode: reduce backdrop blur */ @@ -139,21 +139,21 @@ main, .performance-mode [data-test="main-layout-sidebar-wrapper"], .performance-mode [class^="_bar"], .performance-mode [class^="_sidebarItem"]:hover { - backdrop-filter: blur(5px) !important; - -webkit-backdrop-filter: blur(5px) !important; + backdrop-filter: blur(5px) !important; + -webkit-backdrop-filter: blur(5px) !important; } /* Feed sidebar panel - black tint background for readability */ [data-test="feed-sidebar"] { - background-color: rgba(0, 0, 0, 0.5) !important; - backdrop-filter: blur(10px) !important; - -webkit-backdrop-filter: blur(10px) !important; + background-color: rgba(0, 0, 0, 0.5) !important; + backdrop-filter: blur(10px) !important; + -webkit-backdrop-filter: blur(10px) !important; } /* Performance mode: reduce sidebar backdrop blur */ .performance-mode [data-test="feed-sidebar"] { - backdrop-filter: blur(5px) !important; - -webkit-backdrop-filter: blur(5px) !important; + backdrop-filter: blur(5px) !important; + -webkit-backdrop-filter: blur(5px) !important; } /* Feed sidebar items - transparent */ @@ -162,10 +162,10 @@ main, [class*="_cellContainer"], [data-test="feed-interval"], [data-test="feed-item"] { - background-color: transparent !important; + background-color: transparent !important; } /* Remove bottom gradient */ [class^="_bottomGradient"] { - display: none !important; -} \ No newline at end of file + display: none !important; +} diff --git a/plugins/radiant-lyrics-luna/src/index.ts b/plugins/radiant-lyrics-luna/src/index.ts index 4120563..883abed 100644 --- a/plugins/radiant-lyrics-luna/src/index.ts +++ b/plugins/radiant-lyrics-luna/src/index.ts @@ -16,9 +16,6 @@ export { Settings }; // clean up resources export const unloads = new Set(); - - - // Marker: Styles and Settings Integration // StyleTag instances for different CSS modules const lyricsStyleTag = new StyleTag("RadiantLyrics-lyrics", unloads); @@ -28,243 +25,267 @@ const lyricsGlowStyleTag = new StyleTag("RadiantLyrics-lyrics-glow", unloads); // Apply lyrics glow styles if enabled if (settings.lyricsGlowEnabled) { - lyricsGlowStyleTag.css = lyricsGlow; + lyricsGlowStyleTag.css = lyricsGlow; } // Update CSS variables for lyrics text glow based on settings -const updateRadiantLyricsTextGlow = function(): void { - const root = document.documentElement; - root.style.setProperty('--rl-glow-outer', `${settings.textGlow}px`); - root.style.setProperty('--rl-glow-inner', '2px'); +const updateRadiantLyricsTextGlow = function (): void { + const root = document.documentElement; + root.style.setProperty("--rl-glow-outer", `${settings.textGlow}px`); + root.style.setProperty("--rl-glow-inner", "2px"); }; - // Function to update styles when settings change -const updateRadiantLyricsStyles = function(): void { - if (isHidden) { - // Apply only base styles (button hiding), NOT separated lyrics styles - // to avoid affecting lyrics scrolling behavior - baseStyleTag.css = baseStyles; - - // Apply player bar styles based on setting - if (!settings.playerBarVisible) { - playerBarStyleTag.css = playerBarHidden; - } else { - playerBarStyleTag.remove(); - } - } +const updateRadiantLyricsStyles = function (): void { + if (isHidden) { + // Apply only base styles (button hiding), NOT separated lyrics styles + // to avoid affecting lyrics scrolling behavior + baseStyleTag.css = baseStyles; - // Update lyrics glow based on setting (only if UI is not hidden to avoid interference) - const lyricsContainer = document.querySelector('[class^="_lyricsContainer"]'); - if (lyricsContainer && !isHidden) { - if (settings.lyricsGlowEnabled) { - lyricsContainer.classList.remove('lyrics-glow-disabled'); - lyricsGlowStyleTag.css = lyricsGlow; - updateRadiantLyricsTextGlow(); - } else { - lyricsContainer.classList.add('lyrics-glow-disabled'); - lyricsGlowStyleTag.remove(); - } - } else if (!isHidden) { - observePromise(unloads, '[class^="_lyricsContainer"]').then(el => { - if (!el) return; - if (settings.lyricsGlowEnabled) { - el.classList.remove('lyrics-glow-disabled'); - lyricsGlowStyleTag.css = lyricsGlow; - updateRadiantLyricsTextGlow(); - } else { - el.classList.add('lyrics-glow-disabled'); - lyricsGlowStyleTag.remove(); - } - }).catch(() => {}); - } - - // Track title glow toggle based on settings - const trackTitleEl = document.querySelector('[data-test="now-playing-track-title"]') as HTMLElement | null; - if (trackTitleEl) { - if (settings.trackTitleGlow && settings.lyricsGlowEnabled) { - trackTitleEl.classList.remove('rl-title-glow-disabled'); - } else { - trackTitleEl.classList.add('rl-title-glow-disabled'); - } + // Apply player bar styles based on setting + if (!settings.playerBarVisible) { + playerBarStyleTag.css = playerBarHidden; + } else { + playerBarStyleTag.remove(); } + } + + // Update lyrics glow based on setting (only if UI is not hidden to avoid interference) + const lyricsContainer = document.querySelector('[class^="_lyricsContainer"]'); + if (lyricsContainer && !isHidden) { + if (settings.lyricsGlowEnabled) { + lyricsContainer.classList.remove("lyrics-glow-disabled"); + lyricsGlowStyleTag.css = lyricsGlow; + updateRadiantLyricsTextGlow(); + } else { + lyricsContainer.classList.add("lyrics-glow-disabled"); + lyricsGlowStyleTag.remove(); + } + } else if (!isHidden) { + observePromise(unloads, '[class^="_lyricsContainer"]') + .then((el) => { + if (!el) return; + if (settings.lyricsGlowEnabled) { + el.classList.remove("lyrics-glow-disabled"); + lyricsGlowStyleTag.css = lyricsGlow; + updateRadiantLyricsTextGlow(); + } else { + el.classList.add("lyrics-glow-disabled"); + lyricsGlowStyleTag.remove(); + } + }) + .catch(() => {}); + } + + // Track title glow toggle based on settings + const trackTitleEl = document.querySelector( + '[data-test="now-playing-track-title"]', + ) as HTMLElement | null; + if (trackTitleEl) { + if (settings.trackTitleGlow && settings.lyricsGlowEnabled) { + trackTitleEl.classList.remove("rl-title-glow-disabled"); + } else { + trackTitleEl.classList.add("rl-title-glow-disabled"); + } + } }; - - - // Marker: UI Visibility Control // UI state shared across features var isHidden = false; let unhideButtonAutoFadeTimeout: number | null = null; -const updateButtonStates = function(): void { - const hideButton = document.querySelector('.hide-ui-button') as HTMLElement; - const unhideButton = document.querySelector('.unhide-ui-button') as HTMLElement; - - if (hideButton) { - if (settings.hideUIEnabled && !isHidden) { - hideButton.style.display = 'flex'; - // Small delay to ensure display is set first, then fade in - setTimeout(() => { - hideButton.style.opacity = '1'; - hideButton.style.visibility = 'visible'; - hideButton.style.pointerEvents = 'auto'; - }, 50); - } else { - // Hide UI button immediately when clicked - (couldn't get the fade to work) - hideButton.style.display = 'none'; - hideButton.style.opacity = '0'; - hideButton.style.visibility = 'hidden'; - hideButton.style.pointerEvents = 'none'; - } - } - if (unhideButton) { - // Clear any existing auto-fade timeout - if (unhideButtonAutoFadeTimeout) { - window.clearTimeout(unhideButtonAutoFadeTimeout); - unhideButtonAutoFadeTimeout = null; - } - - if (settings.hideUIEnabled && isHidden) { - unhideButton.style.display = 'flex'; - // Remove the hide-immediately class and let it fade in smoothly - unhideButton.classList.remove('hide-immediately'); - unhideButton.classList.remove('auto-faded'); - // Small delay to ensure display is set first, then fade in - (Works for unhide button.. but not hide button.. because uhh idk) - setTimeout(() => { - unhideButton.style.opacity = '1'; - unhideButton.style.visibility = 'visible'; - unhideButton.style.pointerEvents = 'auto'; - - // Set up auto-fade after 2 seconds - unhideButtonAutoFadeTimeout = window.setTimeout(() => { - if (isHidden && unhideButton && !unhideButton.matches(':hover')) { - unhideButton.classList.add('auto-faded'); - } - }, 2000); - }, 50); - } else { - // Smooth fade out for Unhide UI button - unhideButton.style.opacity = '0'; - unhideButton.style.visibility = 'hidden'; - unhideButton.style.pointerEvents = 'none'; - unhideButton.classList.remove('auto-faded'); - // Keep display: flex to maintain transitions, then hide after fade - setTimeout(() => { - if (unhideButton.style.opacity === '0') { - unhideButton.style.display = 'none'; - } - }, 500); // Wait for transition to complete - } - } -}; +const updateButtonStates = function (): void { + const hideButton = document.querySelector(".hide-ui-button") as HTMLElement; + const unhideButton = document.querySelector( + ".unhide-ui-button", + ) as HTMLElement; + if (hideButton) { + if (settings.hideUIEnabled && !isHidden) { + hideButton.style.display = "flex"; + // Small delay to ensure display is set first, then fade in + setTimeout(() => { + hideButton.style.opacity = "1"; + hideButton.style.visibility = "visible"; + hideButton.style.pointerEvents = "auto"; + }, 50); + } else { + // Hide UI button immediately when clicked - (couldn't get the fade to work) + hideButton.style.display = "none"; + hideButton.style.opacity = "0"; + hideButton.style.visibility = "hidden"; + hideButton.style.pointerEvents = "none"; + } + } + if (unhideButton) { + // Clear any existing auto-fade timeout + if (unhideButtonAutoFadeTimeout) { + window.clearTimeout(unhideButtonAutoFadeTimeout); + unhideButtonAutoFadeTimeout = null; + } + + if (settings.hideUIEnabled && isHidden) { + unhideButton.style.display = "flex"; + // Remove the hide-immediately class and let it fade in smoothly + unhideButton.classList.remove("hide-immediately"); + unhideButton.classList.remove("auto-faded"); + // Small delay to ensure display is set first, then fade in - (Works for unhide button.. but not hide button.. because uhh idk) + setTimeout(() => { + unhideButton.style.opacity = "1"; + unhideButton.style.visibility = "visible"; + unhideButton.style.pointerEvents = "auto"; + + // Set up auto-fade after 2 seconds + unhideButtonAutoFadeTimeout = window.setTimeout(() => { + if (isHidden && unhideButton && !unhideButton.matches(":hover")) { + unhideButton.classList.add("auto-faded"); + } + }, 2000); + }, 50); + } else { + // Smooth fade out for Unhide UI button + unhideButton.style.opacity = "0"; + unhideButton.style.visibility = "hidden"; + unhideButton.style.pointerEvents = "none"; + unhideButton.classList.remove("auto-faded"); + // Keep display: flex to maintain transitions, then hide after fade + setTimeout(() => { + if (unhideButton.style.opacity === "0") { + unhideButton.style.display = "none"; + } + }, 500); // Wait for transition to complete + } + } +}; // Toggle hide/unhide UI -const toggleRadiantLyrics = function(): void { - const nowPlayingContainer = document.querySelector('[class*="_nowPlayingContainer"]') as HTMLElement; - if (isHidden) { - const unhideButton = document.querySelector('.unhide-ui-button') as HTMLElement; - if (unhideButton) unhideButton.classList.add('hide-immediately'); - isHidden = !isHidden; - if (nowPlayingContainer) nowPlayingContainer.classList.remove('radiant-lyrics-ui-hidden'); - document.body.classList.remove('radiant-lyrics-ui-hidden'); - setTimeout(() => { - if (!isHidden) { - lyricsStyleTag.remove(); - baseStyleTag.remove(); - playerBarStyleTag.remove(); - } - }, 500); - updateButtonStates(); - } else { - isHidden = !isHidden; - updateButtonStates(); - setTimeout(() => { - updateRadiantLyricsStyles(); - if (nowPlayingContainer) nowPlayingContainer.classList.add('radiant-lyrics-ui-hidden'); - document.body.classList.add('radiant-lyrics-ui-hidden'); - }, 50); - } +const toggleRadiantLyrics = function (): void { + const nowPlayingContainer = document.querySelector( + '[class*="_nowPlayingContainer"]', + ) as HTMLElement; + if (isHidden) { + const unhideButton = document.querySelector( + ".unhide-ui-button", + ) as HTMLElement; + if (unhideButton) unhideButton.classList.add("hide-immediately"); + isHidden = !isHidden; + if (nowPlayingContainer) + nowPlayingContainer.classList.remove("radiant-lyrics-ui-hidden"); + document.body.classList.remove("radiant-lyrics-ui-hidden"); + setTimeout(() => { + if (!isHidden) { + lyricsStyleTag.remove(); + baseStyleTag.remove(); + playerBarStyleTag.remove(); + } + }, 500); + updateButtonStates(); + } else { + isHidden = !isHidden; + updateButtonStates(); + setTimeout(() => { + updateRadiantLyricsStyles(); + if (nowPlayingContainer) + nowPlayingContainer.classList.add("radiant-lyrics-ui-hidden"); + document.body.classList.add("radiant-lyrics-ui-hidden"); + }, 50); + } }; - // Create buttons -const createHideUIButton = function(): void { - setTimeout(() => { - if (!settings.hideUIEnabled) return; - const fullscreenButton = document.querySelector('[data-test="request-fullscreen"]'); - if (!fullscreenButton || !fullscreenButton.parentElement) { - setTimeout(() => createHideUIButton(), 1000); - return; - } - if (document.querySelector('.hide-ui-button')) return; - const buttonContainer = fullscreenButton.parentElement; - const hideUIButton = document.createElement("button"); - hideUIButton.className = 'hide-ui-button'; - hideUIButton.setAttribute('aria-label', 'Hide UI'); - hideUIButton.setAttribute('title', 'Hide UI'); - hideUIButton.textContent = 'Hide UI'; - hideUIButton.style.backgroundColor = 'var(--wave-color-solid-accent-fill)'; - hideUIButton.style.color = 'black'; - hideUIButton.style.border = 'none'; - hideUIButton.style.borderRadius = '12px'; - hideUIButton.style.height = '40px'; - hideUIButton.style.padding = '0 12px'; - hideUIButton.style.marginLeft = '8px'; - hideUIButton.style.cursor = 'pointer'; - hideUIButton.style.display = 'flex'; - hideUIButton.style.alignItems = 'center'; - hideUIButton.style.justifyContent = 'center'; - hideUIButton.style.fontSize = '12px'; - hideUIButton.style.fontWeight = '600'; - hideUIButton.style.whiteSpace = 'nowrap'; - hideUIButton.style.transition = 'opacity 0.5s ease-in-out, visibility 0.5s ease-in-out, background-color 0.2s ease-in-out'; - hideUIButton.style.opacity = '0'; - hideUIButton.style.visibility = 'hidden'; - hideUIButton.style.pointerEvents = 'none'; - hideUIButton.addEventListener('mouseenter', () => { hideUIButton.style.backgroundColor = 'lightgray'; }); - hideUIButton.addEventListener('mouseleave', () => { hideUIButton.style.backgroundColor = 'var(--wave-color-solid-accent-fill)'; }); - hideUIButton.onclick = toggleRadiantLyrics; - buttonContainer.insertBefore(hideUIButton, fullscreenButton.nextSibling); - setTimeout(() => { - if (settings.hideUIEnabled && !isHidden) { - hideUIButton.style.opacity = '1'; - hideUIButton.style.visibility = 'visible'; - hideUIButton.style.pointerEvents = 'auto'; - } - }, 100); - }, 1000); +const createHideUIButton = function (): void { + setTimeout(() => { + if (!settings.hideUIEnabled) return; + const fullscreenButton = document.querySelector( + '[data-test="request-fullscreen"]', + ); + if (!fullscreenButton || !fullscreenButton.parentElement) { + setTimeout(() => createHideUIButton(), 1000); + return; + } + if (document.querySelector(".hide-ui-button")) return; + const buttonContainer = fullscreenButton.parentElement; + const hideUIButton = document.createElement("button"); + hideUIButton.className = "hide-ui-button"; + hideUIButton.setAttribute("aria-label", "Hide UI"); + hideUIButton.setAttribute("title", "Hide UI"); + hideUIButton.textContent = "Hide UI"; + hideUIButton.style.backgroundColor = "var(--wave-color-solid-accent-fill)"; + hideUIButton.style.color = "black"; + hideUIButton.style.border = "none"; + hideUIButton.style.borderRadius = "12px"; + hideUIButton.style.height = "40px"; + hideUIButton.style.padding = "0 12px"; + hideUIButton.style.marginLeft = "8px"; + hideUIButton.style.cursor = "pointer"; + hideUIButton.style.display = "flex"; + hideUIButton.style.alignItems = "center"; + hideUIButton.style.justifyContent = "center"; + hideUIButton.style.fontSize = "12px"; + hideUIButton.style.fontWeight = "600"; + hideUIButton.style.whiteSpace = "nowrap"; + hideUIButton.style.transition = + "opacity 0.5s ease-in-out, visibility 0.5s ease-in-out, background-color 0.2s ease-in-out"; + hideUIButton.style.opacity = "0"; + hideUIButton.style.visibility = "hidden"; + hideUIButton.style.pointerEvents = "none"; + hideUIButton.addEventListener("mouseenter", () => { + hideUIButton.style.backgroundColor = "lightgray"; + }); + hideUIButton.addEventListener("mouseleave", () => { + hideUIButton.style.backgroundColor = + "var(--wave-color-solid-accent-fill)"; + }); + hideUIButton.onclick = toggleRadiantLyrics; + buttonContainer.insertBefore(hideUIButton, fullscreenButton.nextSibling); + setTimeout(() => { + if (settings.hideUIEnabled && !isHidden) { + hideUIButton.style.opacity = "1"; + hideUIButton.style.visibility = "visible"; + hideUIButton.style.pointerEvents = "auto"; + } + }, 100); + }, 1000); }; - -const createUnhideUIButton = function(): void { - setTimeout(() => { - if (!settings.hideUIEnabled) return; - if (document.querySelector('.unhide-ui-button')) return; - const nowPlayingContainer = document.querySelector('[class*="_nowPlayingContainer"]') as HTMLElement; - if (!nowPlayingContainer) { - setTimeout(() => createUnhideUIButton(), 1000); - return; - } - const unhideUIButton = document.createElement("button"); - unhideUIButton.className = 'unhide-ui-button'; - unhideUIButton.setAttribute('aria-label', 'Unhide UI'); - unhideUIButton.setAttribute('title', 'Unhide UI'); - unhideUIButton.textContent = 'Unhide'; - unhideUIButton.style.cssText = `position: absolute; top: 10px; right: 10px; background-color: rgba(255,255,255,0.2); color: white; border: 1px solid rgba(255,255,255,0.3); border-radius: 12px; height: 40px; padding: 0 12px; cursor: pointer; display: none; align-items: center; justify-content: center; transition: all 0.5s ease-in-out; font-size: 12px; font-weight: 600; white-space: nowrap; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); opacity: 0; visibility: hidden; pointer-events: none;`; - unhideUIButton.addEventListener('mouseenter', () => { unhideUIButton.style.backgroundColor = 'rgba(255,255,255,0.3)'; unhideUIButton.style.transform = 'scale(1.05)'; unhideUIButton.classList.remove('auto-faded'); }); - unhideUIButton.addEventListener('mouseleave', () => { unhideUIButton.style.backgroundColor = 'rgba(255,255,255,0.2)'; unhideUIButton.style.transform = 'scale(1)'; window.setTimeout(() => { if (isHidden && !unhideUIButton.matches(':hover')) { unhideUIButton.classList.add('auto-faded'); } }, 2000); }); - unhideUIButton.onclick = toggleRadiantLyrics; - nowPlayingContainer.appendChild(unhideUIButton); - updateButtonStates(); - }, 1500); +const createUnhideUIButton = function (): void { + setTimeout(() => { + if (!settings.hideUIEnabled) return; + if (document.querySelector(".unhide-ui-button")) return; + const nowPlayingContainer = document.querySelector( + '[class*="_nowPlayingContainer"]', + ) as HTMLElement; + if (!nowPlayingContainer) { + setTimeout(() => createUnhideUIButton(), 1000); + return; + } + const unhideUIButton = document.createElement("button"); + unhideUIButton.className = "unhide-ui-button"; + unhideUIButton.setAttribute("aria-label", "Unhide UI"); + unhideUIButton.setAttribute("title", "Unhide UI"); + unhideUIButton.textContent = "Unhide"; + unhideUIButton.style.cssText = `position: absolute; top: 10px; right: 10px; background-color: rgba(255,255,255,0.2); color: white; border: 1px solid rgba(255,255,255,0.3); border-radius: 12px; height: 40px; padding: 0 12px; cursor: pointer; display: none; align-items: center; justify-content: center; transition: all 0.5s ease-in-out; font-size: 12px; font-weight: 600; white-space: nowrap; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); z-index: 1000; box-shadow: 0 4px 12px rgba(0,0,0,0.3); opacity: 0; visibility: hidden; pointer-events: none;`; + unhideUIButton.addEventListener("mouseenter", () => { + unhideUIButton.style.backgroundColor = "rgba(255,255,255,0.3)"; + unhideUIButton.style.transform = "scale(1.05)"; + unhideUIButton.classList.remove("auto-faded"); + }); + unhideUIButton.addEventListener("mouseleave", () => { + unhideUIButton.style.backgroundColor = "rgba(255,255,255,0.2)"; + unhideUIButton.style.transform = "scale(1)"; + window.setTimeout(() => { + if (isHidden && !unhideUIButton.matches(":hover")) { + unhideUIButton.classList.add("auto-faded"); + } + }, 2000); + }); + unhideUIButton.onclick = toggleRadiantLyrics; + nowPlayingContainer.appendChild(unhideUIButton); + updateButtonStates(); + }, 1500); }; - - - // Marker: Background Rendering // Variable setup let globalSpinningBgStyleTag: StyleTag | null = null; @@ -283,60 +304,73 @@ let nowPlayingGradientOverlay: HTMLElement | null = null; let currentNowPlayingCoverSrc: string | null = null; let spinAnimationAdded = false; - // Update Cover Art background for Now Playing and Global function updateCoverArtBackground(method: number = 0): void { - if (method === 1) { - setTimeout(() => { - updateCoverArtBackground(); - return; - }, 2000); - } + if (method === 1) { + setTimeout(() => { + updateCoverArtBackground(); + return; + }, 2000); + } - let coverArtImageElement = document.querySelector('figure[class*="_albumImage"] > div > div > div > img') as HTMLImageElement; - let coverArtImageSrc: string | null = null; + let coverArtImageElement = document.querySelector( + 'figure[class*="_albumImage"] > div > div > div > img', + ) as HTMLImageElement; + let coverArtImageSrc: string | null = null; - if (coverArtImageElement) { - coverArtImageSrc = coverArtImageElement.src; - // Use higher resolution for better quality, but consider performance mode - const targetRes = settings.performanceMode ? '640x640' : '1280x1280'; - coverArtImageSrc = coverArtImageSrc.replace(/\d+x\d+/, targetRes); - if (coverArtImageElement.src !== coverArtImageSrc) { - coverArtImageElement.src = coverArtImageSrc; - } - } else { - const videoElement = document.querySelector('figure[class*="_albumImage"] > div > div > div > video') as HTMLVideoElement; - if (videoElement) { - coverArtImageSrc = videoElement.getAttribute("poster"); - if (coverArtImageSrc) { - const targetRes = settings.performanceMode ? '640x640' : '1280x1280'; - coverArtImageSrc = coverArtImageSrc.replace(/\d+x\d+/, targetRes); - } - } else { - cleanUpDynamicArt(); - return; - } - } + if (coverArtImageElement) { + coverArtImageSrc = coverArtImageElement.src; + // Use higher resolution for better quality, but consider performance mode + const targetRes = settings.performanceMode ? "640x640" : "1280x1280"; + coverArtImageSrc = coverArtImageSrc.replace(/\d+x\d+/, targetRes); + if (coverArtImageElement.src !== coverArtImageSrc) { + coverArtImageElement.src = coverArtImageSrc; + } + } else { + const videoElement = document.querySelector( + 'figure[class*="_albumImage"] > div > div > div > video', + ) as HTMLVideoElement; + if (videoElement) { + coverArtImageSrc = videoElement.getAttribute("poster"); + if (coverArtImageSrc) { + const targetRes = settings.performanceMode ? "640x640" : "1280x1280"; + coverArtImageSrc = coverArtImageSrc.replace(/\d+x\d+/, targetRes); + } + } else { + cleanUpDynamicArt(); + return; + } + } - // Update backgrounds when we have a valid cover art source - if (coverArtImageSrc) { - // Apply global spinning background if enabled - if (settings.spinningCoverEverywhere) { - applyGlobalSpinningBackground(coverArtImageSrc); - } - - // Apply spinning CoverArt background to the Now Playing container - OPTIMIZED - const nowPlayingContainerElement = document.querySelector('[class*="_nowPlayingContainer"]') as HTMLElement; - if (nowPlayingContainerElement) { - // Create DOM structure if it doesn't exist (REUSE ELEMENTS) - if (!nowPlayingBackgroundContainer || !nowPlayingContainerElement.contains(nowPlayingBackgroundContainer)) { - // Clean up any old elements first - nowPlayingContainerElement.querySelectorAll('.now-playing-background-image, .now-playing-black-bg, .now-playing-gradient-overlay').forEach(el => el.remove()); - - // Create container - nowPlayingBackgroundContainer = document.createElement('div'); - nowPlayingBackgroundContainer.className = 'now-playing-background-container'; - nowPlayingBackgroundContainer.style.cssText = ` + // Update backgrounds when we have a valid cover art source + if (coverArtImageSrc) { + // Apply global spinning background if enabled + if (settings.spinningCoverEverywhere) { + applyGlobalSpinningBackground(coverArtImageSrc); + } + + // Apply spinning CoverArt background to the Now Playing container - OPTIMIZED + const nowPlayingContainerElement = document.querySelector( + '[class*="_nowPlayingContainer"]', + ) as HTMLElement; + if (nowPlayingContainerElement) { + // Create DOM structure if it doesn't exist (REUSE ELEMENTS) + if ( + !nowPlayingBackgroundContainer || + !nowPlayingContainerElement.contains(nowPlayingBackgroundContainer) + ) { + // Clean up any old elements first + nowPlayingContainerElement + .querySelectorAll( + ".now-playing-background-image, .now-playing-black-bg, .now-playing-gradient-overlay", + ) + .forEach((el) => el.remove()); + + // Create container + nowPlayingBackgroundContainer = document.createElement("div"); + nowPlayingBackgroundContainer.className = + "now-playing-background-container"; + nowPlayingBackgroundContainer.style.cssText = ` position: absolute; left: 0; top: 0; @@ -346,12 +380,12 @@ function updateCoverArtBackground(method: number = 0): void { pointer-events: none; overflow: hidden; `; - nowPlayingContainerElement.appendChild(nowPlayingBackgroundContainer); + nowPlayingContainerElement.appendChild(nowPlayingBackgroundContainer); - // Create black background layer - nowPlayingBlackBg = document.createElement('div'); - nowPlayingBlackBg.className = 'now-playing-black-bg'; - nowPlayingBlackBg.style.cssText = ` + // Create black background layer + nowPlayingBlackBg = document.createElement("div"); + nowPlayingBlackBg.className = "now-playing-black-bg"; + nowPlayingBlackBg.style.cssText = ` position: absolute; left: 0; top: 0; @@ -361,12 +395,12 @@ function updateCoverArtBackground(method: number = 0): void { z-index: -2; pointer-events: none; `; - nowPlayingBackgroundContainer.appendChild(nowPlayingBlackBg); + nowPlayingBackgroundContainer.appendChild(nowPlayingBlackBg); - // Create background image - nowPlayingBackgroundImage = document.createElement('img'); - nowPlayingBackgroundImage.className = 'now-playing-background-image'; - nowPlayingBackgroundImage.style.cssText = ` + // Create background image + nowPlayingBackgroundImage = document.createElement("img"); + nowPlayingBackgroundImage.className = "now-playing-background-image"; + nowPlayingBackgroundImage.style.cssText = ` position: absolute; left: 50%; top: 50%; @@ -375,12 +409,12 @@ function updateCoverArtBackground(method: number = 0): void { z-index: -1; transform-origin: center center; `; - nowPlayingBackgroundContainer.appendChild(nowPlayingBackgroundImage); + nowPlayingBackgroundContainer.appendChild(nowPlayingBackgroundImage); - // Create gradient overlay - nowPlayingGradientOverlay = document.createElement('div'); - nowPlayingGradientOverlay.className = 'now-playing-gradient-overlay'; - nowPlayingGradientOverlay.style.cssText = ` + // Create gradient overlay + nowPlayingGradientOverlay = document.createElement("div"); + nowPlayingGradientOverlay.className = "now-playing-gradient-overlay"; + nowPlayingGradientOverlay.style.cssText = ` position: absolute; left: 0; top: 0; @@ -390,91 +424,116 @@ function updateCoverArtBackground(method: number = 0): void { z-index: -1; pointer-events: none; `; - nowPlayingBackgroundContainer.appendChild(nowPlayingGradientOverlay); - } + nowPlayingBackgroundContainer.appendChild(nowPlayingGradientOverlay); + } - // Update image source efficiently - if (nowPlayingBackgroundImage && nowPlayingBackgroundImage.src !== coverArtImageSrc) { - nowPlayingBackgroundImage.src = coverArtImageSrc; - currentNowPlayingCoverSrc = coverArtImageSrc; - } + // Update image source efficiently + if ( + nowPlayingBackgroundImage && + nowPlayingBackgroundImage.src !== coverArtImageSrc + ) { + nowPlayingBackgroundImage.src = coverArtImageSrc; + currentNowPlayingCoverSrc = coverArtImageSrc; + } - // Apply performance-optimized settings - if (nowPlayingBackgroundImage) { - if (settings.performanceMode) { - // Performance mode with spinning enabled - const blur = Math.min(settings.backgroundBlur, 20); - const contrast = Math.min(settings.backgroundContrast, 150); - if (nowPlayingBackgroundImage.style.width !== '70vw') nowPlayingBackgroundImage.style.width = '70vw'; - if (nowPlayingBackgroundImage.style.height !== '70vh') nowPlayingBackgroundImage.style.height = '70vh'; - const filt = `blur(${blur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${contrast}%)`; - if (nowPlayingBackgroundImage.style.filter !== filt) nowPlayingBackgroundImage.style.filter = filt; - const anim = settings.spinningArtEnabled ? `spin ${settings.spinSpeed}s linear infinite` : 'none'; - const wc = settings.spinningArtEnabled ? 'transform' : 'auto'; - if (nowPlayingBackgroundImage.style.animation !== anim) nowPlayingBackgroundImage.style.animation = anim; - if (nowPlayingBackgroundImage.style.willChange !== wc) nowPlayingBackgroundImage.style.willChange = wc; - nowPlayingBackgroundImage.classList.remove('performance-mode-static'); - } else { - // Normal mode - if (nowPlayingBackgroundImage.style.width !== '90vw') nowPlayingBackgroundImage.style.width = '90vw'; - if (nowPlayingBackgroundImage.style.height !== '90vh') nowPlayingBackgroundImage.style.height = '90vh'; - const filt = `blur(${settings.backgroundBlur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${settings.backgroundContrast}%)`; - if (nowPlayingBackgroundImage.style.filter !== filt) nowPlayingBackgroundImage.style.filter = filt; - const anim = settings.spinningArtEnabled ? `spin ${settings.spinSpeed}s linear infinite` : 'none'; - const wc = settings.spinningArtEnabled ? 'transform' : 'auto'; - if (nowPlayingBackgroundImage.style.animation !== anim) nowPlayingBackgroundImage.style.animation = anim; - if (nowPlayingBackgroundImage.style.willChange !== wc) nowPlayingBackgroundImage.style.willChange = wc; - nowPlayingBackgroundImage.classList.remove('performance-mode-static'); - } - } + // Apply performance-optimized settings + if (nowPlayingBackgroundImage) { + if (settings.performanceMode) { + // Performance mode with spinning enabled + const blur = Math.min(settings.backgroundBlur, 20); + const contrast = Math.min(settings.backgroundContrast, 150); + if (nowPlayingBackgroundImage.style.width !== "70vw") + nowPlayingBackgroundImage.style.width = "70vw"; + if (nowPlayingBackgroundImage.style.height !== "70vh") + nowPlayingBackgroundImage.style.height = "70vh"; + const filt = `blur(${blur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${contrast}%)`; + if (nowPlayingBackgroundImage.style.filter !== filt) + nowPlayingBackgroundImage.style.filter = filt; + const anim = settings.spinningArtEnabled + ? `spin ${settings.spinSpeed}s linear infinite` + : "none"; + const wc = settings.spinningArtEnabled ? "transform" : "auto"; + if (nowPlayingBackgroundImage.style.animation !== anim) + nowPlayingBackgroundImage.style.animation = anim; + if (nowPlayingBackgroundImage.style.willChange !== wc) + nowPlayingBackgroundImage.style.willChange = wc; + nowPlayingBackgroundImage.classList.remove("performance-mode-static"); + } else { + // Normal mode + if (nowPlayingBackgroundImage.style.width !== "90vw") + nowPlayingBackgroundImage.style.width = "90vw"; + if (nowPlayingBackgroundImage.style.height !== "90vh") + nowPlayingBackgroundImage.style.height = "90vh"; + const filt = `blur(${settings.backgroundBlur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${settings.backgroundContrast}%)`; + if (nowPlayingBackgroundImage.style.filter !== filt) + nowPlayingBackgroundImage.style.filter = filt; + const anim = settings.spinningArtEnabled + ? `spin ${settings.spinSpeed}s linear infinite` + : "none"; + const wc = settings.spinningArtEnabled ? "transform" : "auto"; + if (nowPlayingBackgroundImage.style.animation !== anim) + nowPlayingBackgroundImage.style.animation = anim; + if (nowPlayingBackgroundImage.style.willChange !== wc) + nowPlayingBackgroundImage.style.willChange = wc; + nowPlayingBackgroundImage.classList.remove("performance-mode-static"); + } + } - // Add keyframe animation only once - if (!spinAnimationAdded) { - const styleSheet = document.createElement('style'); - styleSheet.id = 'spinAnimation'; - styleSheet.textContent = ` + // Add keyframe animation only once + if (!spinAnimationAdded) { + const styleSheet = document.createElement("style"); + styleSheet.id = "spinAnimation"; + styleSheet.textContent = ` @keyframes spin { from { transform: translate(-50%, -50%) rotate(0deg); } to { transform: translate(-50%, -50%) rotate(360deg); } } `; - document.head.appendChild(styleSheet); - spinAnimationAdded = true; - } - } - } + document.head.appendChild(styleSheet); + spinAnimationAdded = true; + } + } + } } - // Function to apply spinning background to the entire app (cover everywhere) const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => { - const appContainer = document.querySelector('[data-test="main"]') as HTMLElement; - - if (!settings.spinningCoverEverywhere) { - cleanUpGlobalSpinningBackground(); - return; - } + const appContainer = document.querySelector( + '[data-test="main"]', + ) as HTMLElement; - // Throttle updates to prevent excessive DOM manipulation - const now = Date.now(); - if (now - lastUpdateTime < getUpdateThrottle() && currentGlobalCoverSrc === coverArtImageSrc) { - return; - } - lastUpdateTime = now; - currentGlobalCoverSrc = coverArtImageSrc; + if (!settings.spinningCoverEverywhere) { + cleanUpGlobalSpinningBackground(); + return; + } - // Add StyleTag if not present - if (!globalSpinningBgStyleTag) { - globalSpinningBgStyleTag = new StyleTag("RadiantLyrics-global-spinning-bg", unloads, coverEverywhereCss); - } + // Throttle updates to prevent excessive DOM manipulation + const now = Date.now(); + if ( + now - lastUpdateTime < getUpdateThrottle() && + currentGlobalCoverSrc === coverArtImageSrc + ) { + return; + } + lastUpdateTime = now; + currentGlobalCoverSrc = coverArtImageSrc; - if (!appContainer) return; + // Add StyleTag if not present + if (!globalSpinningBgStyleTag) { + globalSpinningBgStyleTag = new StyleTag( + "RadiantLyrics-global-spinning-bg", + unloads, + coverEverywhereCss, + ); + } - // Create container structure if it doesn't exist (REUSE DOM ELEMENTS) - if (!globalBackgroundContainer) { - globalBackgroundContainer = document.createElement('div'); - globalBackgroundContainer.className = 'global-background-container'; - globalBackgroundContainer.style.cssText = ` + if (!appContainer) return; + + // Create container structure if it doesn't exist (REUSE DOM ELEMENTS) + if (!globalBackgroundContainer) { + globalBackgroundContainer = document.createElement("div"); + globalBackgroundContainer.className = "global-background-container"; + globalBackgroundContainer.style.cssText = ` position: fixed; left: 0; top: 0; @@ -484,17 +543,17 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => { pointer-events: none; overflow: hidden; `; - appContainer.appendChild(globalBackgroundContainer); + appContainer.appendChild(globalBackgroundContainer); - // Create black background layer - globalBlackBg = document.createElement('div'); - globalBlackBg.className = 'global-spinning-black-bg'; - globalBackgroundContainer.appendChild(globalBlackBg); + // Create black background layer + globalBlackBg = document.createElement("div"); + globalBlackBg.className = "global-spinning-black-bg"; + globalBackgroundContainer.appendChild(globalBlackBg); - // Create image element - globalBackgroundImage = document.createElement('img'); - globalBackgroundImage.className = 'global-spinning-image'; - globalBackgroundImage.style.cssText = ` + // Create image element + globalBackgroundImage = document.createElement("img"); + globalBackgroundImage.className = "global-spinning-image"; + globalBackgroundImage.style.cssText = ` position: absolute; left: 50%; top: 50%; @@ -504,192 +563,202 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => { will-change: transform; transform-origin: center center; `; - globalBackgroundContainer.appendChild(globalBackgroundImage); - } + globalBackgroundContainer.appendChild(globalBackgroundImage); + } - // Update image source efficiently - if (globalBackgroundImage && globalBackgroundImage.src !== coverArtImageSrc) { - globalBackgroundImage.src = coverArtImageSrc; - } + // Update image source efficiently + if (globalBackgroundImage && globalBackgroundImage.src !== coverArtImageSrc) { + globalBackgroundImage.src = coverArtImageSrc; + } - // Apply performance-optimized settings - if (globalBackgroundImage) { - // Performance mode optimizations - if (settings.performanceMode) { - // Performance mode with spinning enabled - globalBackgroundImage.style.width = '100vw'; - globalBackgroundImage.style.height = '100vh'; - globalBackgroundImage.style.filter = `blur(${Math.min(settings.backgroundBlur, 20)}px) brightness(${settings.backgroundBrightness / 100}) contrast(${Math.min(settings.backgroundContrast, 150)}%)`; - if (settings.spinningArtEnabled) { - globalBackgroundImage.style.animation = `spinGlobal ${settings.spinSpeed}s linear infinite`; - globalBackgroundImage.style.willChange = 'transform'; - } - else { - globalBackgroundImage.style.animation = 'none'; - globalBackgroundImage.style.willChange = 'auto'; - } - globalBackgroundImage.classList.remove('performance-mode-static'); - } else { - // Normal mode - globalBackgroundImage.style.width = '150vw'; - globalBackgroundImage.style.height = '150vh'; - globalBackgroundImage.style.filter = `blur(${settings.backgroundBlur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${settings.backgroundContrast}%)`; - if (settings.spinningArtEnabled) { - globalBackgroundImage.style.animation = `spinGlobal ${settings.spinSpeed}s linear infinite`; - globalBackgroundImage.style.willChange = 'transform'; - } - else { - globalBackgroundImage.style.animation = 'none'; - globalBackgroundImage.style.willChange = 'auto'; - } - globalBackgroundImage.classList.remove('performance-mode-static'); - } - } + // Apply performance-optimized settings + if (globalBackgroundImage) { + // Performance mode optimizations + if (settings.performanceMode) { + // Performance mode with spinning enabled + globalBackgroundImage.style.width = "100vw"; + globalBackgroundImage.style.height = "100vh"; + globalBackgroundImage.style.filter = `blur(${Math.min(settings.backgroundBlur, 20)}px) brightness(${settings.backgroundBrightness / 100}) contrast(${Math.min(settings.backgroundContrast, 150)}%)`; + if (settings.spinningArtEnabled) { + globalBackgroundImage.style.animation = `spinGlobal ${settings.spinSpeed}s linear infinite`; + globalBackgroundImage.style.willChange = "transform"; + } else { + globalBackgroundImage.style.animation = "none"; + globalBackgroundImage.style.willChange = "auto"; + } + globalBackgroundImage.classList.remove("performance-mode-static"); + } else { + // Normal mode + globalBackgroundImage.style.width = "150vw"; + globalBackgroundImage.style.height = "150vh"; + globalBackgroundImage.style.filter = `blur(${settings.backgroundBlur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${settings.backgroundContrast}%)`; + if (settings.spinningArtEnabled) { + globalBackgroundImage.style.animation = `spinGlobal ${settings.spinSpeed}s linear infinite`; + globalBackgroundImage.style.willChange = "transform"; + } else { + globalBackgroundImage.style.animation = "none"; + globalBackgroundImage.style.willChange = "auto"; + } + globalBackgroundImage.classList.remove("performance-mode-static"); + } + } }; - // cleanup function -const cleanUpGlobalSpinningBackground = function(): void { - if (globalBackgroundContainer && globalBackgroundContainer.parentNode) { - globalBackgroundContainer.parentNode.removeChild(globalBackgroundContainer); - } - globalBackgroundContainer = null; - globalBackgroundImage = null; - globalBlackBg = null; - currentGlobalCoverSrc = null; - - if (globalSpinningBgStyleTag) { - globalSpinningBgStyleTag.remove(); - globalSpinningBgStyleTag = null; - } -}; +const cleanUpGlobalSpinningBackground = function (): void { + if (globalBackgroundContainer && globalBackgroundContainer.parentNode) { + globalBackgroundContainer.parentNode.removeChild(globalBackgroundContainer); + } + globalBackgroundContainer = null; + globalBackgroundImage = null; + globalBlackBg = null; + currentGlobalCoverSrc = null; + if (globalSpinningBgStyleTag) { + globalSpinningBgStyleTag.remove(); + globalSpinningBgStyleTag = null; + } +}; // Function to update global background when settings change -const updateRadiantLyricsGlobalBackground = function(): void { - // Apply performance mode class to document body - if (settings.performanceMode) { - document.body.classList.add('performance-mode'); - } else { - document.body.classList.remove('performance-mode'); - } - - if (settings.spinningCoverEverywhere) { - // Get current cover art and apply global background - updateCoverArtBackground(); - } else { - cleanUpGlobalSpinningBackground(); - } +const updateRadiantLyricsGlobalBackground = function (): void { + // Apply performance mode class to document body + if (settings.performanceMode) { + document.body.classList.add("performance-mode"); + } else { + document.body.classList.remove("performance-mode"); + } + + if (settings.spinningCoverEverywhere) { + // Get current cover art and apply global background + updateCoverArtBackground(); + } else { + cleanUpGlobalSpinningBackground(); + } }; - // Function to update Now Playing background when settings change -const updateRadiantLyricsNowPlayingBackground = function(): void { - const nowPlayingBackgroundImages = document.querySelectorAll('.now-playing-background-image'); - nowPlayingBackgroundImages.forEach((img: Element) => { - const imgElement = img as HTMLElement; - - // Default values when settings don't affect Now Playing - const defaultBlur = 80; - const defaultBrightness = 40; - const defaultContrast = 120; - const defaultSpinSpeed = 45; - - let blur, brightness, contrast, spinSpeed; - - if (settings.settingsAffectNowPlaying) { - blur = settings.backgroundBlur; - brightness = settings.backgroundBrightness; - contrast = settings.backgroundContrast; - spinSpeed = settings.spinSpeed; - } else { - blur = defaultBlur; - brightness = defaultBrightness; - contrast = defaultContrast; - spinSpeed = defaultSpinSpeed; - } - - // Performance mode optimizations - if (settings.performanceMode) { - // Reduce blur and effects for better performance, but keep spinning - blur = Math.min(blur, 20); - contrast = Math.min(contrast, 150); - if (settings.spinningArtEnabled) { - imgElement.style.animation = `spin ${spinSpeed}s linear infinite`; - imgElement.style.willChange = 'transform'; - } - else { - imgElement.style.animation = 'none'; - imgElement.style.willChange = 'auto'; - } - imgElement.classList.remove('performance-mode-static'); - } else { - if (settings.spinningArtEnabled) { - imgElement.style.animation = `spin ${spinSpeed}s linear infinite`; - imgElement.style.willChange = 'transform'; - } - else { - imgElement.style.animation = 'none'; - imgElement.style.willChange = 'auto'; - } - imgElement.classList.remove('performance-mode-static'); - } - - imgElement.style.filter = `blur(${blur}px) brightness(${brightness / 100}) contrast(${contrast}%)`; - }); +const updateRadiantLyricsNowPlayingBackground = function (): void { + const nowPlayingBackgroundImages = document.querySelectorAll( + ".now-playing-background-image", + ); + nowPlayingBackgroundImages.forEach((img: Element) => { + const imgElement = img as HTMLElement; + + // Default values when settings don't affect Now Playing + const defaultBlur = 80; + const defaultBrightness = 40; + const defaultContrast = 120; + const defaultSpinSpeed = 45; + + let blur, brightness, contrast, spinSpeed; + + if (settings.settingsAffectNowPlaying) { + blur = settings.backgroundBlur; + brightness = settings.backgroundBrightness; + contrast = settings.backgroundContrast; + spinSpeed = settings.spinSpeed; + } else { + blur = defaultBlur; + brightness = defaultBrightness; + contrast = defaultContrast; + spinSpeed = defaultSpinSpeed; + } + + // Performance mode optimizations + if (settings.performanceMode) { + // Reduce blur and effects for better performance, but keep spinning + blur = Math.min(blur, 20); + contrast = Math.min(contrast, 150); + if (settings.spinningArtEnabled) { + imgElement.style.animation = `spin ${spinSpeed}s linear infinite`; + imgElement.style.willChange = "transform"; + } else { + imgElement.style.animation = "none"; + imgElement.style.willChange = "auto"; + } + imgElement.classList.remove("performance-mode-static"); + } else { + if (settings.spinningArtEnabled) { + imgElement.style.animation = `spin ${spinSpeed}s linear infinite`; + imgElement.style.willChange = "transform"; + } else { + imgElement.style.animation = "none"; + imgElement.style.willChange = "auto"; + } + imgElement.classList.remove("performance-mode-static"); + } + + imgElement.style.filter = `blur(${blur}px) brightness(${brightness / 100}) contrast(${contrast}%)`; + }); }; // Make these functions available globally so Settings can call them (window as any).updateRadiantLyricsStyles = updateRadiantLyricsStyles; -(window as any).updateRadiantLyricsGlobalBackground = updateRadiantLyricsGlobalBackground; -(window as any).updateRadiantLyricsNowPlayingBackground = updateRadiantLyricsNowPlayingBackground; +(window as any).updateRadiantLyricsGlobalBackground = + updateRadiantLyricsGlobalBackground; +(window as any).updateRadiantLyricsNowPlayingBackground = + updateRadiantLyricsNowPlayingBackground; (window as any).updateRadiantLyricsTextGlow = updateRadiantLyricsTextGlow; - const cleanUpDynamicArt = function (): void { - // Clean up cached Now Playing elements - if (nowPlayingBackgroundContainer && nowPlayingBackgroundContainer.parentNode) { - nowPlayingBackgroundContainer.parentNode.removeChild(nowPlayingBackgroundContainer); - } - nowPlayingBackgroundContainer = null; - nowPlayingBackgroundImage = null; - nowPlayingBlackBg = null; - nowPlayingGradientOverlay = null; - currentNowPlayingCoverSrc = null; - - // Clean up any remaining elements (fallback) - const nowPlayingBackgroundImages = document.getElementsByClassName("now-playing-background-image"); - Array.from(nowPlayingBackgroundImages).forEach((element) => { - element.remove(); - }); - - // Also clean up global spinning backgrounds - cleanUpGlobalSpinningBackground(); + // Clean up cached Now Playing elements + if ( + nowPlayingBackgroundContainer && + nowPlayingBackgroundContainer.parentNode + ) { + nowPlayingBackgroundContainer.parentNode.removeChild( + nowPlayingBackgroundContainer, + ); + } + nowPlayingBackgroundContainer = null; + nowPlayingBackgroundImage = null; + nowPlayingBlackBg = null; + nowPlayingGradientOverlay = null; + currentNowPlayingCoverSrc = null; + + // Clean up any remaining elements (fallback) + const nowPlayingBackgroundImages = document.getElementsByClassName( + "now-playing-background-image", + ); + Array.from(nowPlayingBackgroundImages).forEach((element) => { + element.remove(); + }); + + // Also clean up global spinning backgrounds + cleanUpGlobalSpinningBackground(); }; - // Reduce work when tab hidden: pause animations; restore on visible -document.addEventListener('visibilitychange', () => { - const isHiddenDoc = document.hidden; - const images = document.querySelectorAll('.global-spinning-image, .now-playing-background-image'); - images.forEach(img => { - const el = img as HTMLElement; - if (isHiddenDoc) { - // Pause animation but keep state - if (el.style.animationPlayState !== 'paused') el.style.animationPlayState = 'paused'; - if (el.style.willChange !== 'auto') el.style.willChange = 'auto'; - } else { - if (el.style.animationPlayState !== 'running') el.style.animationPlayState = 'running'; - if (el.classList.contains('global-spinning-image') || el.classList.contains('now-playing-background-image')) { - if (el.style.willChange !== 'transform') el.style.willChange = 'transform'; - } - } - }); +document.addEventListener("visibilitychange", () => { + const isHiddenDoc = document.hidden; + const images = document.querySelectorAll( + ".global-spinning-image, .now-playing-background-image", + ); + images.forEach((img) => { + const el = img as HTMLElement; + if (isHiddenDoc) { + // Pause animation but keep state + if (el.style.animationPlayState !== "paused") + el.style.animationPlayState = "paused"; + if (el.style.willChange !== "auto") el.style.willChange = "auto"; + } else { + if (el.style.animationPlayState !== "running") + el.style.animationPlayState = "running"; + if ( + el.classList.contains("global-spinning-image") || + el.classList.contains("now-playing-background-image") + ) { + if (el.style.willChange !== "transform") + el.style.willChange = "transform"; + } + } + }); }); // Apply initial performance mode class if (settings.performanceMode) { - document.body.classList.add('performance-mode'); + document.body.classList.add("performance-mode"); } // Initialize text glow CSS variables on load @@ -699,103 +768,107 @@ updateCoverArtBackground(1); // Add cleanup to unloads unloads.add(() => { - cleanUpDynamicArt(); + cleanUpDynamicArt(); - // Clean up auto-fade timeout - if (unhideButtonAutoFadeTimeout) { - window.clearTimeout(unhideButtonAutoFadeTimeout); - unhideButtonAutoFadeTimeout = null; - } + // Clean up auto-fade timeout + if (unhideButtonAutoFadeTimeout) { + window.clearTimeout(unhideButtonAutoFadeTimeout); + unhideButtonAutoFadeTimeout = null; + } - // Clean up our custom buttons - const hideButton = document.querySelector('.hide-ui-button'); - if (hideButton && hideButton.parentNode) { - hideButton.parentNode.removeChild(hideButton); - } - - const unhideButton = document.querySelector('.unhide-ui-button'); - if (unhideButton && unhideButton.parentNode) { - unhideButton.parentNode.removeChild(unhideButton); - } + // Clean up our custom buttons + const hideButton = document.querySelector(".hide-ui-button"); + if (hideButton && hideButton.parentNode) { + hideButton.parentNode.removeChild(hideButton); + } - // Clean up spin animations - const spinAnimationStyle = document.querySelector('#spinAnimation'); - if (spinAnimationStyle && spinAnimationStyle.parentNode) { - spinAnimationStyle.parentNode.removeChild(spinAnimationStyle); - } - - // Clean up global spinning backgrounds - cleanUpGlobalSpinningBackground(); + const unhideButton = document.querySelector(".unhide-ui-button"); + if (unhideButton && unhideButton.parentNode) { + unhideButton.parentNode.removeChild(unhideButton); + } + + // Clean up spin animations + const spinAnimationStyle = document.querySelector("#spinAnimation"); + if (spinAnimationStyle && spinAnimationStyle.parentNode) { + spinAnimationStyle.parentNode.removeChild(spinAnimationStyle); + } + + // Clean up global spinning backgrounds + cleanUpGlobalSpinningBackground(); }); - - - // Marker: Observers // Shared observer-based hooks and polling fallbacks const observeTrackChanges = (): void => { - let lastTrackId: string | null = null; - let checkCount = 0; - let currentInterval = 500; - const checkTrackChange = () => { - const currentTrackId = PlayState.playbackContext?.actualProductId; - if (currentTrackId && currentTrackId !== lastTrackId) { - lastTrackId = currentTrackId; - updateCoverArtBackground(); - checkCount = 0; - currentInterval = 250; - } - checkCount++; - if (checkCount > 10 && currentInterval < 1000) currentInterval = Math.min(currentInterval * 1.2, 1000); - }; - const intervalId = setInterval(() => checkTrackChange(), currentInterval); - unloads.add(() => clearInterval(intervalId)); - const currentTrackId = PlayState.playbackContext?.actualProductId; - if (currentTrackId) { - lastTrackId = currentTrackId; - setTimeout(() => updateCoverArtBackground(), 100); - } + let lastTrackId: string | null = null; + let checkCount = 0; + let currentInterval = 500; + const checkTrackChange = () => { + const currentTrackId = PlayState.playbackContext?.actualProductId; + if (currentTrackId && currentTrackId !== lastTrackId) { + lastTrackId = currentTrackId; + updateCoverArtBackground(); + checkCount = 0; + currentInterval = 250; + } + checkCount++; + if (checkCount > 10 && currentInterval < 1000) + currentInterval = Math.min(currentInterval * 1.2, 1000); + }; + const intervalId = setInterval(() => checkTrackChange(), currentInterval); + unloads.add(() => clearInterval(intervalId)); + const currentTrackId = PlayState.playbackContext?.actualProductId; + if (currentTrackId) { + lastTrackId = currentTrackId; + setTimeout(() => updateCoverArtBackground(), 100); + } }; - function setupHeaderObserver(): void { - const existing = document.querySelector('[data-test="header-container"]'); - if (existing && !document.querySelector('.hide-ui-button')) createHideUIButton(); - observe(unloads, '[data-test="header-container"]', () => { - if (!document.querySelector('.hide-ui-button')) createHideUIButton(); - }); + const existing = document.querySelector('[data-test="header-container"]'); + if (existing && !document.querySelector(".hide-ui-button")) + createHideUIButton(); + observe(unloads, '[data-test="header-container"]', () => { + if (!document.querySelector(".hide-ui-button")) createHideUIButton(); + }); } - function setupNowPlayingObserver(): void { - const existing = document.querySelector('[class*="_nowPlayingContainer"]'); - if (existing && !document.querySelector('.unhide-ui-button')) createUnhideUIButton(); - observe(unloads, '[class*="_nowPlayingContainer"]', () => { - if (!document.querySelector('.unhide-ui-button')) createUnhideUIButton(); - }); + const existing = document.querySelector('[class*="_nowPlayingContainer"]'); + if (existing && !document.querySelector(".unhide-ui-button")) + createUnhideUIButton(); + observe(unloads, '[class*="_nowPlayingContainer"]', () => { + if (!document.querySelector(".unhide-ui-button")) createUnhideUIButton(); + }); } function setupTrackTitleObserver(): void { - const trackTitleEl = document.querySelector('[data-test="now-playing-track-title"]') as HTMLElement | null; + const trackTitleEl = document.querySelector( + '[data-test="now-playing-track-title"]', + ) as HTMLElement | null; if (trackTitleEl) { if (settings.trackTitleGlow && settings.lyricsGlowEnabled) { - trackTitleEl.classList.remove('rl-title-glow-disabled'); + trackTitleEl.classList.remove("rl-title-glow-disabled"); } else { - trackTitleEl.classList.add('rl-title-glow-disabled'); + trackTitleEl.classList.add("rl-title-glow-disabled"); } } - observe(unloads, '[data-test="now-playing-track-title"]', (el) => { - if (!el) return; - if (settings.trackTitleGlow && settings.lyricsGlowEnabled) { - el.classList.remove('rl-title-glow-disabled'); - } else { - el.classList.add('rl-title-glow-disabled'); - } - }); + observe( + unloads, + '[data-test="now-playing-track-title"]', + (el) => { + if (!el) return; + if (settings.trackTitleGlow && settings.lyricsGlowEnabled) { + el.classList.remove("rl-title-glow-disabled"); + } else { + el.classList.add("rl-title-glow-disabled"); + } + }, + ); } // Initialize the button creation and observers (non-polling) setupHeaderObserver(); setupNowPlayingObserver(); setupTrackTitleObserver(); -observeTrackChanges(); \ No newline at end of file +observeTrackChanges(); diff --git a/plugins/radiant-lyrics-luna/src/lyrics-glow.css b/plugins/radiant-lyrics-luna/src/lyrics-glow.css index a552cf1..0752285 100644 --- a/plugins/radiant-lyrics-luna/src/lyrics-glow.css +++ b/plugins/radiant-lyrics-luna/src/lyrics-glow.css @@ -1,82 +1,103 @@ /* Font imports for lyrics */ @font-face { - font-family: "AbyssFont"; - font-weight: 400; - src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 400; + src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") + format("woff2"); } @font-face { - font-family: "AbyssFont"; - font-weight: 500; - src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 500; + src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") + format("woff2"); } @font-face { - font-family: "AbyssFont"; - font-weight: 600; - src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 600; + src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") + format("woff2"); } @font-face { - font-family: "AbyssFont"; - font-weight: 700; - src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2"); + font-family: "AbyssFont"; + font-weight: 700; + src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") + format("woff2"); } /* Enhanced lyrics styling with glow effects */ [class*="_lyricsText"] > div > span[data-current="true"] { - text-shadow: 0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #fff), 0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #fff) !important; - padding-left: 20px; - transition-duration: 0.7s; - font-size: 55px; - color: white !important; - font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - font-weight: 700; + text-shadow: + 0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #fff), + 0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #fff) !important; + padding-left: 20px; + transition-duration: 0.7s; + font-size: 55px; + color: white !important; + font-family: + "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + font-weight: 700; } [class*="_lyricsText"] > div > span { - text-shadow: 0 0 0px transparent, 0 0 0px transparent; - transition-duration: 0.25s; - color: rgba(128, 128, 128, 0.4); - font-size: 40px; - font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - font-weight: 700; + text-shadow: + 0 0 0px transparent, + 0 0 0px transparent; + transition-duration: 0.25s; + color: rgba(128, 128, 128, 0.4); + font-size: 40px; + font-family: + "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + font-weight: 700; } [class*="_lyricsText"] > div > span:hover { - text-shadow: 0 0 var(--rl-glow-inner, 2px) lightgray, 0 0 var(--rl-glow-outer, 20px) lightgray !important; - color: lightgray !important; - padding-left: 20px; - transition-duration: 0.7s; + text-shadow: + 0 0 var(--rl-glow-inner, 2px) lightgray, + 0 0 var(--rl-glow-outer, 20px) lightgray !important; + color: lightgray !important; + padding-left: 20px; + transition-duration: 0.7s; } /* Track title glow */ [data-test="now-playing-track-title"] { - /* Title text color/gradient is left to default app styling; only glow is customized. */ - text-shadow: 0 0 var(--rl-glow-inner, 1px) var(--cl-glow1, #fff), 0 0 var(--rl-glow-outer, 30px) #fff !important; - -webkit-background-clip: initial !important; - background-clip: initial !important; - -webkit-text-fill-color: initial !important; - color: inherit !important; + /* 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; + -webkit-background-clip: initial !important; + background-clip: initial !important; + -webkit-text-fill-color: initial !important; + color: inherit !important; } /* When track title glow setting is disabled, remove glow regardless of Colorama */ .rl-title-glow-disabled[data-test="now-playing-track-title"] { - text-shadow: none !important; + text-shadow: none !important; } /* Current line transitions */ [class*="_lyricsText"] > div > span { - transition: text-shadow 0.7s ease-in-out, color 0.7s ease-in-out, padding 0.7s ease-in-out !important; + transition: + text-shadow 0.7s ease-in-out, + color 0.7s ease-in-out, + padding 0.7s ease-in-out !important; } /* Lyrics container styling */ [class^="_lyricsContainer"] > div > div > span { - margin-bottom: 2rem; - opacity: 1; - font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - font-weight: 700; - font-size: 38px !important; + margin-bottom: 2rem; + opacity: 1; + font-family: + "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + font-weight: 700; + font-size: 38px !important; } /* Reset all lyrics styling when disabled */ @@ -85,13 +106,13 @@ .lyrics-glow-disabled [class*="_lyricsText"] > div > span:hover, .lyrics-glow-disabled [data-test="now-playing-track-title"], .lyrics-glow-disabled [class^="_lyricsContainer"] > div > div > span { - text-shadow: none !important; - padding-left: 0 !important; - transition: none !important; - font-size: inherit !important; - color: inherit !important; - font-family: inherit !important; - font-weight: inherit !important; - margin-bottom: inherit !important; - opacity: inherit !important; -} \ No newline at end of file + text-shadow: none !important; + padding-left: 0 !important; + transition: none !important; + font-size: inherit !important; + color: inherit !important; + font-family: inherit !important; + font-weight: inherit !important; + margin-bottom: inherit !important; + opacity: inherit !important; +} diff --git a/plugins/radiant-lyrics-luna/src/player-bar-hidden.css b/plugins/radiant-lyrics-luna/src/player-bar-hidden.css index e3e67d1..f92bcf5 100644 --- a/plugins/radiant-lyrics-luna/src/player-bar-hidden.css +++ b/plugins/radiant-lyrics-luna/src/player-bar-hidden.css @@ -1,15 +1,15 @@ /* Hide player bar when setting is disabled, but show on hover - only when UI is hidden */ .radiant-lyrics-ui-hidden [data-test="footer-player"] { - opacity: 0 !important; - transition: opacity 0.5s ease-in-out !important; + opacity: 0 !important; + transition: opacity 0.5s ease-in-out !important; } .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 */ .radiant-lyrics-ui-hidden body.rl-footer-hover [data-test="footer-player"], .radiant-lyrics-ui-hidden [data-test="footer-player"]:hover { - opacity: 1 !important; -} \ No newline at end of file + opacity: 1 !important; +} diff --git a/plugins/radiant-lyrics-luna/src/styles.css b/plugins/radiant-lyrics-luna/src/styles.css index 7300dad..24bf2d7 100644 --- a/plugins/radiant-lyrics-luna/src/styles.css +++ b/plugins/radiant-lyrics-luna/src/styles.css @@ -1,30 +1,32 @@ /* Only apply styles when UI is hidden */ .radiant-lyrics-ui-hidden [class*="tabItems"] { - opacity: 0 !important; - transition: opacity 0.4s ease-in-out; + opacity: 0 !important; + transition: opacity 0.4s ease-in-out; } /* Default state - visible */ [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) */ -.radiant-lyrics-ui-hidden [data-test="header-container"]:not(.rl-header-has-hide-btn) { - opacity: 0 !important; - transition: opacity 0.4s ease-in-out; +.radiant-lyrics-ui-hidden +[data-test="header-container"]:not(.rl-header-has-hide-btn) { + opacity: 0 !important; + transition: opacity 0.4s ease-in-out; } /* Keep header visible if it contains the Hide UI button, but hide its other children */ -.radiant-lyrics-ui-hidden [data-test="header-container"].rl-header-has-hide-btn > *:not(.hide-ui-button) { - opacity: 0 !important; - transition: opacity 0.4s ease-in-out; +.radiant-lyrics-ui-hidden [data-test="header-container"].rl-header-has-hide-btn + > *:not(.hide-ui-button) { + opacity: 0 !important; + transition: opacity 0.4s ease-in-out; } /* Default state for header */ [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 */ @@ -32,30 +34,30 @@ [data-test="footer-player"] [class*="_artistName"], [data-test="footer-player"] [class*="_trackInfo"], [data-test="footer-player"] [class*="_trackContainer"] { - margin-top: 0 !important; - transform: none !important; + margin-top: 0 !important; + transform: none !important; } /* Immediate hide class for unhide button with smooth transition (had issues with the fade out.. so I removed it) */ .hide-immediately { - opacity: 0 !important; - visibility: hidden !important; - pointer-events: none !important; + opacity: 0 !important; + visibility: hidden !important; + pointer-events: none !important; } [class^="_bar"] { - background-color: transparent; + background-color: transparent; } -.radiant-lyrics-ui-hidden [class^="_bar"]>*:not(.hide-ui-button) { - opacity: 0 !important; - pointer-events: none !important; - transition: opacity 0.4s ease-in-out; +.radiant-lyrics-ui-hidden [class^="_bar"] > *:not(.hide-ui-button) { + opacity: 0 !important; + pointer-events: none !important; + transition: opacity 0.4s ease-in-out; } /* Default state for bar elements */ -[class^="_bar"]>* { - transition: opacity 0.4s ease-in-out; +[class^="_bar"] > * { + transition: opacity 0.4s ease-in-out; } /* 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 header input, .radiant-lyrics-ui-hidden nav input { - pointer-events: none !important; - cursor: default !important; - user-select: none !important; + pointer-events: none !important; + cursor: default !important; + user-select: none !important; } /* Hide bottom left controls completely - no hover functionality */ /* Exclude heart button in player bar and make sure hidden buttons can't be clicked */ -.radiant-lyrics-ui-hidden [data-test="add-to-playlist"]:not([data-test="footer-player"] *), -.radiant-lyrics-ui-hidden [data-test="remove-from-playlist"]:not([data-test="footer-player"] *), -.radiant-lyrics-ui-hidden [data-test="like-toggle"]:not([data-test="footer-player"] *), -.radiant-lyrics-ui-hidden [data-test="dislike-toggle"]:not([data-test="footer-player"] *), -.radiant-lyrics-ui-hidden [data-test="favorite-toggle"]:not([data-test="footer-player"] *), -.radiant-lyrics-ui-hidden [data-test="heart-button"]:not([data-test="footer-player"] *), -.radiant-lyrics-ui-hidden [data-test="playlist-add"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden +[data-test="add-to-playlist"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + [data-test="remove-from-playlist"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + [data-test="like-toggle"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + [data-test="dislike-toggle"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + [data-test="favorite-toggle"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + [data-test="heart-button"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + [data-test="playlist-add"]:not([data-test="footer-player"] *), .radiant-lyrics-ui-hidden [class*="_trackActions"], .radiant-lyrics-ui-hidden [class*="_bottomLeftControls"], .radiant-lyrics-ui-hidden [class*="_actionButtons"], -.radiant-lyrics-ui-hidden [class*="_favoriteButton"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + [class*="_favoriteButton"]:not([data-test="footer-player"] *), .radiant-lyrics-ui-hidden [class*="_addToPlaylist"], .radiant-lyrics-ui-hidden [class*="_lowerLeft"], .radiant-lyrics-ui-hidden [class*="_bottomActions"], .radiant-lyrics-ui-hidden [class*="_mediaControls"] > div:first-child, -.radiant-lyrics-ui-hidden button[title*="Add to"]:not([data-test="footer-player"] *), -.radiant-lyrics-ui-hidden button[title*="Remove from"]:not([data-test="footer-player"] *), -.radiant-lyrics-ui-hidden button[title*="Like"]:not([data-test="footer-player"] *), -.radiant-lyrics-ui-hidden button[title*="Favorite"]:not([data-test="footer-player"] *), -.radiant-lyrics-ui-hidden button[title*="Heart"]:not([data-test="footer-player"] *), -.radiant-lyrics-ui-hidden button[aria-label*="Add to"]:not([data-test="footer-player"] *), -.radiant-lyrics-ui-hidden button[aria-label*="Remove from"]:not([data-test="footer-player"] *), -.radiant-lyrics-ui-hidden button[aria-label*="Like"]:not([data-test="footer-player"] *), -.radiant-lyrics-ui-hidden button[aria-label*="Favorite"]:not([data-test="footer-player"] *), -.radiant-lyrics-ui-hidden button[aria-label*="Heart"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + button[title*="Add to"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + button[title*="Remove from"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + button[title*="Like"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + button[title*="Favorite"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + button[title*="Heart"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + button[aria-label*="Add to"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + button[aria-label*="Remove from"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + button[aria-label*="Like"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + button[aria-label*="Favorite"]:not([data-test="footer-player"] *), +.radiant-lyrics-ui-hidden + button[aria-label*="Heart"]:not([data-test="footer-player"] *), /* Target buttons in bottom left area specifically - (idk if this is needed.. but it's here) */ -.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] button[class*="_button"]:not(.unhide-ui-button), -.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] [class*="_iconButton"]:not(.unhide-ui-button), + .radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] + button[class*="_button"]:not(.unhide-ui-button), +.radiant-lyrics-ui-hidden + [class*="_nowPlayingContainer"] + [class*="_iconButton"]:not(.unhide-ui-button), /* Additional catch-all for bottom left area buttons - (idk if this is needed.. but it's here) */ -.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] > div > div:first-child button:not(.unhide-ui-button), -.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] > div:first-child button:not(.unhide-ui-button) { - opacity: 0 !important; - pointer-events: none !important; - transition: opacity 0.5s ease-in-out !important; + .radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] + > div + > div:first-child + button:not(.unhide-ui-button), +.radiant-lyrics-ui-hidden + [class*="_nowPlayingContainer"] + > div:first-child + button:not(.unhide-ui-button) { + opacity: 0 !important; + pointer-events: none !important; + transition: opacity 0.5s ease-in-out !important; } /* No hover functionality in Hide UI Mode - buttons stay hidden.. yea thats right, you heard me */ /* Default state for control buttons */ [data-test="add-to-playlist"], -[data-test="remove-from-playlist"], +[data-test="remove-from-playlist"], [data-test="like-toggle"], [data-test="dislike-toggle"], [data-test="favorite-toggle"], @@ -146,33 +175,38 @@ button[aria-label*="Favorite"], button[aria-label*="Heart"], [class*="_nowPlayingContainer"] button[class*="_button"]:not(.unhide-ui-button), [class*="_nowPlayingContainer"] [class*="_iconButton"]:not(.unhide-ui-button), -[class*="_nowPlayingContainer"] > div > div:first-child button:not(.unhide-ui-button), -[class*="_nowPlayingContainer"] > div:first-child button:not(.unhide-ui-button) { - transition: opacity 0.5s ease-in-out; +[class*="_nowPlayingContainer"] + > div + > div:first-child + button:not(.unhide-ui-button), +[class*="_nowPlayingContainer"] + > div:first-child + button:not(.unhide-ui-button) { + transition: opacity 0.5s ease-in-out; } /* Smooth cover art movement when UI is hidden */ [class*="_albumImage"], [class*="_coverArt"], 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*="_coverArt"], .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) */ [class*="_infoWrapper"], [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*="_textContainer"] { - transform: translateX(40px) !important; + transform: translateX(40px) !important; } /* Move parent containers instead of lyrics container directly to preserve gradient fade */ @@ -183,7 +217,7 @@ figure[class*="_albumImage"] { [class*="_sidePanel"], [class*="_lyricsSection"], [class*="_lyricsWrapper"] { - transition: transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out; } .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*="_lyricsSection"], .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 { - 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*/ .unhide-ui-button.auto-faded { - background-color: transparent !important; - border-color: transparent !important; - box-shadow: none !important; - backdrop-filter: none !important; - -webkit-backdrop-filter: none !important; - color: rgba(255, 255, 255, 0.8) !important; - transition: background-color 0.8s ease-in-out, border-color 0.8s ease-in-out, box-shadow 0.8s ease-in-out, backdrop-filter 0.8s ease-in-out, color 0.8s ease-in-out; + background-color: transparent !important; + border-color: transparent !important; + box-shadow: none !important; + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + color: rgba(255, 255, 255, 0.8) !important; + transition: + background-color 0.8s ease-in-out, + border-color 0.8s ease-in-out, + box-shadow 0.8s ease-in-out, + backdrop-filter 0.8s ease-in-out, + color 0.8s ease-in-out; } /* Restore button styling on hover */ .unhide-ui-button.auto-faded:hover { - background-color: rgba(255, 255, 255, 0.2) !important; - border-color: rgba(255, 255, 255, 0.3) !important; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important; - backdrop-filter: blur(10px) !important; - -webkit-backdrop-filter: blur(10px) !important; - color: white !important; - transition: background-color 0.3s ease-in-out, border-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out, backdrop-filter 0.3s ease-in-out, color 0.3s ease-in-out; -} \ No newline at end of file + background-color: rgba(255, 255, 255, 0.2) !important; + border-color: rgba(255, 255, 255, 0.3) !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important; + backdrop-filter: blur(10px) !important; + -webkit-backdrop-filter: blur(10px) !important; + color: white !important; + transition: + background-color 0.3s ease-in-out, + border-color 0.3s ease-in-out, + box-shadow 0.3s ease-in-out, + backdrop-filter 0.3s ease-in-out, + color 0.3s ease-in-out; +} From 0d9b378e430db26ae59c1b94f2d1674298a4832f Mon Sep 17 00:00:00 2001 From: meowarex Date: Tue, 9 Sep 2025 18:31:35 +1000 Subject: [PATCH 02/10] BIOME Refactor --- plugins/colorama-lyrics-luna/src/Settings.tsx | 89 ++++++++++++------- plugins/copy-lyrics-luna/src/index.ts | 28 +++--- plugins/element-hider-luna/src/index.ts | 48 +++++----- plugins/oled-theme-luna/src/index.ts | 20 ++--- plugins/radiant-lyrics-luna/src/Settings.tsx | 37 ++++---- .../src/cover-everywhere.css | 22 +++++ .../radiant-lyrics-luna/src/lyrics-glow.css | 21 +++++ 7 files changed, 176 insertions(+), 89 deletions(-) diff --git a/plugins/colorama-lyrics-luna/src/Settings.tsx b/plugins/colorama-lyrics-luna/src/Settings.tsx index c46521d..f70a1de 100644 --- a/plugins/colorama-lyrics-luna/src/Settings.tsx +++ b/plugins/colorama-lyrics-luna/src/Settings.tsx @@ -2,6 +2,12 @@ import { ReactiveStore } from "@luna/core"; import { LunaSettings, LunaSwitchSetting } from "@luna/ui"; import React from "react"; +declare global { + interface Window { + applyColoramaLyrics?: () => void; + } +} + export type ColoramaMode = | "single" | "gradient-experimental" @@ -24,7 +30,7 @@ export const settings = await ReactiveStore.getPluginStorage("ColoramaLyrics", { }); export const Settings = () => { - const [enabled, setEnabled] = React.useState(settings.enabled); + // const [enabled, setEnabled] = React.useState(settings.enabled); const [mode, setMode] = React.useState(settings.mode); const [singleColor, setSingleColor] = React.useState(settings.singleColor); const [singleAlpha, setSingleAlpha] = React.useState( @@ -54,7 +60,9 @@ export const Settings = () => { const [activeEndpoint, setActiveEndpoint] = React.useState< "single" | "start" | "end" >("single"); - const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType; + const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType< + Record + >; // Helper for HEX normalization const normalizeToRGB = ( @@ -125,14 +133,17 @@ export const Settings = () => { if (!hexColorRegex.test(trimmed)) return; if (mode === "single") { const next = normalizeToRGB(trimmed); - setSingleColor((settings.singleColor = next)); + settings.singleColor = next; + setSingleColor(next); if (updateInput) setCustomInput(next); } else if (mode === "gradient-experimental") { const norm = normalizeToRGB(trimmed); if (activeEndpoint === "end") { - setGradientEnd((settings.gradientEnd = norm)); + settings.gradientEnd = norm; + setGradientEnd(norm); } else { - setGradientStart((settings.gradientStart = norm)); + settings.gradientStart = norm; + setGradientStart(norm); } if (updateInput) setCustomInput(norm); } @@ -152,16 +163,16 @@ export const Settings = () => { } }; - const removeCustomColor = (color: string) => { - const updated = customColors.filter((c) => c !== color); - setCustomColors(updated); - settings.customColors = updated; - }; + // const removeCustomColor = (color: string) => { + // const updated = customColors.filter((c) => c !== color); + // setCustomColors(updated); + // settings.customColors = updated; + // }; const allColors = [...colorPresets, ...customColors]; const requestApply = () => { - (window as any).applyColoramaLyrics?.(); + window.applyColoramaLyrics?.(); }; return ( @@ -185,7 +196,8 @@ export const Settings = () => { value={mode} onChange={(e) => { const next = e.target.value as ColoramaMode; - setMode((settings.mode = next)); + settings.mode = next; + setMode(next); requestApply(); }} style={{ @@ -250,6 +262,7 @@ export const Settings = () => { }} >
@@ -586,7 +610,8 @@ export const Settings = () => { value={singleAlpha} onChange={(e) => { const value = Number(e.target.value); - setSingleAlpha((settings.singleAlpha = value)); + settings.singleAlpha = value; + setSingleAlpha(value); requestApply(); }} style={{ width: "100%" }} @@ -628,9 +653,8 @@ export const Settings = () => { value={gradientStartAlpha} onChange={(e) => { const value = Number(e.target.value); - setGradientStartAlpha( - (settings.gradientStartAlpha = value), - ); + settings.gradientStartAlpha = value; + setGradientStartAlpha(value); requestApply(); }} style={{ width: "100%" }} @@ -668,7 +692,8 @@ export const Settings = () => { value={gradientEndAlpha} onChange={(e) => { const value = Number(e.target.value); - setGradientEndAlpha((settings.gradientEndAlpha = value)); + settings.gradientEndAlpha = value; + setGradientEndAlpha(value); requestApply(); }} style={{ width: "100%" }} @@ -702,7 +727,8 @@ export const Settings = () => { value={gradientAngle} onChange={(e) => { const value = Number(e.target.value); - setGradientAngle((settings.gradientAngle = value)); + settings.gradientAngle = value; + setGradientAngle(value); requestApply(); }} style={{ width: "100%" }} @@ -736,7 +762,8 @@ export const Settings = () => { value={gradientAngle} onChange={(e) => { const value = Number(e.target.value); - setGradientAngle((settings.gradientAngle = value)); + settings.gradientAngle = value; + setGradientAngle(value); requestApply(); }} style={{ width: "100%" }} @@ -756,6 +783,7 @@ export const Settings = () => { cursor: "pointer", fontSize: 12, }} + type="button" > Done @@ -767,7 +795,8 @@ export const Settings = () => { desc="Apply color/gradient only to the currently active lyric line" checked={excludeInactive} onChange={(_: unknown, checked: boolean) => { - setExcludeInactive((settings.excludeInactive = checked)); + settings.excludeInactive = checked; + setExcludeInactive(checked); requestApply(); }} /> diff --git a/plugins/copy-lyrics-luna/src/index.ts b/plugins/copy-lyrics-luna/src/index.ts index f03d40e..04c7e52 100644 --- a/plugins/copy-lyrics-luna/src/index.ts +++ b/plugins/copy-lyrics-luna/src/index.ts @@ -1,4 +1,4 @@ -import { LunaUnload, Tracer } from "@luna/core"; +import { type LunaUnload, Tracer } from "@luna/core"; import { StyleTag } from "@luna/lib"; // Import CSS directly using Luna's file:// syntax - Took me a while to figure out <3 @@ -9,8 +9,8 @@ export const { trace } = Tracer("[Copy Lyrics]"); // clean up resources export const unloads = new Set(); -// StyleTag for lyrics selection styling -const lyricsStyleTag = new StyleTag("Copy-Lyrics", unloads, unlockSelection); +// Style injection via side effect +new StyleTag("Copy-Lyrics", unloads, unlockSelection); function SetClipboard(text: string): void { const textarea = document.createElement("textarea"); @@ -31,17 +31,17 @@ function SetClipboard(text: string): void { let isSelecting = false; -const onMouseDown = function (): void { +const onMouseDown = (): void => { isSelecting = true; }; -const onMouseUp = function (event: MouseEvent): void { +const onMouseUp = (): void => { if (isSelecting) { const selection = window.getSelection(); - if (selection && selection.toString().length > 0) { + if (selection?.toString().length > 0) { const selectedSpans: HTMLSpanElement[] = []; const range = selection.getRangeAt(0); - let container = range.commonAncestorContainer; + const container = range.commonAncestorContainer; // If the container is NOT an element and a document, adjust it. if ( @@ -50,8 +50,8 @@ const onMouseUp = function (event: MouseEvent): void { ) { // 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(); + if (parentElement?.hasAttribute("data-current")) { + const text_ = selection.toString().trim(); SetClipboard(text_); trace.msg.log("Copied to clipboard!"); return; @@ -60,7 +60,7 @@ const onMouseUp = function (event: MouseEvent): void { // Get all the spans inside the container. const spans = (container as Element).getElementsByTagName("span"); - for (let span of spans) { + for (const span of spans) { if (selection.containsNode(span, true)) { selectedSpans.push(span as HTMLSpanElement); } @@ -95,7 +95,7 @@ const onMouseUp = function (event: MouseEvent): void { } }; -const onClickHooked = function (event: MouseEvent): boolean | void { +const onClickHooked = (event: MouseEvent): boolean | undefined => { if (!isSelecting) return; const target = event.target as HTMLElement; @@ -109,15 +109,19 @@ const onClickHooked = function (event: MouseEvent): boolean | void { event.stopImmediatePropagation(); return false; } + return undefined; }; // Add event listener with capture phase to intercept events before they reach other handlers + document.addEventListener("click", onClickHooked, true); + document.addEventListener("mousedown", onMouseDown); + document.addEventListener("mouseup", onMouseUp); // Add cleanup to unloads -unloads.add(() => { +unloads.add((): void => { // Remove event listeners document.removeEventListener("click", onClickHooked, true); document.removeEventListener("mousedown", onMouseDown); diff --git a/plugins/element-hider-luna/src/index.ts b/plugins/element-hider-luna/src/index.ts index 98252be..d20bff4 100644 --- a/plugins/element-hider-luna/src/index.ts +++ b/plugins/element-hider-luna/src/index.ts @@ -1,5 +1,5 @@ -import { LunaUnload, Tracer } from "@luna/core"; -import { StyleTag, ContextMenu } from "@luna/lib"; +import { type LunaUnload, Tracer } from "@luna/core"; +import { StyleTag } from "@luna/lib"; import { settings, Settings } from "./Settings"; // Import CSS directly using Luna's file:// syntax @@ -13,8 +13,8 @@ export { Settings }; // Clean up resources export const unloads = new Set(); -// StyleTag for element hider -const styleTag = new StyleTag("Element-Hider", unloads, styles); +// StyleTag for element hider (side-effect) +new StyleTag("Element-Hider", unloads, styles); // State management let targetElement: HTMLElement | null = null; @@ -144,19 +144,18 @@ function saveHiddenElement(element: HTMLElement): void { } } -// Remove hidden element from persistent storage (for unhiding) -function removeSavedElement(element: HTMLElement): void { - const selector = generateElementSelector(element); - const index = settings.hiddenElements.findIndex( - (stored) => stored.selector === selector, - ); - - if (index !== -1) { - settings.hiddenElements.splice(index, 1); - trace.log(`Permanently removed: ${selector}`); - trace.log(`Remaining stored: ${settings.hiddenElements.length}`); - } -} +// Remove hidden element from persistent storage (for unhiding) - currently unused +// function removeSavedElement(element: HTMLElement): void { +// const selector = generateElementSelector(element); +// const index = settings.hiddenElements.findIndex( +// (stored) => stored.selector === selector, +// ); +// if (index !== -1) { +// settings.hiddenElements.splice(index, 1); +// trace.log(`Permanently removed: ${selector}`); +// trace.log(`Remaining stored: ${settings.hiddenElements.length}`); +// } +// } // Check if an element matches any stored selector (EXACT match only) function matchesStoredSelector(element: HTMLElement): boolean { @@ -324,8 +323,15 @@ function setupElementObserver(): void { } // Global functions -(window as any).showAllElementsFromSettings = unhideAllElements; -(window as any).debugElementHider = () => { +declare global { + interface Window { + showAllElementsFromSettings?: () => void; + debugElementHider?: () => void; + } +} + +window.showAllElementsFromSettings = unhideAllElements; +window.debugElementHider = () => { trace.log(`=== Element Hider Debug Info ===`); trace.log(`Stored elements: ${settings.hiddenElements.length}`); trace.log(`Currently hidden elements: ${hiddenElementsArray.length}`); @@ -662,8 +668,8 @@ unloads.add(() => { removeHighlight(); // Clean up global functions - (window as any).showAllElementsFromSettings = undefined; - (window as any).debugElementHider = undefined; + window.showAllElementsFromSettings = undefined; + window.debugElementHider = undefined; trace.log("Plugin unloaded"); }); diff --git a/plugins/oled-theme-luna/src/index.ts b/plugins/oled-theme-luna/src/index.ts index 9c47707..fb8cdfa 100644 --- a/plugins/oled-theme-luna/src/index.ts +++ b/plugins/oled-theme-luna/src/index.ts @@ -1,11 +1,5 @@ -import { LunaUnload, Tracer } from "@luna/core"; -import { - StyleTag, - observePromise, - PlayState, - Quality, - type MediaItem, -} from "@luna/lib"; +import { type LunaUnload, Tracer } from "@luna/core"; +import { StyleTag, observePromise, PlayState } from "@luna/lib"; import { settings, Settings } from "./Settings"; // Import CSS files directly using Luna's file:// syntax - Took me a while to figure out <3 @@ -109,7 +103,7 @@ const setupQualityMonitoring = (): void => { }; // Function to apply theme styles based on current settings -const applyThemeStyles = function (): void { +const applyThemeStyles = (): void => { // Choose the appropriate CSS file based on settings let selectedStyle: string; @@ -141,7 +135,13 @@ const applyThemeStyles = function (): void { }; // 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 applyThemeStyles(); diff --git a/plugins/radiant-lyrics-luna/src/Settings.tsx b/plugins/radiant-lyrics-luna/src/Settings.tsx index ef17605..d5ef020 100644 --- a/plugins/radiant-lyrics-luna/src/Settings.tsx +++ b/plugins/radiant-lyrics-luna/src/Settings.tsx @@ -54,13 +54,18 @@ export const Settings = () => { settings.trackTitleGlow, ); + // Use a permissive wrapper to align with current usage props + const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType< + Record + >; + return ( - { + onChange={(_event: unknown, checked: boolean) => { setLyricsGlowEnabled((settings.lyricsGlowEnabled = checked)); // Update styles immediately when setting changes if ((window as any).updateRadiantLyricsStyles) { @@ -68,30 +73,30 @@ export const Settings = () => { } }} /> - { + onChange={(_event: unknown, checked: boolean) => { setTrackTitleGlow((settings.trackTitleGlow = checked)); if ((window as any).updateRadiantLyricsStyles) { (window as any).updateRadiantLyricsStyles(); } }} /> - { + onChange={(_event: unknown, checked: boolean) => { setHideUIEnabled((settings.hideUIEnabled = checked)); }} /> - { + onChange={(_event: unknown, checked: boolean) => { console.log("Player Bar Visibility:", checked ? "visible" : "hidden"); setPlayerBarVisible((settings.playerBarVisible = checked)); // Update styles immediately when setting changes @@ -100,11 +105,11 @@ export const Settings = () => { } }} /> - { + onChange={(_event: unknown, checked: boolean) => { console.log( "Spinning Cover Everywhere:", checked ? "enabled" : "disabled", @@ -118,11 +123,11 @@ export const Settings = () => { } }} /> - { + onChange={(_event: unknown, checked: boolean) => { console.log("Performance Mode:", checked ? "enabled" : "disabled"); setPerformanceMode((settings.performanceMode = checked)); // Update background animations immediately when setting changes @@ -134,11 +139,11 @@ export const Settings = () => { } }} /> - { + onChange={(_event: unknown, checked: boolean) => { console.log( "Background Cover Spin:", checked ? "enabled" : "disabled", @@ -253,11 +258,11 @@ export const Settings = () => { } }} /> - { + onChange={(_event: unknown, checked: boolean) => { console.log( "Settings Affect Now Playing:", checked ? "enabled" : "disabled", diff --git a/plugins/radiant-lyrics-luna/src/cover-everywhere.css b/plugins/radiant-lyrics-luna/src/cover-everywhere.css index dad629a..6bdddaf 100644 --- a/plugins/radiant-lyrics-luna/src/cover-everywhere.css +++ b/plugins/radiant-lyrics-luna/src/cover-everywhere.css @@ -47,16 +47,21 @@ .global-spinning-image.performance-mode-static { /* Keep animation enabled in performance mode */ /* Lighter blur for performance */ + /* biome-ignore lint: Required to override app styles in performance mode */ filter: blur(20px) brightness(0.4) contrast(1.2) saturate(1) !important; /* Smaller size for performance */ + /* 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 { /* Keep animation enabled in performance mode */ /* Optimized size and effects for performance */ + /* biome-ignore lint: Required to override inline sizes in performance mode */ width: 80vw !important; + /* biome-ignore lint: Required to override inline sizes in performance mode */ height: 80vh !important; } @@ -89,8 +94,11 @@ @media (prefers-reduced-motion: reduce) { .global-spinning-image, .now-playing-background-image { + /* biome-ignore lint: Accessibility override needs priority */ animation: none !important; + /* biome-ignore lint: Accessibility override needs priority */ transform: translate(-50%, -50%) !important; + /* biome-ignore lint: Accessibility override needs priority */ will-change: auto !important; } } @@ -99,6 +107,7 @@ .performance-mode .global-spinning-image, .performance-mode .now-playing-background-image { /* Keep animations but optimize filter effects */ + /* biome-ignore lint: Intentional override of runtime styles */ filter: blur(10px) brightness(0.4) contrast(1.1) !important; } @@ -121,6 +130,7 @@ main, [class^="_feedSidebarItemDiv"], [class^="_cellContainer"], [class^="_cellTextContainer"] { + /* biome-ignore lint: Ensure background is fully cleared under theme CSS */ background: unset !important; } @@ -129,8 +139,11 @@ main, [data-test="main-layout-sidebar-wrapper"], [class^="_bar"], [class^="_sidebarItem"]:hover { + /* biome-ignore lint: Must beat app inline styles for translucency */ background-color: rgba(0, 0, 0, 0.3) !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; } @@ -139,20 +152,27 @@ main, .performance-mode [data-test="main-layout-sidebar-wrapper"], .performance-mode [class^="_bar"], .performance-mode [class^="_sidebarItem"]:hover { + /* biome-ignore lint: Performance mode style requires priority */ 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 */ [data-test="feed-sidebar"] { + /* biome-ignore lint: Ensure readability over media */ background-color: rgba(0, 0, 0, 0.5) !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 [data-test="feed-sidebar"] { + /* biome-ignore lint: Performance mode style requires priority */ backdrop-filter: blur(5px) !important; + /* biome-ignore lint: Performance mode style requires priority */ -webkit-backdrop-filter: blur(5px) !important; } @@ -162,10 +182,12 @@ main, [class*="_cellContainer"], [data-test="feed-interval"], [data-test="feed-item"] { + /* biome-ignore lint: Match theme transparency */ background-color: transparent !important; } /* Remove bottom gradient */ [class^="_bottomGradient"] { + /* biome-ignore lint: Explicitly remove conflicting gradient */ display: none !important; } diff --git a/plugins/radiant-lyrics-luna/src/lyrics-glow.css b/plugins/radiant-lyrics-luna/src/lyrics-glow.css index 0752285..b455ead 100644 --- a/plugins/radiant-lyrics-luna/src/lyrics-glow.css +++ b/plugins/radiant-lyrics-luna/src/lyrics-glow.css @@ -31,10 +31,12 @@ [class*="_lyricsText"] > div > span[data-current="true"] { text-shadow: 0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #fff), + /* biome-ignore lint: Required to override app glow strength */ 0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #fff) !important; padding-left: 20px; transition-duration: 0.7s; 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", @@ -58,7 +60,9 @@ [class*="_lyricsText"] > div > span:hover { text-shadow: 0 0 var(--rl-glow-inner, 2px) lightgray, + /* biome-ignore lint: Hover glow should override defaults */ 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; @@ -69,15 +73,21 @@ /* 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), + /* biome-ignore lint: Title glow needs priority */ 0 0 var(--rl-glow-outer, 30px) #fff !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 */ .rl-title-glow-disabled[data-test="now-playing-track-title"] { + /* biome-ignore lint: Full reset required */ text-shadow: none !important; } @@ -86,6 +96,7 @@ 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; } @@ -97,6 +108,7 @@ "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; font-weight: 700; + /* biome-ignore lint: Typography override for readability */ font-size: 38px !important; } @@ -106,13 +118,22 @@ .lyrics-glow-disabled [class*="_lyricsText"] > div > span:hover, .lyrics-glow-disabled [data-test="now-playing-track-title"], .lyrics-glow-disabled [class^="_lyricsContainer"] > div > div > span { + /* biome-ignore lint: Hard reset when disabled */ text-shadow: none !important; + /* biome-ignore lint: Hard reset when disabled */ padding-left: 0 !important; + /* biome-ignore lint: Hard reset when disabled */ transition: none !important; + /* biome-ignore lint: Hard reset when disabled */ font-size: 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; } From 11d08b64035c9b8c22974d96781d61adc766af88 Mon Sep 17 00:00:00 2001 From: meowarex Date: Tue, 9 Sep 2025 19:01:12 +1000 Subject: [PATCH 03/10] Flow Refactor --- plugins/colorama-lyrics-luna/src/Settings.tsx | 52 +++++++++++-------- plugins/copy-lyrics-luna/src/index.ts | 40 +++++++++----- plugins/element-hider-luna/src/index.ts | 3 +- plugins/oled-theme-luna/src/index.ts | 28 ++++++++-- plugins/oled-theme-luna/src/light-theme.css | 2 +- plugins/oled-theme-luna/src/oled-friendly.css | 2 +- plugins/radiant-lyrics-luna/src/Settings.tsx | 16 +++--- plugins/radiant-lyrics-luna/src/index.ts | 46 ++++++++++------ .../radiant-lyrics-luna/src/lyrics-glow.css | 9 ++-- 9 files changed, 124 insertions(+), 74 deletions(-) diff --git a/plugins/colorama-lyrics-luna/src/Settings.tsx b/plugins/colorama-lyrics-luna/src/Settings.tsx index f70a1de..603f6d0 100644 --- a/plugins/colorama-lyrics-luna/src/Settings.tsx +++ b/plugins/colorama-lyrics-luna/src/Settings.tsx @@ -8,6 +8,12 @@ declare global { } } +// Define a typed onChange signature for the switch +type SwitchChangeHandler = ( + event: React.ChangeEvent | null, + checked: boolean, +) => void; + export type ColoramaMode = | "single" | "gradient-experimental" @@ -60,9 +66,12 @@ export const Settings = () => { const [activeEndpoint, setActiveEndpoint] = React.useState< "single" | "start" | "end" >("single"); - const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType< - Record - >; + const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType<{ + title: string; + desc?: string; + checked: boolean; + onChange: SwitchChangeHandler; + }>; // Helper for HEX normalization const normalizeToRGB = ( @@ -133,19 +142,19 @@ export const Settings = () => { if (!hexColorRegex.test(trimmed)) return; if (mode === "single") { const next = normalizeToRGB(trimmed); - settings.singleColor = next; setSingleColor(next); + settings.singleColor = next; if (updateInput) setCustomInput(next); } else if (mode === "gradient-experimental") { - const norm = normalizeToRGB(trimmed); + const next = normalizeToRGB(trimmed); if (activeEndpoint === "end") { - settings.gradientEnd = norm; - setGradientEnd(norm); + setGradientEnd(next); + settings.gradientEnd = next; } else { - settings.gradientStart = norm; - setGradientStart(norm); + setGradientStart(next); + settings.gradientStart = next; } - if (updateInput) setCustomInput(norm); + if (updateInput) setCustomInput(next); } requestApply(); }; @@ -500,21 +509,20 @@ export const Settings = () => { key={color} type="button" onClick={() => { + const next = normalizeToRGB(color); if (mode === "single") { - const next = normalizeToRGB(color); - settings.singleColor = next; setSingleColor(next); + settings.singleColor = next; } else if (mode === "gradient-experimental") { - const next = normalizeToRGB(color); if (activeEndpoint === "end") { - settings.gradientEnd = next; setGradientEnd(next); + settings.gradientEnd = next; } else { - settings.gradientStart = next; setGradientStart(next); + settings.gradientStart = next; } } - setCustomInput(normalizeToRGB(color)); + setCustomInput(next); requestApply(); }} style={{ @@ -610,8 +618,8 @@ export const Settings = () => { value={singleAlpha} onChange={(e) => { const value = Number(e.target.value); - settings.singleAlpha = value; setSingleAlpha(value); + settings.singleAlpha = value; requestApply(); }} style={{ width: "100%" }} @@ -653,8 +661,8 @@ export const Settings = () => { value={gradientStartAlpha} onChange={(e) => { const value = Number(e.target.value); - settings.gradientStartAlpha = value; setGradientStartAlpha(value); + settings.gradientStartAlpha = value; requestApply(); }} style={{ width: "100%" }} @@ -692,8 +700,8 @@ export const Settings = () => { value={gradientEndAlpha} onChange={(e) => { const value = Number(e.target.value); - settings.gradientEndAlpha = value; setGradientEndAlpha(value); + settings.gradientEndAlpha = value; requestApply(); }} style={{ width: "100%" }} @@ -727,8 +735,8 @@ export const Settings = () => { value={gradientAngle} onChange={(e) => { const value = Number(e.target.value); - settings.gradientAngle = value; setGradientAngle(value); + settings.gradientAngle = value; requestApply(); }} style={{ width: "100%" }} @@ -762,8 +770,8 @@ export const Settings = () => { value={gradientAngle} onChange={(e) => { const value = Number(e.target.value); - settings.gradientAngle = value; setGradientAngle(value); + settings.gradientAngle = value; requestApply(); }} style={{ width: "100%" }} @@ -794,7 +802,7 @@ export const Settings = () => { title="Exclude Inactive" desc="Apply color/gradient only to the currently active lyric line" checked={excludeInactive} - onChange={(_: unknown, checked: boolean) => { + onChange={(_event: React.ChangeEvent | null, checked: boolean) => { settings.excludeInactive = checked; setExcludeInactive(checked); requestApply(); diff --git a/plugins/copy-lyrics-luna/src/index.ts b/plugins/copy-lyrics-luna/src/index.ts index 04c7e52..cc4cbd5 100644 --- a/plugins/copy-lyrics-luna/src/index.ts +++ b/plugins/copy-lyrics-luna/src/index.ts @@ -41,25 +41,39 @@ const onMouseUp = (): void => { if (selection?.toString().length > 0) { const selectedSpans: HTMLSpanElement[] = []; const range = selection.getRangeAt(0); - const container = range.commonAncestorContainer; + let container: Node | null = range.commonAncestorContainer; - // If the container is NOT an element and a document, adjust it. + // Normalize container: if it's a text node, use its parent element/node + if (container && container.nodeType === Node.TEXT_NODE) { + container = (container.parentElement ?? container.parentNode) as Node | null; + } + + // If parent has data-current, treat as single-line copy case if ( - container.nodeType !== Node.ELEMENT_NODE && - container.nodeType !== Node.DOCUMENT_NODE + container && + container.nodeType === Node.ELEMENT_NODE && + (container as Element).hasAttribute("data-current") ) { - // Get the parent element if it's a text node - const parentElement = container.parentElement; - if (parentElement?.hasAttribute("data-current")) { - const text_ = selection.toString().trim(); - SetClipboard(text_); - trace.msg.log("Copied to clipboard!"); - return; - } + const text_ = selection.toString().trim(); + SetClipboard(text_); + trace.msg.log("Copied to clipboard!"); + return; + } + + // Ensure we have an Element or Document before querying + if ( + !container || + (container.nodeType !== Node.ELEMENT_NODE && + container.nodeType !== Node.DOCUMENT_NODE) + ) { + isSelecting = false; + return; } // Get all the spans inside the container. - const spans = (container as Element).getElementsByTagName("span"); + const spans = (container as Element | Document).getElementsByTagName( + "span", + ); for (const span of spans) { if (selection.containsNode(span, true)) { selectedSpans.push(span as HTMLSpanElement); diff --git a/plugins/element-hider-luna/src/index.ts b/plugins/element-hider-luna/src/index.ts index d20bff4..e021570 100644 --- a/plugins/element-hider-luna/src/index.ts +++ b/plugins/element-hider-luna/src/index.ts @@ -400,8 +400,7 @@ document.addEventListener( const eventX = event.clientX; const eventY = event.clientY; - // Prevent default immediately if we plan to handle it - event.preventDefault(); + // 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(() => { diff --git a/plugins/oled-theme-luna/src/index.ts b/plugins/oled-theme-luna/src/index.ts index fb8cdfa..6d6c82f 100644 --- a/plugins/oled-theme-luna/src/index.ts +++ b/plugins/oled-theme-luna/src/index.ts @@ -31,11 +31,17 @@ const getQualityColor = (audioQuality: string): string => { return QUALITY_COLORS.MAX; } else if (quality?.includes("LOSSLESS")) { return QUALITY_COLORS.HIGH; - } else { + } else if (quality?.includes("HIGH")) { + 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) const resetSeekBarColor = async (): Promise => { try { @@ -82,17 +88,16 @@ const applyQualityColors = async (): Promise => { // Function to monitor track changes using track ID const setupQualityMonitoring = (): void => { + if (qualityMonitoringIntervalId != null) return; let lastTrackId: string | null = null; - const interval = setInterval(() => { + qualityMonitoringIntervalId = window.setInterval(() => { if (!settings.qualityColorMatchedSeekBar) return; const currentTrackId = PlayState.playbackContext?.actualProductId; if (currentTrackId && currentTrackId !== lastTrackId) { - //trace.msg.log(`[OLED Theme] Track ID changed: ${lastTrackId} -> ${currentTrackId}`); lastTrackId = currentTrackId; applyQualityColors(); } }, 250); - unloads.add(() => clearInterval(interval)); // Initial color application (if a track is already loaded) const currentTrackId = PlayState.playbackContext?.actualProductId; @@ -102,6 +107,13 @@ const setupQualityMonitoring = (): void => { } }; +const stopQualityMonitoring = (): void => { + if (qualityMonitoringIntervalId != null) { + window.clearInterval(qualityMonitoringIntervalId); + qualityMonitoringIntervalId = null; + } +}; + // Function to apply theme styles based on current settings const applyThemeStyles = (): void => { // Choose the appropriate CSS file based on settings @@ -126,8 +138,9 @@ const applyThemeStyles = (): void => { ); setupQualityMonitoring(); } else { - // If disabling, reset the seek bar color + // If disabling, reset the seek bar color and stop monitoring resetSeekBarColor(); + stopQualityMonitoring(); } // Apply the selected theme using StyleTag @@ -145,3 +158,8 @@ window.updateOLEDThemeStyles = applyThemeStyles; // Apply the OLED theme initially applyThemeStyles(); + +// Ensure interval is cleared on unload +unloads.add(() => { + stopQualityMonitoring(); +}); diff --git a/plugins/oled-theme-luna/src/light-theme.css b/plugins/oled-theme-luna/src/light-theme.css index 630593f..38d4935 100644 --- a/plugins/oled-theme-luna/src/light-theme.css +++ b/plugins/oled-theme-luna/src/light-theme.css @@ -145,7 +145,7 @@ [data-test="current-media-imagery"] { border: 0 !important; - margin: none; + margin: 0; } [class^="_imageBorder"] { diff --git a/plugins/oled-theme-luna/src/oled-friendly.css b/plugins/oled-theme-luna/src/oled-friendly.css index 4437e77..2f95e54 100644 --- a/plugins/oled-theme-luna/src/oled-friendly.css +++ b/plugins/oled-theme-luna/src/oled-friendly.css @@ -120,7 +120,7 @@ [data-test="current-media-imagery"] { border: 0 !important; - margin: none; + margin: 0; } [class^="_imageBorder"] { diff --git a/plugins/radiant-lyrics-luna/src/Settings.tsx b/plugins/radiant-lyrics-luna/src/Settings.tsx index d5ef020..b5dbab2 100644 --- a/plugins/radiant-lyrics-luna/src/Settings.tsx +++ b/plugins/radiant-lyrics-luna/src/Settings.tsx @@ -65,7 +65,7 @@ export const Settings = () => { title="Lyrics Glow Effect" desc="Enable glowing effect for lyrics & Font Stytling Changes" checked={lyricsGlowEnabled} - onChange={(_event: unknown, checked: boolean) => { + onChange={(_: void, checked: boolean) => { setLyricsGlowEnabled((settings.lyricsGlowEnabled = checked)); // Update styles immediately when setting changes if ((window as any).updateRadiantLyricsStyles) { @@ -77,7 +77,7 @@ export const Settings = () => { title="Track Title Glow" desc="Apply glow to the track title" checked={trackTitleGlow} - onChange={(_event: unknown, checked: boolean) => { + onChange={(_: void, checked: boolean) => { setTrackTitleGlow((settings.trackTitleGlow = checked)); if ((window as any).updateRadiantLyricsStyles) { (window as any).updateRadiantLyricsStyles(); @@ -88,7 +88,7 @@ export const Settings = () => { title="Hide UI Feature" desc="Enable hide/unhide UI functionality with toggle buttons" checked={hideUIEnabled} - onChange={(_event: unknown, checked: boolean) => { + onChange={(_: void, checked: boolean) => { setHideUIEnabled((settings.hideUIEnabled = checked)); }} /> @@ -96,7 +96,7 @@ export const Settings = () => { title="Player Bar Visibility in Hide UI Mode" desc="Keep player bar visible when UI is hidden" checked={playerBarVisible} - onChange={(_event: unknown, checked: boolean) => { + onChange={(_: void, checked: boolean) => { console.log("Player Bar Visibility:", checked ? "visible" : "hidden"); setPlayerBarVisible((settings.playerBarVisible = checked)); // Update styles immediately when setting changes @@ -109,7 +109,7 @@ export const Settings = () => { 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" checked={spinningCoverEverywhere} - onChange={(_event: unknown, checked: boolean) => { + onChange={(_: void, checked: boolean) => { console.log( "Spinning Cover Everywhere:", checked ? "enabled" : "disabled", @@ -127,7 +127,7 @@ export const Settings = () => { title="Performance Mode | Experimental" desc="Performance mode: Reduces blur effects & uses smaller image sizes, to optimize GPU usage" checked={performanceMode} - onChange={(_event: unknown, checked: boolean) => { + onChange={(_: void, checked: boolean) => { console.log("Performance Mode:", checked ? "enabled" : "disabled"); setPerformanceMode((settings.performanceMode = checked)); // Update background animations immediately when setting changes @@ -143,7 +143,7 @@ export const Settings = () => { title="Background Cover Spin" // Cheers @Max/n0201 for the idea <3 desc="Enable the spinning cover art background animation" checked={spinningArtEnabled} - onChange={(_event: unknown, checked: boolean) => { + onChange={(_: void, checked: boolean) => { console.log( "Background Cover Spin:", checked ? "enabled" : "disabled", @@ -262,7 +262,7 @@ export const Settings = () => { title="Settings Affect Now Playing" desc="Apply background settings to Now Playing view" checked={settingsAffectNowPlaying} - onChange={(_event: unknown, checked: boolean) => { + onChange={(_: void, checked: boolean) => { console.log( "Settings Affect Now Playing:", checked ? "enabled" : "disabled", diff --git a/plugins/radiant-lyrics-luna/src/index.ts b/plugins/radiant-lyrics-luna/src/index.ts index 883abed..cf86246 100644 --- a/plugins/radiant-lyrics-luna/src/index.ts +++ b/plugins/radiant-lyrics-luna/src/index.ts @@ -38,30 +38,30 @@ const updateRadiantLyricsTextGlow = function (): void { // Function to update styles when settings change const updateRadiantLyricsStyles = function (): void { if (isHidden) { - // Apply only base styles (button hiding), NOT separated lyrics styles - // to avoid affecting lyrics scrolling behavior + // Apply only base styles (button hiding) and optional player bar hiding baseStyleTag.css = baseStyles; - - // Apply player bar styles based on setting if (!settings.playerBarVisible) { playerBarStyleTag.css = playerBarHidden; } else { playerBarStyleTag.remove(); } + // Ensure lyrics glow styles are not applied when hidden + lyricsGlowStyleTag.remove(); + return; } - // Update lyrics glow based on setting (only if UI is not hidden to avoid interference) + // Update lyrics glow based on setting (only when UI is visible) const lyricsContainer = document.querySelector('[class^="_lyricsContainer"]'); - if (lyricsContainer && !isHidden) { + if (lyricsContainer) { if (settings.lyricsGlowEnabled) { - lyricsContainer.classList.remove("lyrics-glow-disabled"); + (lyricsContainer as HTMLElement).classList.remove("lyrics-glow-disabled"); lyricsGlowStyleTag.css = lyricsGlow; updateRadiantLyricsTextGlow(); } else { - lyricsContainer.classList.add("lyrics-glow-disabled"); + (lyricsContainer as HTMLElement).classList.add("lyrics-glow-disabled"); lyricsGlowStyleTag.remove(); } - } else if (!isHidden) { + } else { observePromise(unloads, '[class^="_lyricsContainer"]') .then((el) => { if (!el) return; @@ -95,6 +95,16 @@ const updateRadiantLyricsStyles = function (): void { var isHidden = false; let unhideButtonAutoFadeTimeout: number | null = null; +// Helper to safely create a one-off timeout that clears previous if any +const safelySetAutoFadeTimeout = ( + existingId: number | null, + fn: () => void, + delay: number, +): number => { + if (existingId != null) window.clearTimeout(existingId); + return window.setTimeout(fn, delay); +}; + const updateButtonStates = function (): void { const hideButton = document.querySelector(".hide-ui-button") as HTMLElement; const unhideButton = document.querySelector( @@ -120,7 +130,7 @@ const updateButtonStates = function (): void { } if (unhideButton) { // Clear any existing auto-fade timeout - if (unhideButtonAutoFadeTimeout) { + if (unhideButtonAutoFadeTimeout != null) { window.clearTimeout(unhideButtonAutoFadeTimeout); unhideButtonAutoFadeTimeout = null; } @@ -137,11 +147,15 @@ const updateButtonStates = function (): void { unhideButton.style.pointerEvents = "auto"; // Set up auto-fade after 2 seconds - unhideButtonAutoFadeTimeout = window.setTimeout(() => { - if (isHidden && unhideButton && !unhideButton.matches(":hover")) { - unhideButton.classList.add("auto-faded"); - } - }, 2000); + unhideButtonAutoFadeTimeout = safelySetAutoFadeTimeout( + unhideButtonAutoFadeTimeout, + () => { + if (isHidden && unhideButton && !unhideButton.matches(":hover")) { + unhideButton.classList.add("auto-faded"); + } + }, + 2000, + ); }, 50); } else { // Smooth fade out for Unhide UI button @@ -771,7 +785,7 @@ unloads.add(() => { cleanUpDynamicArt(); // Clean up auto-fade timeout - if (unhideButtonAutoFadeTimeout) { + if (unhideButtonAutoFadeTimeout != null) { window.clearTimeout(unhideButtonAutoFadeTimeout); unhideButtonAutoFadeTimeout = null; } diff --git a/plugins/radiant-lyrics-luna/src/lyrics-glow.css b/plugins/radiant-lyrics-luna/src/lyrics-glow.css index b455ead..cf04fb9 100644 --- a/plugins/radiant-lyrics-luna/src/lyrics-glow.css +++ b/plugins/radiant-lyrics-luna/src/lyrics-glow.css @@ -9,22 +9,19 @@ @font-face { font-family: "AbyssFont"; font-weight: 500; - src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") - format("woff2"); + src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2"); } @font-face { font-family: "AbyssFont"; font-weight: 600; - src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") - format("woff2"); + src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2"); } @font-face { font-family: "AbyssFont"; font-weight: 700; - src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") - format("woff2"); + src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2"); } /* Enhanced lyrics styling with glow effects */ From d53fd08ee8a6b6f7db2af8abd9b3064c450f0bcb Mon Sep 17 00:00:00 2001 From: meowarex Date: Tue, 9 Sep 2025 19:20:30 +1000 Subject: [PATCH 04/10] Code Review --- plugins/colorama-lyrics-luna/src/Settings.tsx | 22 +++++++-------- plugins/radiant-lyrics-luna/src/Settings.tsx | 27 +++++++++++-------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/plugins/colorama-lyrics-luna/src/Settings.tsx b/plugins/colorama-lyrics-luna/src/Settings.tsx index 603f6d0..5b8e408 100644 --- a/plugins/colorama-lyrics-luna/src/Settings.tsx +++ b/plugins/colorama-lyrics-luna/src/Settings.tsx @@ -142,17 +142,17 @@ export const Settings = () => { if (!hexColorRegex.test(trimmed)) return; if (mode === "single") { const next = normalizeToRGB(trimmed); - setSingleColor(next); settings.singleColor = next; + setSingleColor(next); if (updateInput) setCustomInput(next); } else if (mode === "gradient-experimental") { const next = normalizeToRGB(trimmed); if (activeEndpoint === "end") { - setGradientEnd(next); settings.gradientEnd = next; + setGradientEnd(next); } else { - setGradientStart(next); settings.gradientStart = next; + setGradientStart(next); } if (updateInput) setCustomInput(next); } @@ -511,15 +511,15 @@ export const Settings = () => { onClick={() => { const next = normalizeToRGB(color); if (mode === "single") { - setSingleColor(next); settings.singleColor = next; + setSingleColor(next); } else if (mode === "gradient-experimental") { if (activeEndpoint === "end") { - setGradientEnd(next); settings.gradientEnd = next; + setGradientEnd(next); } else { - setGradientStart(next); settings.gradientStart = next; + setGradientStart(next); } } setCustomInput(next); @@ -618,8 +618,8 @@ export const Settings = () => { value={singleAlpha} onChange={(e) => { const value = Number(e.target.value); - setSingleAlpha(value); settings.singleAlpha = value; + setSingleAlpha(value); requestApply(); }} style={{ width: "100%" }} @@ -661,8 +661,8 @@ export const Settings = () => { value={gradientStartAlpha} onChange={(e) => { const value = Number(e.target.value); - setGradientStartAlpha(value); settings.gradientStartAlpha = value; + setGradientStartAlpha(value); requestApply(); }} style={{ width: "100%" }} @@ -700,8 +700,8 @@ export const Settings = () => { value={gradientEndAlpha} onChange={(e) => { const value = Number(e.target.value); - setGradientEndAlpha(value); settings.gradientEndAlpha = value; + setGradientEndAlpha(value); requestApply(); }} style={{ width: "100%" }} @@ -735,8 +735,8 @@ export const Settings = () => { value={gradientAngle} onChange={(e) => { const value = Number(e.target.value); - setGradientAngle(value); settings.gradientAngle = value; + setGradientAngle(value); requestApply(); }} style={{ width: "100%" }} @@ -770,8 +770,8 @@ export const Settings = () => { value={gradientAngle} onChange={(e) => { const value = Number(e.target.value); - setGradientAngle(value); settings.gradientAngle = value; + setGradientAngle(value); requestApply(); }} style={{ width: "100%" }} diff --git a/plugins/radiant-lyrics-luna/src/Settings.tsx b/plugins/radiant-lyrics-luna/src/Settings.tsx index b5dbab2..73bd486 100644 --- a/plugins/radiant-lyrics-luna/src/Settings.tsx +++ b/plugins/radiant-lyrics-luna/src/Settings.tsx @@ -54,18 +54,23 @@ export const Settings = () => { settings.trackTitleGlow, ); - // Use a permissive wrapper to align with current usage props + // Derive props and override onChange to accept a broader first param type + type BaseSwitchProps = React.ComponentProps; + type AnySwitchProps = Omit & { + onChange: (_: unknown, checked: boolean) => void; + checked: boolean; + }; const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType< - Record + AnySwitchProps >; return ( { + onChange={(_: unknown, checked: boolean) => { setLyricsGlowEnabled((settings.lyricsGlowEnabled = checked)); // Update styles immediately when setting changes if ((window as any).updateRadiantLyricsStyles) { @@ -77,7 +82,7 @@ export const Settings = () => { title="Track Title Glow" desc="Apply glow to the track title" checked={trackTitleGlow} - onChange={(_: void, checked: boolean) => { + onChange={(_: unknown, checked: boolean) => { setTrackTitleGlow((settings.trackTitleGlow = checked)); if ((window as any).updateRadiantLyricsStyles) { (window as any).updateRadiantLyricsStyles(); @@ -88,7 +93,7 @@ export const Settings = () => { title="Hide UI Feature" desc="Enable hide/unhide UI functionality with toggle buttons" checked={hideUIEnabled} - onChange={(_: void, checked: boolean) => { + onChange={(_: unknown, checked: boolean) => { setHideUIEnabled((settings.hideUIEnabled = checked)); }} /> @@ -96,7 +101,7 @@ export const Settings = () => { title="Player Bar Visibility in Hide UI Mode" desc="Keep player bar visible when UI is hidden" checked={playerBarVisible} - onChange={(_: void, checked: boolean) => { + onChange={(_: unknown, checked: boolean) => { console.log("Player Bar Visibility:", checked ? "visible" : "hidden"); setPlayerBarVisible((settings.playerBarVisible = checked)); // Update styles immediately when setting changes @@ -109,7 +114,7 @@ export const Settings = () => { 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" checked={spinningCoverEverywhere} - onChange={(_: void, checked: boolean) => { + onChange={(_: unknown, checked: boolean) => { console.log( "Spinning Cover Everywhere:", checked ? "enabled" : "disabled", @@ -127,7 +132,7 @@ export const Settings = () => { title="Performance Mode | Experimental" desc="Performance mode: Reduces blur effects & uses smaller image sizes, to optimize GPU usage" checked={performanceMode} - onChange={(_: void, checked: boolean) => { + onChange={(_: unknown, checked: boolean) => { console.log("Performance Mode:", checked ? "enabled" : "disabled"); setPerformanceMode((settings.performanceMode = checked)); // Update background animations immediately when setting changes @@ -143,7 +148,7 @@ export const Settings = () => { title="Background Cover Spin" // Cheers @Max/n0201 for the idea <3 desc="Enable the spinning cover art background animation" checked={spinningArtEnabled} - onChange={(_: void, checked: boolean) => { + onChange={(_: unknown, checked: boolean) => { console.log( "Background Cover Spin:", checked ? "enabled" : "disabled", @@ -262,7 +267,7 @@ export const Settings = () => { title="Settings Affect Now Playing" desc="Apply background settings to Now Playing view" checked={settingsAffectNowPlaying} - onChange={(_: void, checked: boolean) => { + onChange={(_: unknown, checked: boolean) => { console.log( "Settings Affect Now Playing:", checked ? "enabled" : "disabled", From 9c9b47c930fc1415cdb64d554808df3b1d6bb6c2 Mon Sep 17 00:00:00 2001 From: meowarex Date: Tue, 9 Sep 2025 19:33:29 +1000 Subject: [PATCH 05/10] Fixed Green HideUI Button --- plugins/radiant-lyrics-luna/src/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/radiant-lyrics-luna/src/index.ts b/plugins/radiant-lyrics-luna/src/index.ts index cf86246..3f02147 100644 --- a/plugins/radiant-lyrics-luna/src/index.ts +++ b/plugins/radiant-lyrics-luna/src/index.ts @@ -225,7 +225,7 @@ const createHideUIButton = function (): void { hideUIButton.setAttribute("aria-label", "Hide UI"); hideUIButton.setAttribute("title", "Hide UI"); hideUIButton.textContent = "Hide UI"; - hideUIButton.style.backgroundColor = "var(--wave-color-solid-accent-fill)"; + hideUIButton.style.backgroundColor = "#ffffff"; hideUIButton.style.color = "black"; hideUIButton.style.border = "none"; hideUIButton.style.borderRadius = "12px"; @@ -245,11 +245,10 @@ const createHideUIButton = function (): void { hideUIButton.style.visibility = "hidden"; hideUIButton.style.pointerEvents = "none"; hideUIButton.addEventListener("mouseenter", () => { - hideUIButton.style.backgroundColor = "lightgray"; + hideUIButton.style.backgroundColor = "#e5e5e5"; }); hideUIButton.addEventListener("mouseleave", () => { - hideUIButton.style.backgroundColor = - "var(--wave-color-solid-accent-fill)"; + hideUIButton.style.backgroundColor = "#ffffff"; }); hideUIButton.onclick = toggleRadiantLyrics; buttonContainer.insertBefore(hideUIButton, fullscreenButton.nextSibling); From 2ea44bd3ccf5c513eeec411e6f102dc086c1da2d Mon Sep 17 00:00:00 2001 From: meowarex Date: Tue, 9 Sep 2025 20:09:24 +1000 Subject: [PATCH 06/10] Background Cover Scale --- plugins/radiant-lyrics-luna/src/Settings.tsx | 25 +++++++ plugins/radiant-lyrics-luna/src/index.ts | 72 +++++++++++++------- 2 files changed, 73 insertions(+), 24 deletions(-) diff --git a/plugins/radiant-lyrics-luna/src/Settings.tsx b/plugins/radiant-lyrics-luna/src/Settings.tsx index 73bd486..5a8577f 100644 --- a/plugins/radiant-lyrics-luna/src/Settings.tsx +++ b/plugins/radiant-lyrics-luna/src/Settings.tsx @@ -16,6 +16,7 @@ export const settings = await ReactiveStore.getPluginStorage("RadiantLyrics", { backgroundBrightness: 40, spinSpeed: 45, settingsAffectNowPlaying: true, + backgroundScale: 15, }); export const Settings = () => { @@ -53,6 +54,9 @@ export const Settings = () => { const [trackTitleGlow, setTrackTitleGlow] = React.useState( settings.trackTitleGlow, ); + const [backgroundScale, setBackgroundScale] = React.useState( + settings.backgroundScale, + ); // Derive props and override onChange to accept a broader first param type type BaseSwitchProps = React.ComponentProps; @@ -165,6 +169,27 @@ export const Settings = () => { } }} /> + { + const next = Math.max(1, Math.min(30, Math.round(value))); + setBackgroundScale((settings.backgroundScale = next)); + // Immediate visual update without throttle + if ((window as any).updateRadiantLyricsGlobalBackground) { + (window as any).updateRadiantLyricsGlobalBackground(); + } + if ( + settings.settingsAffectNowPlaying && + (window as any).updateRadiantLyricsNowPlayingBackground + ) { + (window as any).updateRadiantLyricsNowPlayingBackground(); + } + }} + /> { + const value = Math.round(settings.backgroundScale); + return Math.max(0.1, Math.min(3, value / 10)); +}; // Import CSS files directly using Luna's file:// syntax - Took me a while to figure out <3 import baseStyles from "file://styles.css?minify"; @@ -377,7 +382,9 @@ function updateCoverArtBackground(method: number = 0): void { .querySelectorAll( ".now-playing-background-image, .now-playing-black-bg, .now-playing-gradient-overlay", ) - .forEach((el) => el.remove()); + .forEach((el) => { + el.remove(); + }); // Create container nowPlayingBackgroundContainer = document.createElement("div"); @@ -455,10 +462,13 @@ function updateCoverArtBackground(method: number = 0): void { // Performance mode with spinning enabled const blur = Math.min(settings.backgroundBlur, 20); const contrast = Math.min(settings.backgroundContrast, 150); - if (nowPlayingBackgroundImage.style.width !== "70vw") - nowPlayingBackgroundImage.style.width = "70vw"; - if (nowPlayingBackgroundImage.style.height !== "70vh") - nowPlayingBackgroundImage.style.height = "70vh"; + const scalePm = getScaledMultiplier(); + const widthPm = `${Math.round(scalePm * 100)}vw`; + const heightPm = `${Math.round(scalePm * 100)}vh`; + if (nowPlayingBackgroundImage.style.width !== widthPm) + nowPlayingBackgroundImage.style.width = widthPm; + if (nowPlayingBackgroundImage.style.height !== heightPm) + nowPlayingBackgroundImage.style.height = heightPm; const filt = `blur(${blur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${contrast}%)`; if (nowPlayingBackgroundImage.style.filter !== filt) nowPlayingBackgroundImage.style.filter = filt; @@ -473,10 +483,13 @@ function updateCoverArtBackground(method: number = 0): void { nowPlayingBackgroundImage.classList.remove("performance-mode-static"); } else { // Normal mode - if (nowPlayingBackgroundImage.style.width !== "90vw") - nowPlayingBackgroundImage.style.width = "90vw"; - if (nowPlayingBackgroundImage.style.height !== "90vh") - nowPlayingBackgroundImage.style.height = "90vh"; + const scaleNm = getScaledMultiplier(); + const widthNm = `${Math.round(scaleNm * 100)}vw`; + const heightNm = `${Math.round(scaleNm * 100)}vh`; + if (nowPlayingBackgroundImage.style.width !== widthNm) + nowPlayingBackgroundImage.style.width = widthNm; + if (nowPlayingBackgroundImage.style.height !== heightNm) + nowPlayingBackgroundImage.style.height = heightNm; const filt = `blur(${settings.backgroundBlur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${settings.backgroundContrast}%)`; if (nowPlayingBackgroundImage.style.filter !== filt) nowPlayingBackgroundImage.style.filter = filt; @@ -520,16 +533,15 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => { return; } - // Throttle updates to prevent excessive DOM manipulation + // Only throttle image src updates; style updates below always run for responsiveness const now = Date.now(); - if ( - now - lastUpdateTime < getUpdateThrottle() && - currentGlobalCoverSrc === coverArtImageSrc - ) { - return; + const shouldUpdateImageSrc = + now - lastUpdateTime >= getUpdateThrottle() || + currentGlobalCoverSrc !== coverArtImageSrc; + if (shouldUpdateImageSrc) { + lastUpdateTime = now; + currentGlobalCoverSrc = coverArtImageSrc; } - lastUpdateTime = now; - currentGlobalCoverSrc = coverArtImageSrc; // Add StyleTag if not present if (!globalSpinningBgStyleTag) { @@ -579,18 +591,25 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => { globalBackgroundContainer.appendChild(globalBackgroundImage); } - // Update image source efficiently - if (globalBackgroundImage && globalBackgroundImage.src !== coverArtImageSrc) { + // Update image source efficiently (throttled) + if ( + shouldUpdateImageSrc && + globalBackgroundImage && + globalBackgroundImage.src !== coverArtImageSrc + ) { globalBackgroundImage.src = coverArtImageSrc; } // Apply performance-optimized settings if (globalBackgroundImage) { + const scale = getScaledMultiplier(); + const scaledWidth = `${Math.round(scale * 100)}vw`; + const scaledHeight = `${Math.round(scale * 100)}vh`; // Performance mode optimizations if (settings.performanceMode) { // Performance mode with spinning enabled - globalBackgroundImage.style.width = "100vw"; - globalBackgroundImage.style.height = "100vh"; + globalBackgroundImage.style.width = scaledWidth; + globalBackgroundImage.style.height = scaledHeight; globalBackgroundImage.style.filter = `blur(${Math.min(settings.backgroundBlur, 20)}px) brightness(${settings.backgroundBrightness / 100}) contrast(${Math.min(settings.backgroundContrast, 150)}%)`; if (settings.spinningArtEnabled) { globalBackgroundImage.style.animation = `spinGlobal ${settings.spinSpeed}s linear infinite`; @@ -602,8 +621,8 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => { globalBackgroundImage.classList.remove("performance-mode-static"); } else { // Normal mode - globalBackgroundImage.style.width = "150vw"; - globalBackgroundImage.style.height = "150vh"; + globalBackgroundImage.style.width = scaledWidth; + globalBackgroundImage.style.height = scaledHeight; globalBackgroundImage.style.filter = `blur(${settings.backgroundBlur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${settings.backgroundContrast}%)`; if (settings.spinningArtEnabled) { globalBackgroundImage.style.animation = `spinGlobal ${settings.spinSpeed}s linear infinite`; @@ -664,7 +683,7 @@ const updateRadiantLyricsNowPlayingBackground = function (): void { const defaultContrast = 120; const defaultSpinSpeed = 45; - let blur, brightness, contrast, spinSpeed; + let blur: number, brightness: number, contrast: number, spinSpeed: number; if (settings.settingsAffectNowPlaying) { blur = settings.backgroundBlur; @@ -678,6 +697,11 @@ const updateRadiantLyricsNowPlayingBackground = function (): void { spinSpeed = defaultSpinSpeed; } + // Apply size based on backgroundScale + const scale = getScaledMultiplier(); + imgElement.style.width = `${Math.round(scale * 100)}vw`; + imgElement.style.height = `${Math.round(scale * 100)}vh`; + // Performance mode optimizations if (settings.performanceMode) { // Reduce blur and effects for better performance, but keep spinning From 78d960588c88dbafc03c6c43f30c04c788297403 Mon Sep 17 00:00:00 2001 From: meowarex Date: Tue, 9 Sep 2025 20:23:42 +1000 Subject: [PATCH 07/10] Background Radius --- plugins/radiant-lyrics-luna/src/Settings.tsx | 27 +++++++++++++++++++- plugins/radiant-lyrics-luna/src/index.ts | 11 ++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/plugins/radiant-lyrics-luna/src/Settings.tsx b/plugins/radiant-lyrics-luna/src/Settings.tsx index 5a8577f..f0aa7a1 100644 --- a/plugins/radiant-lyrics-luna/src/Settings.tsx +++ b/plugins/radiant-lyrics-luna/src/Settings.tsx @@ -17,6 +17,7 @@ export const settings = await ReactiveStore.getPluginStorage("RadiantLyrics", { spinSpeed: 45, settingsAffectNowPlaying: true, backgroundScale: 15, + backgroundRadius: 0, }); export const Settings = () => { @@ -57,6 +58,9 @@ export const Settings = () => { 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; @@ -178,7 +182,6 @@ export const Settings = () => { onNumber={(value: number) => { const next = Math.max(1, Math.min(30, Math.round(value))); setBackgroundScale((settings.backgroundScale = next)); - // Immediate visual update without throttle if ((window as any).updateRadiantLyricsGlobalBackground) { (window as any).updateRadiantLyricsGlobalBackground(); } @@ -190,6 +193,28 @@ export const Settings = () => { } }} /> + { + const next = Math.max(0, Math.min(50, Math.round(value))); + setBackgroundRadius((settings.backgroundRadius = next)); + if ((window as any).updateRadiantLyricsGlobalBackground) { + (window as any).updateRadiantLyricsGlobalBackground(); + } + if ( + sessionStorage && + settings.settingsAffectNowPlaying && + (window as any).updateRadiantLyricsNowPlayingBackground + ) { + (window as any).updateRadiantLyricsNowPlayingBackground(); + } + }} + /> { const scale = getScaledMultiplier(); const scaledWidth = `${Math.round(scale * 100)}vw`; const scaledHeight = `${Math.round(scale * 100)}vh`; + const radius = `${Math.max(0, Math.min(50, settings.backgroundRadius ?? 0))}%`; // Performance mode optimizations if (settings.performanceMode) { // Performance mode with spinning enabled globalBackgroundImage.style.width = scaledWidth; globalBackgroundImage.style.height = scaledHeight; globalBackgroundImage.style.filter = `blur(${Math.min(settings.backgroundBlur, 20)}px) brightness(${settings.backgroundBrightness / 100}) contrast(${Math.min(settings.backgroundContrast, 150)}%)`; + if (globalBackgroundImage.style.borderRadius !== radius) + globalBackgroundImage.style.borderRadius = radius; if (settings.spinningArtEnabled) { globalBackgroundImage.style.animation = `spinGlobal ${settings.spinSpeed}s linear infinite`; globalBackgroundImage.style.willChange = "transform"; @@ -624,6 +633,8 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => { globalBackgroundImage.style.width = scaledWidth; globalBackgroundImage.style.height = scaledHeight; globalBackgroundImage.style.filter = `blur(${settings.backgroundBlur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${settings.backgroundContrast}%)`; + if (globalBackgroundImage.style.borderRadius !== radius) + globalBackgroundImage.style.borderRadius = radius; if (settings.spinningArtEnabled) { globalBackgroundImage.style.animation = `spinGlobal ${settings.spinSpeed}s linear infinite`; globalBackgroundImage.style.willChange = "transform"; From f2c31bb33aecc4b4d69d1f66d5c36ac66bb2963f Mon Sep 17 00:00:00 2001 From: meowarex Date: Tue, 9 Sep 2025 20:44:15 +1000 Subject: [PATCH 08/10] Background Cover Scale --- plugins/radiant-lyrics-luna/src/Settings.tsx | 16 ++--- plugins/radiant-lyrics-luna/src/index.ts | 73 +++++++++++--------- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/plugins/radiant-lyrics-luna/src/Settings.tsx b/plugins/radiant-lyrics-luna/src/Settings.tsx index f0aa7a1..29f87d8 100644 --- a/plugins/radiant-lyrics-luna/src/Settings.tsx +++ b/plugins/radiant-lyrics-luna/src/Settings.tsx @@ -174,14 +174,14 @@ export const Settings = () => { }} /> { - const next = Math.max(1, Math.min(30, Math.round(value))); - setBackgroundScale((settings.backgroundScale = next)); + setBackgroundScale((settings.backgroundScale = value)); if ((window as any).updateRadiantLyricsGlobalBackground) { (window as any).updateRadiantLyricsGlobalBackground(); } @@ -195,19 +195,17 @@ export const Settings = () => { /> { - const next = Math.max(0, Math.min(50, Math.round(value))); - setBackgroundRadius((settings.backgroundRadius = next)); + setBackgroundRadius((settings.backgroundRadius = value)); if ((window as any).updateRadiantLyricsGlobalBackground) { (window as any).updateRadiantLyricsGlobalBackground(); } if ( - sessionStorage && settings.settingsAffectNowPlaying && (window as any).updateRadiantLyricsNowPlayingBackground ) { diff --git a/plugins/radiant-lyrics-luna/src/index.ts b/plugins/radiant-lyrics-luna/src/index.ts index 3ac38c6..898f84d 100644 --- a/plugins/radiant-lyrics-luna/src/index.ts +++ b/plugins/radiant-lyrics-luna/src/index.ts @@ -2,10 +2,33 @@ import { LunaUnload, Tracer, ftch } from "@luna/core"; import { StyleTag, PlayState, observePromise, observe } from "@luna/lib"; import { settings, Settings } from "./Settings"; -// Interpret integer backgroundScale (1–30) strictly as 0.1–3.0 multiplier +// Interpret integer backgroundScale (e.g., 10=1.0x, 20=2.0x) const getScaledMultiplier = (): number => { - const value = Math.round(settings.backgroundScale); - return Math.max(0.1, Math.min(3, value / 10)); + const value = settings.backgroundScale; + return value / 10; +}; + +// Helper to size an image using its intrinsic dimensions times scale (in pixels) +const applyScaledPixelSize = (img: HTMLImageElement | null): void => { + if (!img) return; + const scale = getScaledMultiplier(); + const apply = () => { + const w = img.naturalWidth; + const h = img.naturalHeight; + if (w > 0 && h > 0) { + const wPx = Math.round(w * scale); + const hPx = Math.round(h * scale); + const wStr = `${wPx}px`; + const hStr = `${hPx}px`; + if (img.style.width !== wStr) img.style.width = wStr; + if (img.style.height !== hStr) img.style.height = hStr; + } + }; + if (img.complete && img.naturalWidth > 0) { + apply(); + } else { + img.addEventListener("load", apply, { once: true }); + } }; // Import CSS files directly using Luna's file:// syntax - Took me a while to figure out <3 @@ -456,20 +479,16 @@ function updateCoverArtBackground(method: number = 0): void { currentNowPlayingCoverSrc = coverArtImageSrc; } - // Apply performance-optimized settings + // Apply pixel-based size using intrinsic dimensions + applyScaledPixelSize(nowPlayingBackgroundImage); + + // Apply performance-optimized settings (filter/animation); size handled above if (nowPlayingBackgroundImage) { if (settings.performanceMode) { // Performance mode with spinning enabled const blur = Math.min(settings.backgroundBlur, 20); const contrast = Math.min(settings.backgroundContrast, 150); - const scalePm = getScaledMultiplier(); - const widthPm = `${Math.round(scalePm * 100)}vw`; - const heightPm = `${Math.round(scalePm * 100)}vh`; - const radiusPm = `${Math.max(0, Math.min(50, settings.backgroundRadius ?? 0))}%`; - if (nowPlayingBackgroundImage.style.width !== widthPm) - nowPlayingBackgroundImage.style.width = widthPm; - if (nowPlayingBackgroundImage.style.height !== heightPm) - nowPlayingBackgroundImage.style.height = heightPm; + const radiusPm = `${settings.backgroundRadius}%`; if (nowPlayingBackgroundImage.style.borderRadius !== radiusPm) nowPlayingBackgroundImage.style.borderRadius = radiusPm; const filt = `blur(${blur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${contrast}%)`; @@ -486,14 +505,7 @@ function updateCoverArtBackground(method: number = 0): void { nowPlayingBackgroundImage.classList.remove("performance-mode-static"); } else { // Normal mode - const scaleNm = getScaledMultiplier(); - const widthNm = `${Math.round(scaleNm * 100)}vw`; - const heightNm = `${Math.round(scaleNm * 100)}vh`; - const radiusNm = `${Math.max(0, Math.min(50, settings.backgroundRadius ?? 0))}%`; - if (nowPlayingBackgroundImage.style.width !== widthNm) - nowPlayingBackgroundImage.style.width = widthNm; - if (nowPlayingBackgroundImage.style.height !== heightNm) - nowPlayingBackgroundImage.style.height = heightNm; + const radiusNm = `${settings.backgroundRadius}%`; if (nowPlayingBackgroundImage.style.borderRadius !== radiusNm) nowPlayingBackgroundImage.style.borderRadius = radiusNm; const filt = `blur(${settings.backgroundBlur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${settings.backgroundContrast}%)`; @@ -608,15 +620,12 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => { // Apply performance-optimized settings if (globalBackgroundImage) { - const scale = getScaledMultiplier(); - const scaledWidth = `${Math.round(scale * 100)}vw`; - const scaledHeight = `${Math.round(scale * 100)}vh`; - const radius = `${Math.max(0, Math.min(50, settings.backgroundRadius ?? 0))}%`; + // Pixel-based sizing based on intrinsic dimensions + applyScaledPixelSize(globalBackgroundImage); + const radius = `${settings.backgroundRadius}%`; // Performance mode optimizations if (settings.performanceMode) { // Performance mode with spinning enabled - globalBackgroundImage.style.width = scaledWidth; - globalBackgroundImage.style.height = scaledHeight; globalBackgroundImage.style.filter = `blur(${Math.min(settings.backgroundBlur, 20)}px) brightness(${settings.backgroundBrightness / 100}) contrast(${Math.min(settings.backgroundContrast, 150)}%)`; if (globalBackgroundImage.style.borderRadius !== radius) globalBackgroundImage.style.borderRadius = radius; @@ -630,8 +639,6 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => { globalBackgroundImage.classList.remove("performance-mode-static"); } else { // Normal mode - globalBackgroundImage.style.width = scaledWidth; - globalBackgroundImage.style.height = scaledHeight; globalBackgroundImage.style.filter = `blur(${settings.backgroundBlur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${settings.backgroundContrast}%)`; if (globalBackgroundImage.style.borderRadius !== radius) globalBackgroundImage.style.borderRadius = radius; @@ -686,7 +693,7 @@ const updateRadiantLyricsNowPlayingBackground = function (): void { ".now-playing-background-image", ); nowPlayingBackgroundImages.forEach((img: Element) => { - const imgElement = img as HTMLElement; + const imgElement = img as HTMLImageElement; // Default values when settings don't affect Now Playing const defaultBlur = 80; @@ -708,10 +715,10 @@ const updateRadiantLyricsNowPlayingBackground = function (): void { spinSpeed = defaultSpinSpeed; } - // Apply size based on backgroundScale - const scale = getScaledMultiplier(); - imgElement.style.width = `${Math.round(scale * 100)}vw`; - imgElement.style.height = `${Math.round(scale * 100)}vh`; + // Apply pixel-based size using intrinsic dimensions and current scale + applyScaledPixelSize(imgElement); + const radius = `${settings.backgroundRadius}%`; + if (imgElement.style.borderRadius !== radius) imgElement.style.borderRadius = radius; // Performance mode optimizations if (settings.performanceMode) { From fa0a7b7f563f0f909a5d607c1d7b0adf4eae19be Mon Sep 17 00:00:00 2001 From: meowarex Date: Tue, 9 Sep 2025 20:58:28 +1000 Subject: [PATCH 09/10] Adjusted Cover Scale Settings --- plugins/radiant-lyrics-luna/src/Settings.tsx | 36 ++++----- plugins/radiant-lyrics-luna/src/index.ts | 82 ++++++++++++++------ 2 files changed, 77 insertions(+), 41 deletions(-) diff --git a/plugins/radiant-lyrics-luna/src/Settings.tsx b/plugins/radiant-lyrics-luna/src/Settings.tsx index 29f87d8..0b7a978 100644 --- a/plugins/radiant-lyrics-luna/src/Settings.tsx +++ b/plugins/radiant-lyrics-luna/src/Settings.tsx @@ -17,7 +17,7 @@ export const settings = await ReactiveStore.getPluginStorage("RadiantLyrics", { spinSpeed: 45, settingsAffectNowPlaying: true, backgroundScale: 15, - backgroundRadius: 0, + backgroundRadius: 25, }); export const Settings = () => { @@ -174,6 +174,21 @@ export const Settings = () => { }} /> { + setTextGlow((settings.textGlow = value)); + // Update variables immediately when setting changes + if ((window as any).updateRadiantLyricsTextGlow) { + (window as any).updateRadiantLyricsTextGlow(); + } + }} + /> + { /> { @@ -213,21 +228,6 @@ export const Settings = () => { } }} /> - { - setTextGlow((settings.textGlow = value)); - // Update variables immediately when setting changes - if ((window as any).updateRadiantLyricsTextGlow) { - (window as any).updateRadiantLyricsTextGlow(); - } - }} - /> { return value / 10; }; -// Helper to size an image using its intrinsic dimensions times scale (in pixels) -const applyScaledPixelSize = (img: HTMLImageElement | null): void => { - if (!img) return; - const scale = getScaledMultiplier(); - const apply = () => { - const w = img.naturalWidth; - const h = img.naturalHeight; - if (w > 0 && h > 0) { - const wPx = Math.round(w * scale); - const hPx = Math.round(h * scale); - const wStr = `${wPx}px`; - const hStr = `${hPx}px`; - if (img.style.width !== wStr) img.style.width = wStr; - if (img.style.height !== hStr) img.style.height = hStr; - } - }; - if (img.complete && img.naturalWidth > 0) { - apply(); - } else { - img.addEventListener("load", apply, { once: true }); - } -}; - // Import CSS files directly using Luna's file:// syntax - Took me a while to figure out <3 import baseStyles from "file://styles.css?minify"; import playerBarHidden from "file://player-bar-hidden.css?minify"; @@ -333,6 +310,7 @@ let globalSpinningBgStyleTag: StyleTag | null = null; let globalBackgroundContainer: HTMLElement | null = null; let globalBackgroundImage: HTMLImageElement | null = null; let globalBlackBg: HTMLElement | null = null; +let globalGradientOverlay: HTMLElement | null = null; let currentGlobalCoverSrc: string | null = null; let lastUpdateTime = 0; const getUpdateThrottle = () => (settings.performanceMode ? 1500 : 500); @@ -345,6 +323,29 @@ let nowPlayingGradientOverlay: HTMLElement | null = null; let currentNowPlayingCoverSrc: string | null = null; let spinAnimationAdded = false; +// apply scaled pixel sizes to cover art +const applyScaledPixelSize = (img: HTMLImageElement | null): void => { + if (!img) return; + const scale = getScaledMultiplier(); + const apply = () => { + const w = img.naturalWidth; + const h = img.naturalHeight; + if (w > 0 && h > 0) { + const wPx = Math.round(w * scale); + const hPx = Math.round(h * scale); + const wStr = `${wPx}px`; + const hStr = `${hPx}px`; + if (img.style.width !== wStr) img.style.width = wStr; + if (img.style.height !== hStr) img.style.height = hStr; + } + }; + if (img.complete && img.naturalWidth > 0) { + apply(); + } else { + img.addEventListener("load", apply, { once: true }); + } +}; + // Update Cover Art background for Now Playing and Global function updateCoverArtBackground(method: number = 0): void { if (method === 1) { @@ -607,6 +608,38 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => { transform-origin: center center; `; globalBackgroundContainer.appendChild(globalBackgroundImage); + + // Create gradient overlay + globalGradientOverlay = document.createElement("div"); + globalGradientOverlay.className = "global-spinning-gradient-overlay"; + globalGradientOverlay.style.cssText = ` + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at center, transparent 0%, rgba(0, 0, 0, 0.3) 60%, rgba(0, 0, 0, 0.8) 90%); + z-index: -1; + pointer-events: none; + `; + globalBackgroundContainer.appendChild(globalGradientOverlay); + } + + // Ensure gradient overlay exists even if container was pre-existing + if (!globalGradientOverlay && globalBackgroundContainer) { + globalGradientOverlay = document.createElement("div"); + globalGradientOverlay.className = "global-spinning-gradient-overlay"; + globalGradientOverlay.style.cssText = ` + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at center, transparent 0%, rgba(0, 0, 0, 0.3) 60%, rgba(0, 0, 0, 0.8) 90%); + z-index: -1; + pointer-events: none; + `; + globalBackgroundContainer.appendChild(globalGradientOverlay); } // Update image source efficiently (throttled) @@ -629,6 +662,7 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => { globalBackgroundImage.style.filter = `blur(${Math.min(settings.backgroundBlur, 20)}px) brightness(${settings.backgroundBrightness / 100}) contrast(${Math.min(settings.backgroundContrast, 150)}%)`; if (globalBackgroundImage.style.borderRadius !== radius) globalBackgroundImage.style.borderRadius = radius; + // Do not apply radius to vignette overlay; matches Now Playing behavior if (settings.spinningArtEnabled) { globalBackgroundImage.style.animation = `spinGlobal ${settings.spinSpeed}s linear infinite`; globalBackgroundImage.style.willChange = "transform"; @@ -642,6 +676,7 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => { globalBackgroundImage.style.filter = `blur(${settings.backgroundBlur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${settings.backgroundContrast}%)`; if (globalBackgroundImage.style.borderRadius !== radius) globalBackgroundImage.style.borderRadius = radius; + // Do not apply radius to vignette overlay; matches Now Playing behavior if (settings.spinningArtEnabled) { globalBackgroundImage.style.animation = `spinGlobal ${settings.spinSpeed}s linear infinite`; globalBackgroundImage.style.willChange = "transform"; @@ -662,6 +697,7 @@ const cleanUpGlobalSpinningBackground = function (): void { globalBackgroundContainer = null; globalBackgroundImage = null; globalBlackBg = null; + globalGradientOverlay = null; currentGlobalCoverSrc = null; if (globalSpinningBgStyleTag) { From b9a9588f9d70eee2d306d518628124241006e197 Mon Sep 17 00:00:00 2001 From: meowarex Date: Tue, 9 Sep 2025 21:09:08 +1000 Subject: [PATCH 10/10] Adjusted Default Settings --- plugins/radiant-lyrics-luna/src/Settings.tsx | 34 ++++++++++---------- plugins/radiant-lyrics-luna/src/index.ts | 22 ++++++------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/plugins/radiant-lyrics-luna/src/Settings.tsx b/plugins/radiant-lyrics-luna/src/Settings.tsx index 0b7a978..cbec929 100644 --- a/plugins/radiant-lyrics-luna/src/Settings.tsx +++ b/plugins/radiant-lyrics-luna/src/Settings.tsx @@ -3,21 +3,21 @@ import { LunaSettings, LunaSwitchSetting, LunaNumberSetting } from "@luna/ui"; import React from "react"; export const settings = await ReactiveStore.getPluginStorage("RadiantLyrics", { - hideUIEnabled: true, - trackTitleGlow: false, - playerBarVisible: false, lyricsGlowEnabled: true, - textGlow: 20, - spinningCoverEverywhere: true, + trackTitleGlow: false, + hideUIEnabled: true, + playerBarVisible: false, + CoverEverywhere: true, performanceMode: false, - spinningArtEnabled: true, + spinningArt: true, + textGlow: 20, + backgroundScale: 15, + backgroundRadius: 25, backgroundContrast: 120, backgroundBlur: 80, backgroundBrightness: 40, spinSpeed: 45, settingsAffectNowPlaying: true, - backgroundScale: 15, - backgroundRadius: 25, }); export const Settings = () => { @@ -31,14 +31,14 @@ export const Settings = () => { settings.lyricsGlowEnabled, ); const [textGlow, setTextGlow] = React.useState(settings.textGlow); - const [spinningCoverEverywhere, setSpinningCoverEverywhere] = React.useState( - settings.spinningCoverEverywhere, + const [CoverEverywhere, setCoverEverywhere] = React.useState( + settings.CoverEverywhere, ); const [performanceMode, setPerformanceMode] = React.useState( settings.performanceMode, ); - const [spinningArtEnabled, setSpinningArtEnabled] = React.useState( - settings.spinningArtEnabled, + const [spinningArt, setspinningArt] = React.useState( + settings.spinningArt, ); const [backgroundContrast, setBackgroundContrast] = React.useState( settings.backgroundContrast, @@ -121,14 +121,14 @@ export const Settings = () => { { console.log( "Spinning Cover Everywhere:", checked ? "enabled" : "disabled", ); - setSpinningCoverEverywhere( - (settings.spinningCoverEverywhere = checked), + setCoverEverywhere( + (settings.CoverEverywhere = checked), ); // Update styles immediately when setting changes if ((window as any).updateRadiantLyricsGlobalBackground) { @@ -155,13 +155,13 @@ export const Settings = () => { { console.log( "Background Cover Spin:", checked ? "enabled" : "disabled", ); - setSpinningArtEnabled((settings.spinningArtEnabled = checked)); + setspinningArt((settings.spinningArt = checked)); if ((window as any).updateRadiantLyricsGlobalBackground) { (window as any).updateRadiantLyricsGlobalBackground(); } diff --git a/plugins/radiant-lyrics-luna/src/index.ts b/plugins/radiant-lyrics-luna/src/index.ts index 9a7ea44..424088d 100644 --- a/plugins/radiant-lyrics-luna/src/index.ts +++ b/plugins/radiant-lyrics-luna/src/index.ts @@ -387,7 +387,7 @@ function updateCoverArtBackground(method: number = 0): void { // Update backgrounds when we have a valid cover art source if (coverArtImageSrc) { // Apply global spinning background if enabled - if (settings.spinningCoverEverywhere) { + if (settings.CoverEverywhere) { applyGlobalSpinningBackground(coverArtImageSrc); } @@ -495,10 +495,10 @@ function updateCoverArtBackground(method: number = 0): void { const filt = `blur(${blur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${contrast}%)`; if (nowPlayingBackgroundImage.style.filter !== filt) nowPlayingBackgroundImage.style.filter = filt; - const anim = settings.spinningArtEnabled + const anim = settings.spinningArt ? `spin ${settings.spinSpeed}s linear infinite` : "none"; - const wc = settings.spinningArtEnabled ? "transform" : "auto"; + const wc = settings.spinningArt ? "transform" : "auto"; if (nowPlayingBackgroundImage.style.animation !== anim) nowPlayingBackgroundImage.style.animation = anim; if (nowPlayingBackgroundImage.style.willChange !== wc) @@ -512,10 +512,10 @@ function updateCoverArtBackground(method: number = 0): void { const filt = `blur(${settings.backgroundBlur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${settings.backgroundContrast}%)`; if (nowPlayingBackgroundImage.style.filter !== filt) nowPlayingBackgroundImage.style.filter = filt; - const anim = settings.spinningArtEnabled + const anim = settings.spinningArt ? `spin ${settings.spinSpeed}s linear infinite` : "none"; - const wc = settings.spinningArtEnabled ? "transform" : "auto"; + const wc = settings.spinningArt ? "transform" : "auto"; if (nowPlayingBackgroundImage.style.animation !== anim) nowPlayingBackgroundImage.style.animation = anim; if (nowPlayingBackgroundImage.style.willChange !== wc) @@ -547,7 +547,7 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => { '[data-test="main"]', ) as HTMLElement; - if (!settings.spinningCoverEverywhere) { + if (!settings.CoverEverywhere) { cleanUpGlobalSpinningBackground(); return; } @@ -663,7 +663,7 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => { if (globalBackgroundImage.style.borderRadius !== radius) globalBackgroundImage.style.borderRadius = radius; // Do not apply radius to vignette overlay; matches Now Playing behavior - if (settings.spinningArtEnabled) { + if (settings.spinningArt) { globalBackgroundImage.style.animation = `spinGlobal ${settings.spinSpeed}s linear infinite`; globalBackgroundImage.style.willChange = "transform"; } else { @@ -677,7 +677,7 @@ const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => { if (globalBackgroundImage.style.borderRadius !== radius) globalBackgroundImage.style.borderRadius = radius; // Do not apply radius to vignette overlay; matches Now Playing behavior - if (settings.spinningArtEnabled) { + if (settings.spinningArt) { globalBackgroundImage.style.animation = `spinGlobal ${settings.spinSpeed}s linear infinite`; globalBackgroundImage.style.willChange = "transform"; } else { @@ -715,7 +715,7 @@ const updateRadiantLyricsGlobalBackground = function (): void { document.body.classList.remove("performance-mode"); } - if (settings.spinningCoverEverywhere) { + if (settings.CoverEverywhere) { // Get current cover art and apply global background updateCoverArtBackground(); } else { @@ -761,7 +761,7 @@ const updateRadiantLyricsNowPlayingBackground = function (): void { // Reduce blur and effects for better performance, but keep spinning blur = Math.min(blur, 20); contrast = Math.min(contrast, 150); - if (settings.spinningArtEnabled) { + if (settings.spinningArt) { imgElement.style.animation = `spin ${spinSpeed}s linear infinite`; imgElement.style.willChange = "transform"; } else { @@ -770,7 +770,7 @@ const updateRadiantLyricsNowPlayingBackground = function (): void { } imgElement.classList.remove("performance-mode-static"); } else { - if (settings.spinningArtEnabled) { + if (settings.spinningArt) { imgElement.style.animation = `spin ${spinSpeed}s linear infinite`; imgElement.style.willChange = "transform"; } else {