48 Commits

Author SHA1 Message Date
Meow Meow 50c40e6978 Merge pull request #90 from meowarex/dev
Prevent New UI
2026-03-25 15:07:40 +11:00
Meow Meow ec42f5c287 Merge pull request #89 from meowarex/dev
Platform Param (DX Logs)
2026-03-10 21:00:47 +11:00
Meow Meow db6310cef4 Merge pull request #88 from meowarex/dev
Fix Race Condition
2026-03-02 14:34:39 +11:00
Meow Meow ee2243443e Merge pull request #87 from meowarex/dev
Better Glow Cutoff Patch | Thx Aya <3
2026-02-28 22:34:14 +11:00
Meow Meow 2deda8aed1 Merge pull request #86 from meowarex/dev
Hotfix #7000...
2026-02-28 21:34:21 +11:00
Meow Meow b403c3a80c Merge pull request #85 from meowarex/dev
Patched Context Menus.. Again..
2026-02-28 21:20:14 +11:00
Meow Meow 48c8738bcd Merge pull request #84 from meowarex/dev
Quick Hotfix
2026-02-28 20:58:43 +11:00
Meow Meow 7627bd7051 Merge pull request #83 from meowarex/dev
Fixed Context Menus & other things
2026-02-28 19:54:29 +11:00
Meow Meow 20a2c2b7f7 Merge pull request #82 from meowarex/dev
Add Lyrics to Tracks, Tidal doesn't have <3
2026-02-28 19:12:32 +11:00
Meow Meow f0139165a9 Merge pull request #81 from meowarex/dev
Romanize ALL Tracks <3
2026-02-28 16:45:57 +11:00
Meow Meow e4df0a8c64 Merge pull request #80 from meowarex/dev
Apply Effects to ALL Tracks!
2026-02-28 16:04:57 +11:00
Meow Meow 8ee9717f25 Merge pull request #79 from meowarex/dev
Added Romanized Lyrics
2026-02-27 19:21:00 +11:00
Meow Meow 5ead825b3d Merge pull request #78 from meowarex/dev
Fixed Mini Cover Art Padding
2026-02-25 21:38:12 +11:00
Meow Meow 1a2e25c717 Merge pull request #77 from meowarex/dev
Added Lyric Font Size
2026-02-25 21:09:31 +11:00
Meow Meow a2cb822a2c Merge pull request #76 from meowarex/dev
Fixed Colorama lyrics
2026-02-25 20:41:53 +11:00
Meow Meow e223f933c6 Merge pull request #75 from meowarex/dev
Reduced 404 Spams
2026-02-25 17:32:22 +11:00
Meow Meow 031bb107f8 Merge pull request #74 from meowarex/dev
Apply Context Aware & Bubbled lyrics to Line
2026-02-24 23:38:58 +11:00
Meow Meow a6371240ef Merge pull request #73 from meowarex/dev
Update Gitignore <3
2026-02-24 23:28:49 +11:00
Meow Meow 92697d7396 Merge pull request #72 from meowarex/dev
CodeReview
2026-02-24 23:26:27 +11:00
Meow Meow 5e6e897395 Merge pull request #71 from meowarex/dev
Context Aware Lyrics & Aniamtions (Line)
2026-02-24 23:09:59 +11:00
Meow Meow 4749f50b95 Merge pull request #70 from meowarex/dev
ISRC Support
2026-02-24 15:32:51 +11:00
Meow Meow b48d248cda Merge pull request #69 from meowarex/dev
WIP Animations
2026-02-21 03:55:36 +11:00
Meow Meow 4af872133e Merge pull request #68 from meowarex/dev
REMIX Detection
2026-02-21 01:07:26 +11:00
Meow Meow 0f9d5a75d8 Merge pull request #67 from meowarex/dev
Massive Cleanup + Logging Easter Egg
2026-02-20 23:54:38 +11:00
Meow Meow 764cb1aa96 Merge pull request #66 from meowarex/dev
Syllable Lyrics <3
2026-02-20 23:06:14 +11:00
Meow Meow e062b4bd02 Merge pull request #65 from meowarex/dev
Rewrite Timeouts + Bug Fixes
2026-02-20 15:31:55 +11:00
Meow Meow 9f01ecd1ff Merge pull request #64 from meowarex/dev
WBW + Observer Refactor + Prep for Syllables
2026-02-19 23:54:52 +11:00
Meow Meow e59121968d Merge pull request #63 from meowarex/dev
Added Quality Matched Seeker Color
2026-02-13 14:49:20 +11:00
Meow Meow 8fbb48f8fe Merge pull request #62 from meowarex/dev
Deprecated Obsidian | Merged into RL <3
2026-02-11 21:07:54 +11:00
Meow Meow b351fa859a Merge pull request #61 from meowarex/dev
Merged Obsidian into RL + Added Conditional Settings Visability
2026-02-11 20:56:54 +11:00
Meow Meow 353b72e1e1 Merge pull request #60 from meowarex/dev
Updated Sticky Lyrics Default
2026-02-09 22:59:32 +11:00
Meow Meow c648f3df95 Merge pull request #59 from meowarex/dev
Added Sticky Lyrics
2026-02-09 22:57:46 +11:00
Meow Meow e376fb745b Merge pull request #58 from meowarex/dev
Removed Small Header BG
2026-02-09 19:40:13 +11:00
Meow Meow ca085ce31b Merge pull request #57 from meowarex/dev
Fix Tidals New Sticky Header
2026-02-08 00:36:36 +11:00
Meow Meow 34e0a51bcd Merge pull request #56 from meowarex/dev
Fixed Media Table Border + Search Bar Width
2026-01-15 21:27:50 +11:00
Meow Meow 8fdfff10e7 Merge pull request #55 from meowarex/dev
HideUI Now hides header bar (Minimize, Fullscreen & Search)
2025-12-30 14:54:25 +11:00
Meow Meow fbd0c2b696 Merge pull request #54 from meowarex/dev
Fixed HideUI & Removed Animations :(
2025-12-30 14:15:40 +11:00
Meow Meow 4ad4b5879c Merge pull request #53 from meowarex/dev
Updated for Sidebar 3.0 + Fixed Image Radius + Fixed Header Clipping
2025-12-30 13:44:05 +11:00
Meow Meow 764c71b45f Merge pull request #52 from meowarex/dev
Renamed oled-theme to obsidian
2025-09-09 21:39:28 +10:00
Meow Meow 1876a37185 Merge pull request #51 from meowarex/dev
Background Scale & Radius
2025-09-09 21:10:32 +10:00
Meow Meow 8c27eebd88 Fix Dependencies 2025-08-17 14:52:32 +10:00
Meow Meow 9fd8208996 Merge pull request #50 from meowarex/dev
Fixed Title Glow Persistence
2025-08-14 21:40:51 +10:00
Meow Meow a1ddb0ede6 Merge pull request #49 from meowarex/dev
Improved Settings + Labeling
2025-08-13 21:33:05 +10:00
Meow Meow 411e20b9f7 Merge pull request #48 from meowarex/dev
Colorama-Lyrics Plugin + Title Glow Setting
2025-08-12 23:45:22 +10:00
Meow Meow 50215fa0f5 Merge pull request #47 from meowarex/dev
Adjust Lyric Glow Setting
2025-08-12 22:30:30 +10:00
Meow Meow 5761c01973 Merge pull request #46 from meowarex/dev
Performance Overhaul
2025-08-12 22:03:22 +10:00
Meow Meow 62e15b0d3d Merge pull request #45 from meowarex/dev
WIP | Element Hider Plugin
2025-06-12 05:09:27 +10:00
Meow Meow 13cbe01bd8 Merge pull request #44 from meowarex/dev
Use Positive Logic for Cover Spin Setting
2025-06-11 23:20:15 +10:00
12 changed files with 1875 additions and 1309 deletions
-3
View File
@@ -18,8 +18,5 @@
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"tsx": "^4.19.4", "tsx": "^4.19.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"
},
"dependencies": {
"pnpm": "^10.14.0"
} }
} }
+148 -126
View File
@@ -7,11 +7,17 @@ import visualizerStyles from "file://styles.css?minify";
export const { trace } = Tracer("[Audio Visualizer]"); export const { trace } = Tracer("[Audio Visualizer]");
// Helper function for consistent logging
const log = (message: string) => console.log(`[Audio Visualizer] ${message}`); const log = (message: string) => console.log(`[Audio Visualizer] ${message}`);
const warn = (message: string) => console.warn(`[Audio Visualizer] ${message}`);
const error = (message: string) =>
console.error(`[Audio Visualizer] ${message}`);
export { Settings }; export { Settings };
// Basic config with settings
const config = { const config = {
enabled: true, enabled: true,
position: "left" as "left" | "right",
width: 200, width: 200,
height: 40, height: 40,
get barCount() { get barCount() {
@@ -25,6 +31,7 @@ const config = {
}, },
sensitivity: 1.5, sensitivity: 1.5,
smoothing: 0.8, smoothing: 0.8,
visualizerType: "bars" as "bars" | "waveform" | "circular",
}; };
// Clean up resources // Clean up resources
@@ -42,15 +49,10 @@ let animationId: number | null = null;
let currentAudioElement: HTMLAudioElement | null = null; let currentAudioElement: HTMLAudioElement | null = null;
let isSourceConnected: boolean = false; let isSourceConnected: boolean = false;
// Each placement gets its own container/canvas/context // Canvas and container elements
interface VisualizerSlot { let visualizerContainer: HTMLDivElement | null = null;
container: HTMLDivElement | null; let canvas: HTMLCanvasElement | null = null;
canvas: HTMLCanvasElement | null; let canvasContext: CanvasRenderingContext2D | null = null;
ctx: CanvasRenderingContext2D | null;
}
const navSlot: VisualizerSlot = { container: null, canvas: null, ctx: null };
const npSlot: VisualizerSlot = { container: null, canvas: null, ctx: null };
// Find the audio element - this is a bit of a hack but it works // Find the audio element - this is a bit of a hack but it works
const findAudioElement = (): HTMLAudioElement | null => { const findAudioElement = (): HTMLAudioElement | null => {
@@ -138,7 +140,10 @@ const initializeAudioVisualizer = async (): Promise<void> => {
audioContext.resume().catch(() => {}); // Fire and forget audioContext.resume().catch(() => {}); // Fire and forget
} }
createVisualizerUI(); // Create UI only if it doesn't exist
if (!visualizerContainer) {
createVisualizerUI();
}
// Start animation only if not already running // Start animation only if not already running
if (!animationId) { if (!animationId) {
@@ -150,116 +155,120 @@ const initializeAudioVisualizer = async (): Promise<void> => {
} }
}; };
const makeSlotElements = (): { container: HTMLDivElement; canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } | null => { // Create the visualizer UI container and canvas
const container = document.createElement("div");
container.className = "audio-visualizer-container";
container.style.cssText = `
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 4px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
`;
const cvs = document.createElement("canvas");
cvs.width = config.width;
cvs.height = config.height;
cvs.style.cssText = `
width: ${config.width}px;
height: ${config.height}px;
border-radius: 4px;
`;
container.appendChild(cvs);
const ctx = cvs.getContext("2d");
if (!ctx) return null;
return { container, canvas: cvs, ctx };
};
const clearSlot = (slot: VisualizerSlot): void => {
slot.container?.remove();
slot.container = null;
slot.canvas = null;
slot.ctx = null;
};
const ensureNavSlot = (): void => {
if (navSlot.container?.isConnected) return;
clearSlot(navSlot);
const searchField = document.querySelector('input[class*="_searchField"]') as HTMLInputElement;
if (!searchField) return;
const searchContainer = searchField.parentElement;
if (!searchContainer?.parentElement) return;
const els = makeSlotElements();
if (!els) return;
els.container.style.marginRight = "12px";
Object.assign(navSlot, els);
searchContainer.parentElement.insertBefore(els.container, searchContainer);
};
const ensureNpSlot = (): void => {
if (npSlot.container?.isConnected) return;
clearSlot(npSlot);
const artistInfo = document.querySelector('[data-test="artist-info"]');
if (!artistInfo) return;
const leftContent = artistInfo.parentElement;
if (!leftContent) return;
const els = makeSlotElements();
if (!els) return;
els.container.style.marginLeft = "12px";
Object.assign(npSlot, els);
leftContent.insertBefore(els.container, artistInfo.nextSibling);
};
const createVisualizerUI = (): void => { const createVisualizerUI = (): void => {
// Remove existing visualizer if it exists
removeVisualizerUI();
if (!config.enabled) return; if (!config.enabled) return;
ensureNavSlot();
ensureNpSlot(); // Find the search bar
const searchField = document.querySelector(
'input[class*="_searchField"]',
) as HTMLInputElement;
if (!searchField) {
warn("Search field not found");
return;
}
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;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 4px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
`;
// 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");
// 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 => { const removeVisualizerUI = (): void => {
clearSlot(navSlot); if (visualizerContainer) {
clearSlot(npSlot); visualizerContainer.remove();
visualizerContainer = null;
canvas = null;
canvasContext = null;
}
}; };
// Animation loop for rendering visualizer // Animation loop for rendering visualizer
const animate = (): void => { const animate = (): void => {
// Re-attach slots that got disconnected from the DOM if (!canvasContext || !canvas) {
createVisualizerUI(); animationId = null;
const slots = [navSlot, npSlot].filter(s => s.ctx && s.canvas);
if (slots.length === 0) {
animationId = requestAnimationFrame(animate);
return; return;
} }
// 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; let hasRealAudio = false;
if (analyser && dataArray) { if (analyser && dataArray) {
analyser.getByteFrequencyData(dataArray); analyser.getByteFrequencyData(dataArray);
// Check if there's actual audio signal (not just silence)
const avgVolume = const avgVolume =
dataArray.reduce((sum, val) => sum + val, 0) / dataArray.length; dataArray.reduce((sum, val) => sum + val, 0) / dataArray.length;
hasRealAudio = avgVolume > 5; hasRealAudio = avgVolume > 5; // Threshold for detecting actual audio
} }
for (const slot of slots) { // Clear canvas
const ctx = slot.ctx!; canvasContext.clearRect(0, 0, canvas.width, canvas.height);
const cvs = slot.canvas!;
ctx.fillStyle = config.color;
ctx.strokeStyle = config.color;
ctx.clearRect(0, 0, cvs.width, cvs.height);
if (hasRealAudio && analyser && dataArray) { if (hasRealAudio && analyser && dataArray) {
drawBars(ctx, cvs); // Draw real audio visualization
} else { switch (config.visualizerType) {
drawScrollingWave(ctx, cvs); 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);
@@ -282,54 +291,67 @@ const drawRoundedRect = (
ctx.fill(); ctx.fill();
}; };
const drawScrollingWave = (ctx: CanvasRenderingContext2D, cvs: HTMLCanvasElement): void => { // Draw scrolling wave effect when no audio is detected
waveTime += 0.05 / [navSlot, npSlot].filter(s => s.ctx).length; const drawScrollingWave = (): void => {
if (!canvasContext || !canvas) return;
waveTime += 0.05; // Speed of wave animation
const barCount = config.barCount; const barCount = config.barCount;
const barWidth = cvs.width / barCount; const barWidth = canvas.width / barCount;
const maxHeight = cvs.height * 0.6; const maxHeight = canvas.height * 0.6;
ctx.fillStyle = config.color; canvasContext.fillStyle = config.color;
for (let i = 0; i < barCount; i++) { for (let i = 0; i < barCount; i++) {
// Create a sine wave that scrolls back and forth
const x = i / barCount; const x = i / barCount;
const wave1 = Math.sin(x * Math.PI * 2 + waveTime) * 0.3; const wave1 = Math.sin(x * Math.PI * 2 + waveTime) * 0.3;
const wave2 = Math.sin(x * Math.PI * 4 + waveTime * 1.3) * 0.2; const wave2 = Math.sin(x * Math.PI * 4 + waveTime * 1.3) * 0.2;
const wave3 = Math.sin(x * Math.PI * 6 + waveTime * 0.7) * 0.1; const wave3 = Math.sin(x * Math.PI * 6 + waveTime * 0.7) * 0.1;
const combinedWave = (wave1 + wave2 + wave3 + 1) / 2;
// 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; const travelWave = Math.sin(x * Math.PI * 3 - waveTime * 2) * 0.5 + 0.5;
const barHeight = maxHeight * combinedWave * travelWave * 0.8 + 2;
// Final height calculation
const barHeight = maxHeight * combinedWave * travelWave * 0.8 + 2; // Minimum height of 2px
const xPos = i * barWidth; const xPos = i * barWidth;
const yPos = (cvs.height - barHeight) / 2; const yPos = (canvas.height - barHeight) / 2;
// Draw rounded or square bars based on setting
if (config.barRounding) { if (config.barRounding) {
drawRoundedRect(ctx, xPos, yPos, barWidth - 1, barHeight, 2); drawRoundedRect(canvasContext, xPos, yPos, barWidth - 1, barHeight, 2);
} else { } else {
ctx.fillRect(xPos, yPos, barWidth - 1, barHeight); canvasContext.fillRect(xPos, yPos, barWidth - 1, barHeight);
} }
} }
}; };
const drawBars = (ctx: CanvasRenderingContext2D, cvs: HTMLCanvasElement): void => { // Draw frequency bars - default
if (!dataArray) return; const drawBars = (): void => {
if (!canvasContext || !dataArray || !canvas) return;
const barWidth = cvs.width / config.barCount; const barWidth = canvas.width / config.barCount;
const heightScale = cvs.height / 255; const heightScale = canvas.height / 255;
ctx.fillStyle = config.color; canvasContext.fillStyle = config.color;
for (let i = 0; i < config.barCount; i++) { for (let i = 0; i < config.barCount; i++) {
const dataIndex = Math.floor(i * (dataArray.length / config.barCount)); const dataIndex = Math.floor(i * (dataArray.length / config.barCount));
const barHeight = dataArray[dataIndex] * config.sensitivity * heightScale; const barHeight = dataArray[dataIndex] * config.sensitivity * heightScale;
const x = i * barWidth; const x = i * barWidth;
const y = cvs.height - barHeight; const y = canvas.height - barHeight;
// Draw rounded or square bars based on setting
if (config.barRounding) { if (config.barRounding) {
drawRoundedRect(ctx, x, y, barWidth - 1, barHeight, 2); drawRoundedRect(canvasContext, x, y, barWidth - 1, barHeight, 2);
} else { } else {
ctx.fillRect(x, y, barWidth - 1, barHeight); canvasContext.fillRect(x, y, barWidth - 1, barHeight);
} }
} }
}; };
@@ -390,23 +412,23 @@ const drawBars = (ctx: CanvasRenderingContext2D, cvs: HTMLCanvasElement): void =
// } // }
// }; // };
// Update visualizer settings
const updateAudioVisualizer = (): void => { const updateAudioVisualizer = (): void => {
if (analyser) { if (analyser) {
analyser.fftSize = 512; // use a fixed size that provides enough frequency bins
analyser.fftSize = 512; // Fixed power of 2 - important
analyser.smoothingTimeConstant = config.smoothing; analyser.smoothingTimeConstant = config.smoothing;
dataArray = new Uint8Array(analyser.frequencyBinCount); dataArray = new Uint8Array(analyser.frequencyBinCount);
} }
for (const slot of [navSlot, npSlot]) { if (canvas) {
if (slot.canvas) { canvas.width = config.width;
slot.canvas.width = config.width; canvas.height = config.height;
slot.canvas.height = config.height; canvas.style.width = `${config.width}px`;
slot.canvas.style.width = `${config.width}px`; canvas.style.height = `${config.height}px`;
slot.canvas.style.height = `${config.height}px`;
}
} }
removeVisualizerUI(); // Recreate UI if position changed
createVisualizerUI(); createVisualizerUI();
}; };
+21 -11
View File
@@ -1,40 +1,50 @@
/* Audio Visualizer CSS */ /* Audio Visualizer CSS - Only applies to the Visualizer */
.audio-visualizer-container { #audio-visualizer-container {
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
animation: av-fadeIn 0.5s ease-out;
} }
.audio-visualizer-container:hover { #audio-visualizer-container:hover {
transform: scale(1.02); transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
} }
.audio-visualizer-container canvas { #audio-visualizer-container canvas {
display: block; display: block;
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
} }
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
.audio-visualizer-container { #audio-visualizer-container {
margin: 4px; margin: 4px;
padding: 2px; padding: 2px;
} }
.audio-visualizer-container canvas { #audio-visualizer-container canvas {
max-width: 150px; max-width: 150px;
max-height: 30px; max-height: 30px;
} }
} }
.audio-visualizer-container.active { /* Where to put the thingy */
[class*="_searchField"] {
transition: all 0.3s ease-in-out;
}
[data-type="search-field"] {
min-width: 220px !important;
}
/* 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);
} }
@keyframes av-fadeIn { /* Fade in animation */
@keyframes fadeIn {
from { from {
opacity: 0; opacity: 0;
transform: scale(0.8); transform: scale(0.8);
@@ -45,6 +55,6 @@
} }
} }
[data-type="search-field"] { #audio-visualizer-container {
min-width: 220px !important; animation: fadeIn 0.5s ease-out;
} }
+539 -104
View File
@@ -8,24 +8,53 @@ declare global {
} }
} }
// Define a typed onChange signature for the switch
type SwitchChangeHandler = ( type SwitchChangeHandler = (
event: React.ChangeEvent<HTMLInputElement> | null, event: React.ChangeEvent<HTMLInputElement> | null,
checked: boolean, checked: boolean,
) => void; ) => void;
export type ColoramaMode =
| "single"
| "gradient-experimental"
| "cover"
| "cover-gradient";
export const settings = await ReactiveStore.getPluginStorage("ColoramaLyrics", { export const settings = await ReactiveStore.getPluginStorage("ColoramaLyrics", {
enabled: true, enabled: true,
mode: "single" as ColoramaMode,
// Store colors as RGB hex (#RRGGBB) and opacity separately (0-100)
singleColor: "#FFFFFF", singleColor: "#FFFFFF",
singleAlpha: 100, singleAlpha: 100,
gradientStart: "#FFFFFF",
gradientStartAlpha: 100,
gradientEnd: "#AAFFFF",
gradientEndAlpha: 100,
gradientAngle: 0,
customColors: [] as string[], customColors: [] as string[],
excludeInactive: false, excludeInactive: false,
}); });
export const Settings = () => { export const Settings = () => {
// const [enabled, setEnabled] = React.useState(settings.enabled);
const [mode, setMode] = React.useState<ColoramaMode>(settings.mode);
const [singleColor, setSingleColor] = React.useState(settings.singleColor); const [singleColor, setSingleColor] = React.useState(settings.singleColor);
const [singleAlpha, setSingleAlpha] = React.useState<number>( const [singleAlpha, setSingleAlpha] = React.useState<number>(
settings.singleAlpha ?? 100, settings.singleAlpha ?? 100,
); );
const [gradientStart, setGradientStart] = React.useState(
settings.gradientStart,
);
const [gradientStartAlpha, setGradientStartAlpha] = React.useState<number>(
settings.gradientStartAlpha ?? 100,
);
const [gradientEnd, setGradientEnd] = React.useState(settings.gradientEnd);
const [gradientEndAlpha, setGradientEndAlpha] = React.useState<number>(
settings.gradientEndAlpha ?? 100,
);
const [gradientAngle, setGradientAngle] = React.useState(
settings.gradientAngle,
);
const [customInput, setCustomInput] = React.useState(settings.singleColor); const [customInput, setCustomInput] = React.useState(settings.singleColor);
const [customColors, setCustomColors] = React.useState(settings.customColors); const [customColors, setCustomColors] = React.useState(settings.customColors);
const [showPicker, setShowPicker] = React.useState(false); const [showPicker, setShowPicker] = React.useState(false);
@@ -34,6 +63,9 @@ export const Settings = () => {
const [excludeInactive, setExcludeInactive] = React.useState( const [excludeInactive, setExcludeInactive] = React.useState(
settings.excludeInactive, settings.excludeInactive,
); );
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<{
title: string; title: string;
desc?: string; desc?: string;
@@ -41,23 +73,28 @@ export const Settings = () => {
onChange: SwitchChangeHandler; onChange: SwitchChangeHandler;
}>; }>;
// Helper for HEX normalization
const normalizeToRGB = ( const normalizeToRGB = (
hex: string, hex: string,
fallback: string = "#FFFFFF", fallback: string = "#FFFFFF",
): string => { ): string => {
let v = hex.trim().toLowerCase(); let v = hex.trim().toLowerCase();
if (!v.startsWith("#")) v = `#${v}`; if (!v.startsWith("#")) v = `#${v}`;
// #rgb or #rgba -> expand
if (/^#([0-9a-f]{3,4})$/.test(v)) { if (/^#([0-9a-f]{3,4})$/.test(v)) {
const m = v.slice(1); const m = v.slice(1);
const r = m[0]; const r = m[0];
const g = m[1]; const g = m[1];
const b = m[2]; const b = m[2];
// ignore alpha if provided (#rgba)
return `#${r}${r}${g}${g}${b}${b}`.toUpperCase(); return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
} }
// #aarrggbb -> strip alpha
if (/^#([0-9a-f]{8})$/.test(v)) { if (/^#([0-9a-f]{8})$/.test(v)) {
const rrggbb = v.slice(3); const rrggbb = v.slice(3);
return `#${rrggbb}`.toUpperCase(); return `#${rrggbb}`.toUpperCase();
} }
// #rrggbb
if (/^#([0-9a-f]{6})$/.test(v)) return v.toUpperCase(); if (/^#([0-9a-f]{6})$/.test(v)) return v.toUpperCase();
return fallback; return fallback;
}; };
@@ -84,7 +121,8 @@ export const Settings = () => {
"#1976D2", "#1976D2",
]; ];
const openPicker = () => { const openPicker = (endpoint: "single" | "start" | "end" = "single") => {
setActiveEndpoint(endpoint);
setShowPicker(true); setShowPicker(true);
setShouldRender(true); setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10); setTimeout(() => setIsAnimatingIn(true), 10);
@@ -102,10 +140,22 @@ export const Settings = () => {
const applyCustomInputColor = (raw: string, updateInput: boolean): void => { const applyCustomInputColor = (raw: string, updateInput: boolean): void => {
const trimmed = raw.trim(); const trimmed = raw.trim();
if (!hexColorRegex.test(trimmed)) return; if (!hexColorRegex.test(trimmed)) return;
const next = normalizeToRGB(trimmed); if (mode === "single") {
settings.singleColor = next; const next = normalizeToRGB(trimmed);
setSingleColor(next); settings.singleColor = next;
if (updateInput) setCustomInput(next); setSingleColor(next);
if (updateInput) setCustomInput(next);
} else if (mode === "gradient-experimental") {
const next = normalizeToRGB(trimmed);
if (activeEndpoint === "end") {
settings.gradientEnd = next;
setGradientEnd(next);
} else {
settings.gradientStart = next;
setGradientStart(next);
}
if (updateInput) setCustomInput(next);
}
requestApply(); requestApply();
}; };
@@ -122,6 +172,12 @@ export const Settings = () => {
} }
}; };
// 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 = () => { const requestApply = () => {
@@ -130,11 +186,66 @@ export const Settings = () => {
return ( return (
<LunaSettings> <LunaSettings>
{/* Single color picker button */} {/* Mode selection via dropdown (aligned right) */}
<div <div
style={{ style={{
padding: "8px 0", padding: "8px 0",
display: "flex", display: "flex",
alignItems: "center",
gap: 12,
}}
>
<div style={{ display: "flex", flexDirection: "column" }}>
<div style={{ fontWeight: "normal", fontSize: "1.075rem" }}>Mode</div>
<div style={{ opacity: 0.7, fontSize: 14 }}>
Choose how lyrics are colored
</div>
</div>
<select
value={mode}
onChange={(e) => {
const next = e.target.value as ColoramaMode;
settings.mode = next;
setMode(next);
requestApply();
}}
style={{
padding: "6px 10px",
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.08)",
color: "#fff",
cursor: "pointer",
marginLeft: "auto",
minWidth: 180,
}}
>
<option value="single" style={{ color: "#000", background: "#fff" }}>
Single
</option>
<option
value="gradient-experimental"
style={{ color: "#000", background: "#fff" }}
>
Gradient - Experimental
</option>
<option value="cover" style={{ color: "#000", background: "#fff" }}>
Cover - Experimental
</option>
<option
value="cover-gradient"
style={{ color: "#000", background: "#fff" }}
>
Cover (Gradient) - Experimental
</option>
</select>
</div>
{/* Single color */}
<div
style={{
padding: "8px 0",
display: mode === "single" ? "flex" : "none",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
}} }}
@@ -161,7 +272,7 @@ export const Settings = () => {
> >
<button <button
type="button" type="button"
onClick={() => (showPicker ? closePicker() : openPicker())} onClick={() => (showPicker ? closePicker() : openPicker("single"))}
style={{ style={{
width: 32, width: 32,
height: 32, height: 32,
@@ -174,7 +285,84 @@ export const Settings = () => {
</div> </div>
</div> </div>
{/* Color picker modal */} {/* Gradient controls (open picker) */}
<div
style={{
padding: "8px 0",
display: mode === "gradient-experimental" ? "flex" : "none",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div>
<div
style={{
fontWeight: "normal",
fontSize: "1.075rem",
marginBottom: 4,
}}
>
Gradient (Experimental)
</div>
<div style={{ opacity: 0.7, fontSize: 14 }}>Set colors & angle</div>
</div>
<button
type="button"
onClick={() => {
setCustomInput(gradientStart);
openPicker("start");
}}
style={{
padding: "8px 12px",
borderRadius: 8,
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.08)",
color: "#fff",
cursor: "pointer",
}}
>
Configure
</button>
</div>
{/* Cover gradient controls (open picker for angle) */}
<div
style={{
padding: "8px 0",
display: mode === "cover-gradient" ? "flex" : "none",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div>
<div
style={{
fontWeight: "normal",
fontSize: "1.075rem",
marginBottom: 4,
}}
>
Cover (Gradient) - Experimental
</div>
<div style={{ opacity: 0.7, fontSize: 14 }}>Set angle</div>
</div>
<button
type="button"
onClick={() => openPicker("start")}
style={{
padding: "8px 12px",
borderRadius: 8,
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.08)",
color: "#fff",
cursor: "pointer",
}}
>
Configure
</button>
</div>
{/* Modal for picking and managing colors (reused) */}
{shouldRender && ( {shouldRender && (
<> <>
<button <button
@@ -227,122 +415,369 @@ export const Settings = () => {
fontSize: 14, fontSize: 14,
}} }}
> >
Lyrics Color {mode === "single" ? "Single Color" : "Gradient Colors"}
</div> </div>
<div {mode === "gradient-experimental" && (
style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: 8,
marginBottom: 16,
}}
>
{allColors.map((color) => (
<button
key={color}
type="button"
onClick={() => {
const next = normalizeToRGB(color);
settings.singleColor = next;
setSingleColor(next);
setCustomInput(next);
requestApply();
}}
style={{
width: 32,
height: 32,
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.2)",
background: normalizeToRGB(color),
cursor: "pointer",
}}
/>
))}
</div>
<div style={{ marginBottom: 12 }}>
<div <div
style={{ style={{
color: "rgba(255,255,255,0.7)", display: "flex",
fontSize: 12, gap: 8,
marginBottom: 6, alignItems: "center",
marginBottom: 12,
}} }}
> >
Custom Hex (#RRGGBB) <div style={{ color: "rgba(255,255,255,0.7)", fontSize: 12 }}>
</div> Editing
<div style={{ display: "flex", gap: 8, alignItems: "center" }}> </div>
<input
type="text"
value={customInput}
onChange={(e) => 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",
}}
/>
<button <button
onClick={() => { onClick={() => {
applyCustomInputColor(customInput, false); setActiveEndpoint("start");
addCustomColor(); setCustomInput(gradientStart);
}} }}
style={{ style={{
width: 32,
height: 32,
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.3)",
background: "rgba(255,255,255,0.15)",
color: "#fff",
cursor: "pointer",
fontSize: 16,
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", gap: 8,
transition: "all 0.2s ease", padding: "6px 10px",
borderRadius: 8,
border:
activeEndpoint === "start"
? "1px solid rgba(255,255,255,0.5)"
: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.05)",
color: "#fff",
cursor: "pointer",
}} }}
type="button" type="button"
> >
+ <span
style={{
width: 14,
height: 14,
borderRadius: 3,
background: normalizeToRGB(gradientStart),
border: "1px solid rgba(255,255,255,0.3)",
}}
/>
<span style={{ fontSize: 12 }}>Start</span>
</button>
<button
type="button"
onClick={() => {
setActiveEndpoint("end");
setCustomInput(gradientEnd);
}}
style={{
display: "flex",
alignItems: "center",
gap: 8,
padding: "6px 10px",
borderRadius: 8,
border:
activeEndpoint === "end"
? "1px solid rgba(255,255,255,0.5)"
: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.05)",
color: "#fff",
cursor: "pointer",
}}
>
<span
style={{
width: 14,
height: 14,
borderRadius: 3,
background: normalizeToRGB(gradientEnd),
border: "1px solid rgba(255,255,255,0.3)",
}}
/>
<span style={{ fontSize: 12 }}>End</span>
</button> </button>
</div> </div>
</div> )}
<div style={{ marginBottom: 16 }}> {mode !== "cover-gradient" && (
<div <div
style={{ style={{
color: "rgba(255,255,255,0.8)", display: "grid",
fontSize: 12, gridTemplateColumns: "repeat(7, 1fr)",
marginBottom: 6, gap: 8,
marginBottom: 16,
}} }}
> >
Alpha {allColors.map((color) => (
<button
key={color}
type="button"
onClick={() => {
const next = normalizeToRGB(color);
if (mode === "single") {
settings.singleColor = next;
setSingleColor(next);
} else if (mode === "gradient-experimental") {
if (activeEndpoint === "end") {
settings.gradientEnd = next;
setGradientEnd(next);
} else {
settings.gradientStart = next;
setGradientStart(next);
}
}
setCustomInput(next);
requestApply();
}}
style={{
width: 32,
height: 32,
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.2)",
background: normalizeToRGB(color),
cursor: "pointer",
}}
/>
))}
</div> </div>
<input )}
type="range" {mode !== "cover-gradient" && (
min={5} <div style={{ marginBottom: 12 }}>
max={100} <div
step={1} style={{
value={singleAlpha} color: "rgba(255,255,255,0.7)",
onChange={(e) => { fontSize: 12,
const value = Number(e.target.value); marginBottom: 6,
settings.singleAlpha = value; }}
setSingleAlpha(value); >
requestApply(); Custom Hex (#RRGGBB)
}} </div>
style={{ width: "100%" }} <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
/> <input
</div> type="text"
value={customInput}
onChange={(e) => 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",
}}
/>
<button
onClick={() => {
applyCustomInputColor(customInput, false);
addCustomColor();
}}
style={{
width: 32,
height: 32,
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.3)",
background: "rgba(255,255,255,0.15)",
color: "#fff",
cursor: "pointer",
fontSize: 16,
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.2s ease",
}}
type="button"
>
+
</button>
</div>
</div>
)}
{/* Sliders inside picker based on mode */}
{mode === "single" && (
<div style={{ marginBottom: 16 }}>
<div
style={{
color: "rgba(255,255,255,0.8)",
fontSize: 12,
marginBottom: 6,
}}
>
Alpha
</div>
<input
type="range"
min={0}
max={100}
step={1}
value={singleAlpha}
onChange={(e) => {
const value = Number(e.target.value);
settings.singleAlpha = value;
setSingleAlpha(value);
requestApply();
}}
style={{ width: "100%" }}
/>
</div>
)}
{mode === "gradient-experimental" && (
<div style={{ marginBottom: 16, display: "grid", gap: 16 }}>
<div>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
marginBottom: 6,
}}
>
<div
style={{
width: 12,
height: 12,
borderRadius: 3,
background: normalizeToRGB(gradientStart),
border: "1px solid rgba(255,255,255,0.3)",
}}
/>
<div
style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}
>
Start Alpha
</div>
</div>
<input
type="range"
min={0}
max={100}
step={1}
value={gradientStartAlpha}
onChange={(e) => {
const value = Number(e.target.value);
settings.gradientStartAlpha = value;
setGradientStartAlpha(value);
requestApply();
}}
style={{ width: "100%" }}
/>
</div>
<div>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
marginBottom: 6,
}}
>
<div
style={{
width: 12,
height: 12,
borderRadius: 3,
background: normalizeToRGB(gradientEnd),
border: "1px solid rgba(255,255,255,0.3)",
}}
/>
<div
style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}
>
End Alpha
</div>
</div>
<input
type="range"
min={0}
max={100}
step={1}
value={gradientEndAlpha}
onChange={(e) => {
const value = Number(e.target.value);
settings.gradientEndAlpha = value;
setGradientEndAlpha(value);
requestApply();
}}
style={{ width: "100%" }}
/>
</div>
<div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 6,
}}
>
<div
style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}
>
Angle
</div>
<div
style={{ color: "rgba(255,255,255,0.6)", fontSize: 12 }}
>
{gradientAngle}°
</div>
</div>
<input
type="range"
min={0}
max={360}
step={1}
value={gradientAngle}
onChange={(e) => {
const value = Number(e.target.value);
settings.gradientAngle = value;
setGradientAngle(value);
requestApply();
}}
style={{ width: "100%" }}
/>
</div>
</div>
)}
{mode === "cover-gradient" && (
<div style={{ marginBottom: 16 }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 6,
}}
>
<div style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}>
Angle
</div>
<div style={{ color: "rgba(255,255,255,0.6)", fontSize: 12 }}>
{gradientAngle}°
</div>
</div>
<input
type="range"
min={0}
max={360}
step={1}
value={gradientAngle}
onChange={(e) => {
const value = Number(e.target.value);
settings.gradientAngle = value;
setGradientAngle(value);
requestApply();
}}
style={{ width: "100%" }}
/>
</div>
)}
<button <button
onClick={closePicker} onClick={closePicker}
@@ -365,7 +800,7 @@ export const Settings = () => {
)} )}
<AnySwitch <AnySwitch
title="Exclude Inactive" title="Exclude Inactive"
desc="Apply color only to the currently active lyric line" desc="Apply color/gradient only to the currently active lyric line"
checked={excludeInactive} checked={excludeInactive}
onChange={(_event: React.ChangeEvent<HTMLInputElement> | null, checked: boolean) => { onChange={(_event: React.ChangeEvent<HTMLInputElement> | null, checked: boolean) => {
settings.excludeInactive = checked; settings.excludeInactive = checked;
+141 -5
View File
@@ -1,5 +1,5 @@
import { LunaUnload, Tracer } from "@luna/core"; import { LunaUnload, Tracer } from "@luna/core";
import { StyleTag } from "@luna/lib"; import { StyleTag, PlayState } from "@luna/lib";
import { settings, Settings } from "./Settings"; import { settings, Settings } from "./Settings";
import styles from "file://styles.css?minify"; import styles from "file://styles.css?minify";
@@ -11,6 +11,66 @@ export const unloads = new Set<LunaUnload>();
new StyleTag("ColoramaLyrics", unloads, styles); new StyleTag("ColoramaLyrics", unloads, styles);
// Simple dominant color extraction from current cover art
async function getCoverArtElement(): Promise<HTMLImageElement | null> {
const img = document.querySelector(
'figure[class*="_albumImage"] > div > div > div > img',
) as HTMLImageElement | null;
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<void>((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;
// Simple k-means-ish binning into 16 buckets per channel
const buckets = new Map<string, number>();
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 { function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
let v = hex.trim(); let v = hex.trim();
if (!v.startsWith("#")) v = `#${v}`; if (!v.startsWith("#")) v = `#${v}`;
@@ -26,6 +86,8 @@ function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
const b = parseInt(v.slice(5, 7), 16); const b = parseInt(v.slice(5, 7), 16);
return { r, g, b }; return { r, g, b };
} }
// 8-digit hex expects #AARRGGBB. Indices 1-3 are the alpha byte (ignored here),
// so r/g/b are extracted from v.slice(3,5), v.slice(5,7), v.slice(7,9) respectively.
if (/^#([0-9a-fA-F]{8})$/.test(v)) { if (/^#([0-9a-fA-F]{8})$/.test(v)) {
const r = parseInt(v.slice(3, 5), 16); const r = parseInt(v.slice(3, 5), 16);
const g = parseInt(v.slice(5, 7), 16); const g = parseInt(v.slice(5, 7), 16);
@@ -51,28 +113,102 @@ function applySingleColor(color: string) {
document.documentElement.style.setProperty("--cl-lyrics-color", rgba); document.documentElement.style.setProperty("--cl-lyrics-color", rgba);
document.documentElement.style.setProperty("--cl-glow1", rgba); document.documentElement.style.setProperty("--cl-glow1", rgba);
document.documentElement.style.setProperty("--cl-glow2", rgba); document.documentElement.style.setProperty("--cl-glow2", rgba);
document.documentElement.style.removeProperty("--cl-grad-start");
document.documentElement.style.removeProperty("--cl-grad-end");
document.documentElement.style.removeProperty("--cl-grad-angle");
document.body.classList.remove("colorama-gradient");
document.body.classList.add("colorama-single"); document.body.classList.add("colorama-single");
} }
function applyGradient(start: string, end: string, angle: number) {
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");
}
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);
}
}
function applyColoramaLyrics(): void { function applyColoramaLyrics(): void {
if (!settings.enabled) { if (!settings.enabled) {
document.body.classList.remove("colorama-single"); document.body.classList.remove("colorama-single", "colorama-gradient");
return; return;
} }
// Toggle only-active-line mode class
if (settings.excludeInactive) { if (settings.excludeInactive) {
document.body.classList.add("colorama-only-active"); document.body.classList.add("colorama-only-active");
} else { } else {
document.body.classList.remove("colorama-only-active"); document.body.classList.remove("colorama-only-active");
} }
resetModeClasses();
applySingleColor(settings.singleColor); 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; (window as any).applyColoramaLyrics = applyColoramaLyrics;
setTimeout(() => applyColoramaLyrics(), 200); // 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();
}
// Initial apply and observers
setTimeout(() => applyColoramaLyrics(), 200);
observeTrackChanges();
// for some reason, re-apply after Radiant updates its styles/backgrounds
function hookRadiantUpdates(): void { function hookRadiantUpdates(): void {
const w = window as any; const w = window as any;
const wrap = (name: string) => { const wrap = (name: string) => {
+113
View File
@@ -1,6 +1,9 @@
/* Variables used by Colorama Lyrics */ /* Variables used by Colorama Lyrics */
:root { :root {
--cl-lyrics-color: #ffffff; --cl-lyrics-color: #ffffff;
--cl-grad-start: #ffffff;
--cl-grad-end: #88aaff;
--cl-grad-angle: 0deg;
--cl-glow1: #ffffff; --cl-glow1: #ffffff;
--cl-glow2: #ffffff; --cl-glow2: #ffffff;
} }
@@ -21,9 +24,54 @@
-webkit-text-fill-color: 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;
}
/* 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;
}
/* Keep song title color unchanged; its glow is controlled in Radiant CSS */
/* Color Radiant glow shadows using Colorama colors (respect RL sizes) */ /* Color Radiant glow shadows using Colorama colors (respect RL sizes) */
.colorama-single [class*="_lyricsText"] > div > span[data-current="true"], .colorama-single [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-single .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"] [class^="_lyricsContainer"]
> div > div
> div > div
@@ -42,6 +90,20 @@
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important; 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 */
}
/* MARKER: Radiant WBW Lyrics Support */ /* MARKER: Radiant WBW Lyrics Support */
/* Single color: active wbw words & syllable finished */ /* Single color: active wbw words & syllable finished */
@@ -61,6 +123,31 @@
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important; 0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
} }
/* Gradient: active wbw words */
.colorama-gradient .rl-wbw-word.rl-wbw-active {
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;
}
/* Gradient: syllable finished (solid color — gradient conflicts with sweep animation) */
.colorama-gradient .rl-wbw-word.rl-syl-finished {
color: var(--cl-glow1, #ffffff) !important;
}
/* Gradient: active wbw word glow */
.colorama-gradient .rl-wbw-word.rl-wbw-active {
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: wbw words pick up Colorama colors */ /* Hover: wbw words pick up Colorama colors */
.colorama-single .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover, .colorama-single .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover,
.colorama-single .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word.rl-wbw-word-hover { .colorama-single .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word.rl-wbw-word-hover {
@@ -70,8 +157,23 @@
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important; 0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
} }
.colorama-gradient .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover,
.colorama-gradient .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word.rl-wbw-word-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;
}
/* Only-active: wbw words on inactive lines stay default */ /* Only-active: wbw words on inactive lines stay default */
body.colorama-only-active.colorama-single body.colorama-only-active.colorama-single
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word,
body.colorama-only-active.colorama-gradient
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word { .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word {
color: rgba(128, 128, 128, 0.4) !important; color: rgba(128, 128, 128, 0.4) !important;
background: none !important; background: none !important;
@@ -83,6 +185,8 @@ body.colorama-only-active.colorama-single
/* Only-active: hover on inactive wbw lines keeps default */ /* Only-active: hover on inactive wbw lines keeps default */
body.colorama-only-active.colorama-single body.colorama-only-active.colorama-single
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover,
body.colorama-only-active.colorama-gradient
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover { .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover {
color: lightgray !important; color: lightgray !important;
background: none !important; background: none !important;
@@ -94,8 +198,13 @@ body.colorama-only-active.colorama-single
/* Only color active line mode */ /* Only color active line mode */
body.colorama-only-active.colorama-single [class*="_lyricsText"] body.colorama-only-active.colorama-single [class*="_lyricsText"]
> div
> span:not([data-current="true"]),
body.colorama-only-active.colorama-gradient
[class*="_lyricsText"]
> div > div
> span:not([data-current="true"]) { > span:not([data-current="true"]) {
/* Match Radiant inactive styling */
color: rgba(128, 128, 128, 0.4) !important; color: rgba(128, 128, 128, 0.4) !important;
background: none !important; background: none !important;
-webkit-background-clip: initial !important; -webkit-background-clip: initial !important;
@@ -106,6 +215,10 @@ body.colorama-only-active.colorama-single [class*="_lyricsText"]
/* In only-active mode, keep TIDAL defaults even on hover for inactive lines */ /* In only-active mode, keep TIDAL defaults even on hover for inactive lines */
body.colorama-only-active.colorama-single [class*="_lyricsText"] body.colorama-only-active.colorama-single [class*="_lyricsText"]
> div
> span:not([data-current="true"]):hover,
body.colorama-only-active.colorama-gradient
[class*="_lyricsText"]
> div > div
> span:not([data-current="true"]):hover { > span:not([data-current="true"]):hover {
color: lightgray !important; color: lightgray !important;
+18 -2
View File
@@ -21,6 +21,7 @@ declare global {
export const settings = await ReactiveStore.getPluginStorage("RadiantLyrics", { export const settings = await ReactiveStore.getPluginStorage("RadiantLyrics", {
lyricsGlowEnabled: true, lyricsGlowEnabled: true,
trackTitleGlow: false,
hideUIEnabled: true, hideUIEnabled: true,
playerBarVisible: false, playerBarVisible: false,
qualityProgressColor: true, qualityProgressColor: true,
@@ -83,6 +84,9 @@ export const Settings = () => {
const [spinSpeed, setSpinSpeed] = React.useState(settings.spinSpeed); const [spinSpeed, setSpinSpeed] = React.useState(settings.spinSpeed);
const [settingsAffectNowPlaying, setSettingsAffectNowPlaying] = const [settingsAffectNowPlaying, setSettingsAffectNowPlaying] =
React.useState(settings.settingsAffectNowPlaying); React.useState(settings.settingsAffectNowPlaying);
const [trackTitleGlow, setTrackTitleGlow] = React.useState(
settings.trackTitleGlow,
);
const [backgroundScale, setBackgroundScale] = React.useState( const [backgroundScale, setBackgroundScale] = React.useState(
settings.backgroundScale, settings.backgroundScale,
); );
@@ -180,7 +184,19 @@ export const Settings = () => {
} }
}} }}
/> />
{lyricsGlowEnabled && ( <AnySwitch
title="Track Title Glow"
desc="Apply glow to the track title"
checked={trackTitleGlow}
onChange={(_: unknown, checked: boolean) => {
settings.trackTitleGlow = checked;
setTrackTitleGlow(checked);
if (window.updateRadiantLyricsStyles) {
window.updateRadiantLyricsStyles();
}
}}
/>
{(lyricsGlowEnabled || trackTitleGlow) && (
<LunaNumberSetting <LunaNumberSetting
title="Text Glow" title="Text Glow"
desc="Adjust the glow size of lyrics (0-100, default: 20)" desc="Adjust the glow size of lyrics (0-100, default: 20)"
@@ -330,7 +346,7 @@ export const Settings = () => {
/> />
<AnySwitch <AnySwitch
title="Floating Player Bar" title="Floating Player Bar"
desc="When disabled, the player bar becomes a square edge-to-edge bar" desc="Floating rounded player bar with backdrop blur"
checked={floatingPlayerBar} checked={floatingPlayerBar}
onChange={(_: unknown, checked: boolean) => { onChange={(_: unknown, checked: boolean) => {
settings.floatingPlayerBar = checked; settings.floatingPlayerBar = checked;
@@ -43,18 +43,6 @@
backface-visibility: hidden; backface-visibility: hidden;
} }
/* Hide Tidal's native now-playing background color overlay */
[data-test="new-now-playing"] > [class*="_background_"] {
/* biome-ignore lint: Must override native album-art-derived background */
display: none !important;
}
/* Ensure the now-playing container itself is transparent */
[class*="_nowPlayingContainer"] {
/* biome-ignore lint: Must override any inline background styles */
background: transparent !important;
}
/* Now Playing Background Container Optimization */ /* Now Playing Background Container Optimization */
.now-playing-background-container { .now-playing-background-container {
position: absolute; position: absolute;
@@ -62,7 +50,7 @@
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 0; z-index: -3;
pointer-events: none; pointer-events: none;
overflow: hidden; overflow: hidden;
/* Hardware acceleration */ /* Hardware acceleration */
@@ -70,14 +58,6 @@
backface-visibility: hidden; backface-visibility: hidden;
} }
/* Ensure now-playing content renders above the dynamic background */
[data-test="new-now-playing"] > header,
[data-test="new-now-playing"] > [class*="_content_"],
[data-test="new-now-playing"] > .unhide-ui-button {
position: relative;
z-index: 1;
}
/* Optimized keyframe animations with GPU acceleration */ /* Optimized keyframe animations with GPU acceleration */
@keyframes spinGlobal { @keyframes spinGlobal {
from { from {
@@ -109,27 +89,34 @@
filter: blur(10px) brightness(0.4) contrast(1.1) !important; filter: blur(10px) brightness(0.4) contrast(1.1) !important;
} }
/* Make app chrome transparent for cover-everywhere background */ /* Make Notification Feed sidebar transparent */
body, body,
#wimp, #wimp,
main, main,
[class^="_sidebarWrapper"], [class^="_sidebarWrapper"],
[class^="_mainContainer"], [class^="_mainContainer"],
[class*="smallHeader"],
[data-test="main-layout-sidebar-wrapper"], [data-test="main-layout-sidebar-wrapper"],
[data-test="main-layout-header"], [data-test="main-layout-header"],
[data-test="feed-sidebar"], [data-test="feed-sidebar"],
[data-test="stream-metadata"],
[data-test="footer-player"], [data-test="footer-player"],
[class^="_feedSidebarVStack"], /* Notification Feed sidebar specific container */
[class^="_feedSidebarVStack"],
[class^="_feedSidebarSpacer"], [class^="_feedSidebarSpacer"],
[class^="_feedSidebarItem"], [class^="_feedSidebarItem"],
[class^="_feedSidebarItemDiv"], [class^="_feedSidebarItemDiv"],
[class^="_cellContainer"] { [class^="_cellContainer"],
[class^="_cellTextContainer"] {
/* biome-ignore lint: Ensure background is fully cleared under theme CSS */ /* biome-ignore lint: Ensure background is fully cleared under theme CSS */
background: unset !important; background: unset !important;
} }
/* Make sidebar semi-transparent with optimized backdrop-filter */ /* Make sidebar and player bar semi-transparent with optimized backdrop-filter */
[data-test="main-layout-sidebar-wrapper"] { [data-test="footer-player"],
[data-test="main-layout-sidebar-wrapper"],
[class^="_bar"],
[class^="_sidebarItem"]:hover {
/* biome-ignore lint: Must beat app inline styles for translucency */ /* biome-ignore lint: Must beat app inline styles for translucency */
background-color: rgba(0, 0, 0, 0.3) !important; background-color: rgba(0, 0, 0, 0.3) !important;
/* biome-ignore lint: Must beat app inline styles for translucency */ /* biome-ignore lint: Must beat app inline styles for translucency */
@@ -139,7 +126,10 @@ main,
} }
/* Performance mode: reduce backdrop blur */ /* Performance mode: reduce backdrop blur */
.performance-mode [data-test="main-layout-sidebar-wrapper"] { .performance-mode [data-test="footer-player"],
.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 */ /* biome-ignore lint: Performance mode style requires priority */
backdrop-filter: blur(5px) !important; backdrop-filter: blur(5px) !important;
/* biome-ignore lint: Performance mode style requires priority */ /* biome-ignore lint: Performance mode style requires priority */
@@ -173,3 +163,9 @@ main,
/* biome-ignore lint: Match theme transparency */ /* biome-ignore lint: Match theme transparency */
background-color: transparent !important; background-color: transparent !important;
} }
/* Remove bottom gradient */
[class^="_bottomGradient"] {
/* biome-ignore lint: Explicitly remove conflicting gradient */
display: none !important;
}
@@ -1,22 +1,9 @@
/* Square Player Bar override — injected when floating is disabled */ /* Floating Rounded Player Bar from Obsidian <3 */
/* MARKER: Floating Player Bar CSS */ /* MARKER: Floating Player Bar CSS */
[data-test="footer-player"] { [data-test="footer-player"] {
/* biome-ignore lint: Override native floating position */ position: absolute !important;
bottom: 0 !important; backdrop-filter: blur(10px);
/* biome-ignore lint: Override native floating position */ border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin, rgba(255, 255, 255, 0.1)) !important;
left: 0 !important;
/* biome-ignore lint: Override native floating position */
right: 0 !important;
/* biome-ignore lint: Override native floating position */
width: 100% !important;
/* biome-ignore lint: Override native floating position */
margin: 0 !important;
/* biome-ignore lint: Force square corners */
border-radius: 0 !important;
/* biome-ignore lint: Remove floating border */
border: none !important;
/* biome-ignore lint: Remove floating shadow */
box-shadow: none !important;
} }
File diff suppressed because it is too large Load Diff
+41 -24
View File
@@ -28,7 +28,7 @@
} }
/* Enhanced lyrics styling with glow effects */ /* Enhanced lyrics styling with glow effects */
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"][class*="_current_"] { [class*="_lyricsText"] > div > span[data-current="true"] {
text-shadow: text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #fff), 0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #fff),
/* biome-ignore lint: Required to override app glow strength */ /* biome-ignore lint: Required to override app glow strength */
@@ -44,12 +44,12 @@
font-weight: 700; font-weight: 700;
} }
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"] { [class*="_lyricsText"] > div > span {
text-shadow: text-shadow:
0 0 0px transparent, 0 0 0px transparent,
0 0 0px transparent; 0 0 0px transparent;
transition-duration: 0.25s; transition-duration: 0.25s;
color: rgba(255, 255, 255, 0.4); color: rgba(128, 128, 128, 0.4);
font-size: calc(40px * var(--rl-font-scale, 1)); font-size: calc(40px * var(--rl-font-scale, 1));
font-family: font-family:
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
@@ -57,7 +57,7 @@
font-weight: 700; font-weight: 700;
} }
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"]:hover { [class*="_lyricsText"] > div > span:hover {
text-shadow: text-shadow:
0 0 var(--rl-glow-inner, 2px) lightgray, 0 0 var(--rl-glow-inner, 2px) lightgray,
/* biome-ignore lint: Hover glow should override defaults */ /* biome-ignore lint: Hover glow should override defaults */
@@ -68,8 +68,31 @@
transition-duration: 0.7s; 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),
/* 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;
}
/* Current line transitions */ /* Current line transitions */
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"] { [class*="_lyricsText"] > div > span {
transition: transition:
text-shadow 0.7s ease-in-out, text-shadow 0.7s ease-in-out,
color 0.7s ease-in-out, color 0.7s ease-in-out,
@@ -82,11 +105,14 @@
padding-left: var(--rl-glow-outer) !important; padding-left: var(--rl-glow-outer) !important;
} }
[data-rl-injected][role="tabpanel"] {
transform: translateX(calc(var(--rl-glow-outer) * -1)) !important;
}
/* Lyrics container styling */ /* Lyrics container styling */
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"] { [class^="_lyricsContainer"] > div > div > span {
margin-bottom: 2rem; margin-bottom: 2rem;
/* biome-ignore lint: Must beat Tidal _hasCues_ opacity */ opacity: 1;
opacity: 1 !important;
font-family: font-family:
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
@@ -95,11 +121,6 @@
font-size: calc(38px * var(--rl-font-scale, 1)) !important; font-size: calc(38px * var(--rl-font-scale, 1)) !important;
} }
/* Hide the old Musixmatch attribution footer in the lyrics panel */
[data-test="now-playing-lyrics"] [class*="_footer_"] {
display: none !important;
}
/* MARKER: WBW lyrics CSS */ /* MARKER: WBW lyrics CSS */
/* hide tidal spans for wbw */ /* hide tidal spans for wbw */
@@ -199,14 +220,12 @@
animation-delay: var(--rl-line-delay, 0ms); animation-delay: var(--rl-line-delay, 0ms);
} }
/* Word span — opacity override needed to beat Tidal's ._hasCues_ span { opacity:.35 } */ /* Word span */
.rl-wbw-word { .rl-wbw-word {
text-shadow: text-shadow:
0 0 0px transparent, 0 0 0px transparent,
0 0 0px transparent; 0 0 0px transparent;
color: rgba(255, 255, 255, 0.4); color: rgba(128, 128, 128, 0.4);
/* biome-ignore lint: Must beat Tidal _hasCues_ opacity */
opacity: 1 !important;
transition: transition:
text-shadow 0.15s ease-out, text-shadow 0.15s ease-out,
color 0.15s ease-out; color 0.15s ease-out;
@@ -283,7 +302,7 @@
transparent 100% transparent 100%
), ),
linear-gradient(90deg, var(--cl-glow1, #fff) 100%, transparent 100%), linear-gradient(90deg, var(--cl-glow1, #fff) 100%, transparent 100%),
linear-gradient(90deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.4)); linear-gradient(90deg, rgba(128, 128, 128, 0.4), rgba(128, 128, 128, 0.4));
background-size: background-size:
0.75em 100%, 0.75em 100%,
0% 100%, 0% 100%,
@@ -360,9 +379,7 @@
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 700; font-weight: 700;
color: rgba(255, 255, 255, 0.4); color: rgba(128, 128, 128, 0.4);
/* biome-ignore lint: Must beat Tidal _hasCues_ opacity */
opacity: 1 !important;
text-shadow: 0 0 0px transparent; text-shadow: 0 0 0px transparent;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
@@ -379,7 +396,7 @@
transition: transition:
max-height 0.3s ease, max-height 0.3s ease,
opacity 0.5s ease; opacity 0.5s ease;
color: rgba(255, 255, 255, 0.4); color: rgba(128, 128, 128, 0.4);
} }
.rl-wbw-line.rl-wbw-line-active .rl-wbw-bg-container { .rl-wbw-line.rl-wbw-line-active .rl-wbw-bg-container {
@@ -417,8 +434,8 @@
} }
/* Reset glow when disabled */ /* Reset glow when disabled */
.lyrics-glow-disabled [data-test="now-playing-lyrics"] span[data-test="lyrics-line"][class*="_current_"], .lyrics-glow-disabled [class*="_lyricsText"] > div > span[data-current="true"],
.lyrics-glow-disabled [data-test="now-playing-lyrics"] span[data-test="lyrics-line"]:hover { .lyrics-glow-disabled [class*="_lyricsText"] > div > span:hover {
/* biome-ignore lint: Kill glow on active/hover lines */ /* biome-ignore lint: Kill glow on active/hover lines */
text-shadow: none !important; text-shadow: none !important;
} }
+113 -55
View File
@@ -11,6 +11,8 @@
/* Rounded corners */ /* Rounded corners */
[class*="_thumbnail_"], [class*="_thumbnail_"],
[class*="_imageWrapper_"], [class*="_imageWrapper_"],
[class*="_coverImage_"],
[class*="_overlayIconWrapperAlbum_"],
[class*="_playButton_"] { [class*="_playButton_"] {
border-radius: 5px !important; border-radius: 5px !important;
} }
@@ -18,22 +20,18 @@
/* MARKER: HideUI CSS*/ /* MARKER: HideUI CSS*/
/* Only apply styles when UI is hidden — hide toggle buttons */ /* Only apply styles when UI is hidden */
.radiant-lyrics-ui-hidden [data-test="toggle-lyrics"], .radiant-lyrics-ui-hidden [class*="tabItems"] {
.radiant-lyrics-ui-hidden [data-test="toggle-credits"],
.radiant-lyrics-ui-hidden [data-test="toggle-similar-tracks"] {
opacity: 0 !important; opacity: 0 !important;
transition: opacity 0.4s ease-in-out; transition: opacity 0.4s ease-in-out;
} }
.radiant-lyrics-ui-hidden [data-test="toggle-lyrics"]:hover, .radiant-lyrics-ui-hidden [class*="tabItems"]:hover {
.radiant-lyrics-ui-hidden [data-test="toggle-credits"]:hover,
.radiant-lyrics-ui-hidden [data-test="toggle-similar-tracks"]:hover {
opacity: 1 !important; opacity: 1 !important;
} }
/* Hide header container (search, minimize, fullscreen) when UI is hidden */ /* Hide header container (search, minimize, fullscreen) when UI is hidden */
.radiant-lyrics-ui-hidden [data-test="header"] { .radiant-lyrics-ui-hidden [data-test="header-container"] {
opacity: 0 !important; opacity: 0 !important;
visibility: hidden !important; visibility: hidden !important;
transition: opacity 0.4s ease-in-out, visibility 0s linear 0.4s; transition: opacity 0.4s ease-in-out, visibility 0s linear 0.4s;
@@ -81,8 +79,8 @@
/* MARKER: Sticky Lyrics CSS */ /* MARKER: Sticky Lyrics CSS */
/* Lyrics toggle button */ /* Lyrics tab */
[data-test="toggle-lyrics"]:has(.sticky-lyrics-trigger) { [data-test="tabs-lyrics"]:has(.sticky-lyrics-trigger) {
position: relative !important; position: relative !important;
padding-right: 38px !important; padding-right: 38px !important;
} }
@@ -117,41 +115,35 @@
transition: background 0.2s ease; transition: background 0.2s ease;
} }
/* When Lyrics toggle is pressed — show divider & adjust icon */ /* When Lyrics tab is active — show divider & make icon black*/
[data-test="toggle-lyrics"][aria-pressed="true"] .sticky-lyrics-trigger { [data-test="tabs-lyrics"][aria-selected="true"] .sticky-lyrics-trigger {
color: rgb(30, 30, 30); color: black;
cursor: pointer; cursor: pointer;
} }
[data-test="toggle-lyrics"][aria-pressed="true"] .sticky-lyrics-trigger::before { [data-test="tabs-lyrics"][aria-selected="true"] .sticky-lyrics-trigger::before {
background: rgba(0, 0, 0, 0.15); background: rgba(0, 0, 0, 0.25);
} }
[data-test="toggle-lyrics"][aria-pressed="true"] .sticky-lyrics-trigger:hover { [data-test="tabs-lyrics"][aria-selected="true"] .sticky-lyrics-trigger:hover {
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.6);
} }
/* Animate widening when dropdown opens */ /* Square the Lyrics button bottom corners when dropdown is open */
[data-test="toggle-lyrics"]:has(.sticky-lyrics-trigger) { [data-test="tabs-lyrics"].sticky-lyrics-open {
transition: min-width 0.12s ease-out; border-bottom-left-radius: 0 !important;
border-bottom-right-radius: 0 !important;
} }
/* Reduce top rounding, square bottom, and kill hover tint when dropdown is open */ /* Dropdown */
body.rl-dropdown-open [data-test="toggle-lyrics"] {
border-radius: 12px 12px 0 0 !important;
background-color: rgb(255, 255, 255) !important;
min-width: 150px !important;
}
/* Dropdown — right-aligned under the Lyrics button */
.sticky-lyrics-dropdown { .sticky-lyrics-dropdown {
position: fixed; position: fixed;
background: rgb(255, 255, 255); background: white;
border-radius: 0 0 12px 12px; border-radius: 0 0 16px 16px;
padding: 8px 12px 10px; padding: 8px 12px 10px;
box-sizing: border-box; box-sizing: border-box;
z-index: 10000; z-index: 10000;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
clip-path: inset(0 -20px -20px -20px); clip-path: inset(0 -20px -20px -20px);
animation: stickyLyricsDropdownIn 0.12s ease-out; animation: stickyLyricsDropdownIn 0.12s ease-out;
} }
@@ -159,11 +151,11 @@ body.rl-dropdown-open [data-test="toggle-lyrics"] {
@keyframes stickyLyricsDropdownIn { @keyframes stickyLyricsDropdownIn {
from { from {
opacity: 0; opacity: 0;
transform: translateY(-4px); clip-path: inset(0 0 100% 0);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); clip-path: inset(0 0 0 0);
} }
} }
@@ -178,7 +170,7 @@ body.rl-dropdown-open [data-test="toggle-lyrics"] {
.sticky-lyrics-label { .sticky-lyrics-label {
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
color: rgba(0, 0, 0, 0.8); color: rgba(0, 0, 0, 1);
white-space: nowrap; white-space: nowrap;
} }
@@ -204,7 +196,7 @@ body.rl-dropdown-open [data-test="toggle-lyrics"] {
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.15); background-color: rgba(0, 0, 0, 0.2);
transition: 0.3s; transition: 0.3s;
border-radius: 18px; border-radius: 18px;
} }
@@ -219,16 +211,15 @@ body.rl-dropdown-open [data-test="toggle-lyrics"] {
background-color: white; background-color: white;
transition: 0.3s; transition: 0.3s;
border-radius: 50%; border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
} }
.sticky-lyrics-switch input:checked + .sticky-lyrics-slider { .sticky-lyrics-switch input:checked + .sticky-lyrics-slider {
background-color: rgb(30, 30, 30); background-color: black;
} }
.sticky-lyrics-switch input:checked + .sticky-lyrics-slider::before { .sticky-lyrics-switch input:checked + .sticky-lyrics-slider::before {
transform: translateX(16px); transform: translateX(16px);
background-color: rgb(255, 255, 255);
} }
/* Segmented control (Line | Word | Syllable) */ /* Segmented control (Line | Word | Syllable) */
@@ -239,7 +230,7 @@ body.rl-dropdown-open [data-test="toggle-lyrics"] {
.rl-seg-control { .rl-seg-control {
display: flex; display: flex;
background: rgba(0, 0, 0, 0.06); background: rgba(0, 0, 0, 0.08);
border-radius: 10px; border-radius: 10px;
padding: 2px; padding: 2px;
gap: 2px; gap: 2px;
@@ -250,7 +241,7 @@ body.rl-dropdown-open [data-test="toggle-lyrics"] {
flex: 1; flex: 1;
border: none; border: none;
background: transparent; background: transparent;
color: rgba(0, 0, 0, 0.4); color: rgba(0, 0, 0, 0.5);
font-size: 10px; font-size: 10px;
font-weight: 600; font-weight: 600;
padding: 5px 0; padding: 5px 0;
@@ -262,32 +253,99 @@ body.rl-dropdown-open [data-test="toggle-lyrics"] {
.rl-seg-btn:hover { .rl-seg-btn:hover {
color: rgba(0, 0, 0, 0.7); color: rgba(0, 0, 0, 0.7);
background: rgba(0, 0, 0, 0.06); background: rgba(0, 0, 0, 0.05);
} }
.rl-seg-btn.rl-seg-active { .rl-seg-btn.rl-seg-active {
background: rgb(30, 30, 30); background: white;
color: rgb(255, 255, 255); color: black;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
} }
/* MARKER: PATCHES (Random Fixes for Tidals Changes) */ /* MARKER: PATCHES (Random Fixes for Tidals Changes) */
/* These change allot so i gave them their own section */ /* These change allot so i gave them their own section */
/* Remove max-width cap on now-playing content */ /* Fixes the new Sticky Header Tidal added.. in the shittest jankiest way possible */
[class*="_contentInner"] { /* [class*="_stickyHeader"] {
max-width: none !important; background: transparent !important;
backdrop-filter: blur(50px);
background-color: transparent !important;
width: fit-content !important;
padding-right: 3.5% !important;
-webkit-mask-image:
linear-gradient(to bottom, black 60%, transparent),
linear-gradient(to right, black 85%, transparent) !important;
mask-image:
linear-gradient(to bottom, black 60%, transparent),
linear-gradient(to right, black 85%, transparent) !important;
-webkit-mask-composite: source-in !important;
mask-composite: intersect !important;
padding-bottom: 5px !important;
} }
/* Round now-playing artwork corners */ [class*="_playQueueItems"]{
[data-test="now-playing-artwork"] { border-radius: 2.5px 0 0 0 !important;
/* biome-ignore lint: Override flat corners */
border-radius: 10px !important;
} }
/* Hide the Overlay Scrollbar (people just use mouse scroll) */ [data-test="playqueue-sticky-clear-active-items"] {
.os-scrollbar { visibility: collapse !important;
display: none !important; width: 0px !important;
pointer-events: none !important; }
[data-test="playqueue-sticky-clear-source-items"] {
visibility: collapse !important;
width: 0px !important;
} */
/* Remove the background color from the small header */
[class*="_smallHeader"]::before {
background-color: transparent !important;
}
/* fixes Tidals broken mini cover art padding | Cheers Aya <3*/
._imageBorder_110890a {
filter: opacity(0);
}
._container_14bcbd4._playingFrom_79b167e {
transform: scale(1.01) translatex(.1em);
}
._leftColumn_aaf28de {
min-height: 110%;
transform: translatey(-.23em);
}
._imageryContainer_f99fc07.image {
transform: scale(1.03) translatey(.2em) translatex(.1em);
background-color: #00000000;
padding: 0em !important;
}
._image_145331a._cellImage_0ef8dd3 {
border-radius: .7em !important;
}
[data-test="footer-player"] {
._container_14bcbd4._playingFrom_79b167e > ._text_15008b2._medium20_1lyag_192._marketText_1lyag_1 {
transform: translatey(-.2em);
}
[class="image _imageryContainer_f99fc07"] {
transform: translatey(.3em) !important;
}
._image_145331a._cellImage_0ef8dd3 {
border-radius: .25em !important;
}
._toggleButton_809eee8 {
transform: translateY(-.22em);
}
[class="image _imageryContainer_f99fc07"]:hover {
[class="_cellImage_0ef8dd3 _image_145331a"] {
filter: brightness(.3);
}
}
._notFullscreenOverlay_1442d60 {
background: none !important;
transition: 0ms;
}
._notFullscreenOverlay_1442d60 ._nowPlayingButton_c1a86fa {
background-color: rgba(245, 245, 220, 0);
}
} }