mirror of
https://github.com/meowarex/TidaLuna-Plugins.git
synced 2026-06-18 03:43:10 +10:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b2478c301 | |||
| 59562f8264 | |||
| 40853d6e64 | |||
| 74e3c97147 | |||
| b79e15b6c5 | |||
| 9d1ca88e46 | |||
| bff87b96a1 | |||
| 9d6afcaaf5 | |||
| 8f995d8474 |
@@ -18,5 +18,8 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,11 @@ 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() {
|
||||||
@@ -31,7 +25,6 @@ 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
|
||||||
@@ -49,10 +42,15 @@ let animationId: number | null = null;
|
|||||||
let currentAudioElement: HTMLAudioElement | null = null;
|
let currentAudioElement: HTMLAudioElement | null = null;
|
||||||
let isSourceConnected: boolean = false;
|
let isSourceConnected: boolean = false;
|
||||||
|
|
||||||
// Canvas and container elements
|
// Each placement gets its own container/canvas/context
|
||||||
let visualizerContainer: HTMLDivElement | null = null;
|
interface VisualizerSlot {
|
||||||
let canvas: HTMLCanvasElement | null = null;
|
container: HTMLDivElement | null;
|
||||||
let canvasContext: CanvasRenderingContext2D | null = null;
|
canvas: HTMLCanvasElement | 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 => {
|
||||||
@@ -140,10 +138,7 @@ const initializeAudioVisualizer = async (): Promise<void> => {
|
|||||||
audioContext.resume().catch(() => {}); // Fire and forget
|
audioContext.resume().catch(() => {}); // Fire and forget
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create UI only if it doesn't exist
|
createVisualizerUI();
|
||||||
if (!visualizerContainer) {
|
|
||||||
createVisualizerUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start animation only if not already running
|
// Start animation only if not already running
|
||||||
if (!animationId) {
|
if (!animationId) {
|
||||||
@@ -155,120 +150,116 @@ const initializeAudioVisualizer = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the visualizer UI container and canvas
|
const makeSlotElements = (): { container: HTMLDivElement; canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } | null => {
|
||||||
const createVisualizerUI = (): void => {
|
const container = document.createElement("div");
|
||||||
// Remove existing visualizer if it exists
|
container.className = "audio-visualizer-container";
|
||||||
removeVisualizerUI();
|
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);
|
||||||
|
`;
|
||||||
|
|
||||||
if (!config.enabled) return;
|
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;
|
||||||
|
`;
|
||||||
|
|
||||||
// Find the search bar
|
container.appendChild(cvs);
|
||||||
const searchField = document.querySelector(
|
const ctx = cvs.getContext("2d");
|
||||||
'input[class*="_searchField"]',
|
if (!ctx) return null;
|
||||||
) as HTMLInputElement;
|
return { container, canvas: cvs, ctx };
|
||||||
if (!searchField) {
|
};
|
||||||
warn("Search field not found");
|
|
||||||
return;
|
const clearSlot = (slot: VisualizerSlot): void => {
|
||||||
}
|
slot.container?.remove();
|
||||||
|
slot.container = null;
|
||||||
const searchContainer = searchField.parentElement;
|
slot.canvas = null;
|
||||||
if (!searchContainer) {
|
slot.ctx = null;
|
||||||
warn("Search container not found");
|
};
|
||||||
return;
|
|
||||||
}
|
const ensureNavSlot = (): void => {
|
||||||
|
if (navSlot.container?.isConnected) return;
|
||||||
// Create visualizer container
|
clearSlot(navSlot);
|
||||||
visualizerContainer = document.createElement("div");
|
|
||||||
visualizerContainer.id = "audio-visualizer-container";
|
const searchField = document.querySelector('input[class*="_searchField"]') as HTMLInputElement;
|
||||||
visualizerContainer.style.cssText = `
|
if (!searchField) return;
|
||||||
display: flex;
|
const searchContainer = searchField.parentElement;
|
||||||
align-items: center;
|
if (!searchContainer?.parentElement) return;
|
||||||
justify-content: center;
|
|
||||||
margin-${config.position === "left" ? "right" : "left"}: 12px;
|
const els = makeSlotElements();
|
||||||
background: rgba(0, 0, 0, 0.2);
|
if (!els) return;
|
||||||
border-radius: 8px;
|
els.container.style.marginRight = "12px";
|
||||||
padding: 4px;
|
Object.assign(navSlot, els);
|
||||||
backdrop-filter: blur(10px);
|
searchContainer.parentElement.insertBefore(els.container, searchContainer);
|
||||||
-webkit-backdrop-filter: blur(10px);
|
};
|
||||||
`;
|
|
||||||
|
const ensureNpSlot = (): void => {
|
||||||
// Create canvas
|
if (npSlot.container?.isConnected) return;
|
||||||
canvas = document.createElement("canvas");
|
clearSlot(npSlot);
|
||||||
canvas.width = config.width;
|
|
||||||
canvas.height = config.height;
|
const artistInfo = document.querySelector('[data-test="artist-info"]');
|
||||||
canvas.style.cssText = `
|
if (!artistInfo) return;
|
||||||
width: ${config.width}px;
|
const leftContent = artistInfo.parentElement;
|
||||||
height: ${config.height}px;
|
if (!leftContent) return;
|
||||||
border-radius: 4px;
|
|
||||||
`;
|
const els = makeSlotElements();
|
||||||
|
if (!els) return;
|
||||||
visualizerContainer.appendChild(canvas);
|
els.container.style.marginLeft = "12px";
|
||||||
canvasContext = canvas.getContext("2d");
|
Object.assign(npSlot, els);
|
||||||
|
leftContent.insertBefore(els.container, artistInfo.nextSibling);
|
||||||
// Insert visualizer next to search bar
|
};
|
||||||
if (config.position === "left") {
|
|
||||||
searchContainer.parentElement?.insertBefore(
|
const createVisualizerUI = (): void => {
|
||||||
visualizerContainer,
|
if (!config.enabled) return;
|
||||||
searchContainer,
|
ensureNavSlot();
|
||||||
);
|
ensureNpSlot();
|
||||||
} else {
|
|
||||||
searchContainer.parentElement?.insertBefore(
|
|
||||||
visualizerContainer,
|
|
||||||
searchContainer.nextSibling,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove visualizer UI
|
|
||||||
const removeVisualizerUI = (): void => {
|
const removeVisualizerUI = (): void => {
|
||||||
if (visualizerContainer) {
|
clearSlot(navSlot);
|
||||||
visualizerContainer.remove();
|
clearSlot(npSlot);
|
||||||
visualizerContainer = null;
|
|
||||||
canvas = null;
|
|
||||||
canvasContext = null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Animation loop for rendering visualizer
|
// Animation loop for rendering visualizer
|
||||||
const animate = (): void => {
|
const animate = (): void => {
|
||||||
if (!canvasContext || !canvas) {
|
// Re-attach slots that got disconnected from the DOM
|
||||||
animationId = null;
|
createVisualizerUI();
|
||||||
|
|
||||||
|
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; // Threshold for detecting actual audio
|
hasRealAudio = avgVolume > 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear canvas
|
for (const slot of slots) {
|
||||||
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
|
const ctx = slot.ctx!;
|
||||||
|
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) {
|
||||||
// Draw real audio visualization
|
drawBars(ctx, cvs);
|
||||||
switch (config.visualizerType) {
|
} else {
|
||||||
case "bars": // Is implemented YAYYY (default)
|
drawScrollingWave(ctx, cvs);
|
||||||
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);
|
||||||
@@ -291,67 +282,54 @@ const drawRoundedRect = (
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Draw scrolling wave effect when no audio is detected
|
const drawScrollingWave = (ctx: CanvasRenderingContext2D, cvs: HTMLCanvasElement): void => {
|
||||||
const drawScrollingWave = (): void => {
|
waveTime += 0.05 / [navSlot, npSlot].filter(s => s.ctx).length;
|
||||||
if (!canvasContext || !canvas) return;
|
|
||||||
|
|
||||||
waveTime += 0.05; // Speed of wave animation
|
|
||||||
|
|
||||||
const barCount = config.barCount;
|
const barCount = config.barCount;
|
||||||
const barWidth = canvas.width / barCount;
|
const barWidth = cvs.width / barCount;
|
||||||
const maxHeight = canvas.height * 0.6;
|
const maxHeight = cvs.height * 0.6;
|
||||||
|
|
||||||
canvasContext.fillStyle = config.color;
|
ctx.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 = (canvas.height - barHeight) / 2;
|
const yPos = (cvs.height - barHeight) / 2;
|
||||||
|
|
||||||
// Draw rounded or square bars based on setting
|
|
||||||
if (config.barRounding) {
|
if (config.barRounding) {
|
||||||
drawRoundedRect(canvasContext, xPos, yPos, barWidth - 1, barHeight, 2);
|
drawRoundedRect(ctx, xPos, yPos, barWidth - 1, barHeight, 2);
|
||||||
} else {
|
} else {
|
||||||
canvasContext.fillRect(xPos, yPos, barWidth - 1, barHeight);
|
ctx.fillRect(xPos, yPos, barWidth - 1, barHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Draw frequency bars - default
|
const drawBars = (ctx: CanvasRenderingContext2D, cvs: HTMLCanvasElement): void => {
|
||||||
const drawBars = (): void => {
|
if (!dataArray) return;
|
||||||
if (!canvasContext || !dataArray || !canvas) return;
|
|
||||||
|
|
||||||
const barWidth = canvas.width / config.barCount;
|
const barWidth = cvs.width / config.barCount;
|
||||||
const heightScale = canvas.height / 255;
|
const heightScale = cvs.height / 255;
|
||||||
|
|
||||||
canvasContext.fillStyle = config.color;
|
ctx.fillStyle = config.color;
|
||||||
|
|
||||||
for (let i = 0; i < config.barCount; i++) {
|
for (let i = 0; i < config.barCount; i++) {
|
||||||
const dataIndex = Math.floor(i * (dataArray.length / config.barCount));
|
const dataIndex = Math.floor(i * (dataArray.length / config.barCount));
|
||||||
const barHeight = dataArray[dataIndex] * config.sensitivity * heightScale;
|
const barHeight = dataArray[dataIndex] * config.sensitivity * heightScale;
|
||||||
|
|
||||||
const x = i * barWidth;
|
const x = i * barWidth;
|
||||||
const y = canvas.height - barHeight;
|
const y = cvs.height - barHeight;
|
||||||
|
|
||||||
// Draw rounded or square bars based on setting
|
|
||||||
if (config.barRounding) {
|
if (config.barRounding) {
|
||||||
drawRoundedRect(canvasContext, x, y, barWidth - 1, barHeight, 2);
|
drawRoundedRect(ctx, x, y, barWidth - 1, barHeight, 2);
|
||||||
} else {
|
} else {
|
||||||
canvasContext.fillRect(x, y, barWidth - 1, barHeight);
|
ctx.fillRect(x, y, barWidth - 1, barHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -412,23 +390,23 @@ const drawBars = (): void => {
|
|||||||
// }
|
// }
|
||||||
// };
|
// };
|
||||||
|
|
||||||
// Update visualizer settings
|
|
||||||
const updateAudioVisualizer = (): void => {
|
const updateAudioVisualizer = (): void => {
|
||||||
if (analyser) {
|
if (analyser) {
|
||||||
// use a fixed size that provides enough frequency bins
|
analyser.fftSize = 512;
|
||||||
analyser.fftSize = 512; // Fixed power of 2 - important
|
|
||||||
analyser.smoothingTimeConstant = config.smoothing;
|
analyser.smoothingTimeConstant = config.smoothing;
|
||||||
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canvas) {
|
for (const slot of [navSlot, npSlot]) {
|
||||||
canvas.width = config.width;
|
if (slot.canvas) {
|
||||||
canvas.height = config.height;
|
slot.canvas.width = config.width;
|
||||||
canvas.style.width = `${config.width}px`;
|
slot.canvas.height = config.height;
|
||||||
canvas.style.height = `${config.height}px`;
|
slot.canvas.style.width = `${config.width}px`;
|
||||||
|
slot.canvas.style.height = `${config.height}px`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recreate UI if position changed
|
removeVisualizerUI();
|
||||||
createVisualizerUI();
|
createVisualizerUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,40 @@
|
|||||||
/* Audio Visualizer CSS - Only applies to the Visualizer */
|
/* Audio Visualizer CSS */
|
||||||
|
|
||||||
#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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Where to put the thingy */
|
.audio-visualizer-container.active {
|
||||||
[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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fade in animation */
|
@keyframes av-fadeIn {
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.8);
|
transform: scale(0.8);
|
||||||
@@ -55,6 +45,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#audio-visualizer-container {
|
[data-type="search-field"] {
|
||||||
animation: fadeIn 0.5s ease-out;
|
min-width: 220px !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,53 +8,24 @@ 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);
|
||||||
@@ -63,9 +34,6 @@ 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;
|
||||||
@@ -73,28 +41,23 @@ 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;
|
||||||
};
|
};
|
||||||
@@ -121,8 +84,7 @@ export const Settings = () => {
|
|||||||
"#1976D2",
|
"#1976D2",
|
||||||
];
|
];
|
||||||
|
|
||||||
const openPicker = (endpoint: "single" | "start" | "end" = "single") => {
|
const openPicker = () => {
|
||||||
setActiveEndpoint(endpoint);
|
|
||||||
setShowPicker(true);
|
setShowPicker(true);
|
||||||
setShouldRender(true);
|
setShouldRender(true);
|
||||||
setTimeout(() => setIsAnimatingIn(true), 10);
|
setTimeout(() => setIsAnimatingIn(true), 10);
|
||||||
@@ -140,22 +102,10 @@ 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;
|
||||||
if (mode === "single") {
|
const next = normalizeToRGB(trimmed);
|
||||||
const next = normalizeToRGB(trimmed);
|
settings.singleColor = next;
|
||||||
settings.singleColor = next;
|
setSingleColor(next);
|
||||||
setSingleColor(next);
|
if (updateInput) setCustomInput(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();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,12 +122,6 @@ 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 = () => {
|
||||||
@@ -186,66 +130,11 @@ export const Settings = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LunaSettings>
|
<LunaSettings>
|
||||||
{/* Mode selection via dropdown (aligned right) */}
|
{/* Single color picker button */}
|
||||||
<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",
|
||||||
}}
|
}}
|
||||||
@@ -272,7 +161,7 @@ export const Settings = () => {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => (showPicker ? closePicker() : openPicker("single"))}
|
onClick={() => (showPicker ? closePicker() : openPicker())}
|
||||||
style={{
|
style={{
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
@@ -285,84 +174,7 @@ export const Settings = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Gradient controls (open picker) */}
|
{/* Color picker modal */}
|
||||||
<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
|
||||||
@@ -415,369 +227,122 @@ export const Settings = () => {
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{mode === "single" ? "Single Color" : "Gradient Colors"}
|
Lyrics Color
|
||||||
</div>
|
</div>
|
||||||
{mode === "gradient-experimental" && (
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
display: "grid",
|
||||||
display: "flex",
|
gridTemplateColumns: "repeat(7, 1fr)",
|
||||||
gap: 8,
|
gap: 8,
|
||||||
alignItems: "center",
|
marginBottom: 16,
|
||||||
marginBottom: 12,
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{allColors.map((color) => (
|
||||||
<div style={{ color: "rgba(255,255,255,0.7)", fontSize: 12 }}>
|
|
||||||
Editing
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setActiveEndpoint("start");
|
|
||||||
setCustomInput(gradientStart);
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 8,
|
|
||||||
padding: "6px 10px",
|
|
||||||
borderRadius: 8,
|
|
||||||
border:
|
|
||||||
activeEndpoint === "start"
|
|
||||||
? "1px solid rgba(255,255,255,0.5)"
|
|
||||||
: "1px solid rgba(255,255,255,0.2)",
|
|
||||||
background: "rgba(255,255,255,0.05)",
|
|
||||||
color: "#fff",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
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
|
<button
|
||||||
|
key={color}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveEndpoint("end");
|
const next = normalizeToRGB(color);
|
||||||
setCustomInput(gradientEnd);
|
settings.singleColor = next;
|
||||||
|
setSingleColor(next);
|
||||||
|
setCustomInput(next);
|
||||||
|
requestApply();
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
width: 32,
|
||||||
alignItems: "center",
|
height: 32,
|
||||||
gap: 8,
|
borderRadius: 6,
|
||||||
padding: "6px 10px",
|
border: "1px solid rgba(255,255,255,0.2)",
|
||||||
borderRadius: 8,
|
background: normalizeToRGB(color),
|
||||||
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",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<span
|
))}
|
||||||
style={{
|
</div>
|
||||||
width: 14,
|
<div style={{ marginBottom: 12 }}>
|
||||||
height: 14,
|
|
||||||
borderRadius: 3,
|
|
||||||
background: normalizeToRGB(gradientEnd),
|
|
||||||
border: "1px solid rgba(255,255,255,0.3)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span style={{ fontSize: 12 }}>End</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{mode !== "cover-gradient" && (
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "grid",
|
color: "rgba(255,255,255,0.7)",
|
||||||
gridTemplateColumns: "repeat(7, 1fr)",
|
fontSize: 12,
|
||||||
gap: 8,
|
marginBottom: 6,
|
||||||
marginBottom: 16,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{allColors.map((color) => (
|
Custom Hex (#RRGGBB)
|
||||||
<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>
|
||||||
)}
|
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||||
{mode !== "cover-gradient" && (
|
<input
|
||||||
<div style={{ marginBottom: 12 }}>
|
type="text"
|
||||||
<div
|
value={customInput}
|
||||||
style={{
|
onChange={(e) => setCustomInput(e.target.value)}
|
||||||
color: "rgba(255,255,255,0.7)",
|
onKeyDown={(e) => {
|
||||||
fontSize: 12,
|
if (e.key === "Enter") {
|
||||||
marginBottom: 6,
|
applyCustomInputColor(customInput, true);
|
||||||
}}
|
|
||||||
>
|
|
||||||
Custom Hex (#RRGGBB)
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={customInput}
|
|
||||||
onChange={(e) => setCustomInput(e.target.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
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();
|
addCustomColor();
|
||||||
}}
|
}
|
||||||
style={{
|
}}
|
||||||
width: 32,
|
placeholder="#RRGGBB"
|
||||||
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={{
|
style={{
|
||||||
color: "rgba(255,255,255,0.8)",
|
flex: 1,
|
||||||
fontSize: 12,
|
padding: "8px 12px",
|
||||||
marginBottom: 6,
|
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",
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
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>
|
<button
|
||||||
)}
|
onClick={() => {
|
||||||
|
applyCustomInputColor(customInput, false);
|
||||||
{mode === "gradient-experimental" && (
|
addCustomColor();
|
||||||
<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={{
|
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: "space-between",
|
justifyContent: "center",
|
||||||
marginBottom: 6,
|
transition: "all 0.2s ease",
|
||||||
}}
|
}}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<div style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}>
|
+
|
||||||
Angle
|
</button>
|
||||||
</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>
|
||||||
)}
|
</div>
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: "rgba(255,255,255,0.8)",
|
||||||
|
fontSize: 12,
|
||||||
|
marginBottom: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Alpha
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={5}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
value={singleAlpha}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = Number(e.target.value);
|
||||||
|
settings.singleAlpha = value;
|
||||||
|
setSingleAlpha(value);
|
||||||
|
requestApply();
|
||||||
|
}}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={closePicker}
|
onClick={closePicker}
|
||||||
@@ -800,7 +365,7 @@ export const Settings = () => {
|
|||||||
)}
|
)}
|
||||||
<AnySwitch
|
<AnySwitch
|
||||||
title="Exclude Inactive"
|
title="Exclude Inactive"
|
||||||
desc="Apply color/gradient only to the currently active lyric line"
|
desc="Apply color 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;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LunaUnload, Tracer } from "@luna/core";
|
import { LunaUnload, Tracer } from "@luna/core";
|
||||||
import { StyleTag, PlayState } from "@luna/lib";
|
import { StyleTag } 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,66 +11,6 @@ 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}`;
|
||||||
@@ -86,8 +26,6 @@ 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);
|
||||||
@@ -113,102 +51,28 @@ 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", "colorama-gradient");
|
document.body.classList.remove("colorama-single");
|
||||||
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();
|
|
||||||
switch (settings.mode) {
|
applySingleColor(settings.singleColor);
|
||||||
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;
|
||||||
|
|
||||||
// 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);
|
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) => {
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
/* 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;
|
||||||
}
|
}
|
||||||
@@ -24,54 +21,9 @@
|
|||||||
-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
|
||||||
@@ -90,20 +42,6 @@
|
|||||||
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 */
|
||||||
@@ -123,31 +61,6 @@
|
|||||||
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 {
|
||||||
@@ -157,23 +70,8 @@
|
|||||||
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;
|
||||||
@@ -185,8 +83,6 @@ body.colorama-only-active.colorama-gradient
|
|||||||
|
|
||||||
/* 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;
|
||||||
@@ -198,13 +94,8 @@ body.colorama-only-active.colorama-gradient
|
|||||||
|
|
||||||
/* 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;
|
||||||
@@ -215,10 +106,6 @@ body.colorama-only-active.colorama-gradient
|
|||||||
|
|
||||||
/* 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;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ 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,
|
||||||
@@ -84,9 +83,6 @@ 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,
|
||||||
);
|
);
|
||||||
@@ -184,19 +180,7 @@ export const Settings = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<AnySwitch
|
{lyricsGlowEnabled && (
|
||||||
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)"
|
||||||
@@ -346,7 +330,7 @@ export const Settings = () => {
|
|||||||
/>
|
/>
|
||||||
<AnySwitch
|
<AnySwitch
|
||||||
title="Floating Player Bar"
|
title="Floating Player Bar"
|
||||||
desc="Floating rounded player bar with backdrop blur"
|
desc="When disabled, the player bar becomes a square edge-to-edge bar"
|
||||||
checked={floatingPlayerBar}
|
checked={floatingPlayerBar}
|
||||||
onChange={(_: unknown, checked: boolean) => {
|
onChange={(_: unknown, checked: boolean) => {
|
||||||
settings.floatingPlayerBar = checked;
|
settings.floatingPlayerBar = checked;
|
||||||
|
|||||||
@@ -43,6 +43,18 @@
|
|||||||
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;
|
||||||
@@ -50,7 +62,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: -3;
|
z-index: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
/* Hardware acceleration */
|
/* Hardware acceleration */
|
||||||
@@ -58,6 +70,14 @@
|
|||||||
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 {
|
||||||
@@ -89,34 +109,27 @@
|
|||||||
filter: blur(10px) brightness(0.4) contrast(1.1) !important;
|
filter: blur(10px) brightness(0.4) contrast(1.1) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make Notification Feed sidebar transparent */
|
/* Make app chrome transparent for cover-everywhere background */
|
||||||
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"],
|
||||||
/* Notification Feed sidebar specific container */
|
[class^="_feedSidebarVStack"],
|
||||||
[class^="_feedSidebarVStack"],
|
|
||||||
[class^="_feedSidebarSpacer"],
|
[class^="_feedSidebarSpacer"],
|
||||||
[class^="_feedSidebarItem"],
|
[class^="_feedSidebarItem"],
|
||||||
[class^="_feedSidebarItemDiv"],
|
[class^="_feedSidebarItemDiv"],
|
||||||
[class^="_cellContainer"],
|
[class^="_cellContainer"] {
|
||||||
[class^="_cellTextContainer"] {
|
|
||||||
/* 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 and player bar semi-transparent with optimized backdrop-filter */
|
/* Make sidebar semi-transparent with optimized backdrop-filter */
|
||||||
[data-test="footer-player"],
|
[data-test="main-layout-sidebar-wrapper"] {
|
||||||
[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 */
|
||||||
@@ -126,10 +139,7 @@ main,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Performance mode: reduce backdrop blur */
|
/* Performance mode: reduce backdrop blur */
|
||||||
.performance-mode [data-test="footer-player"],
|
.performance-mode [data-test="main-layout-sidebar-wrapper"] {
|
||||||
.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 */
|
||||||
@@ -163,9 +173,3 @@ 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,9 +1,22 @@
|
|||||||
/* Floating Rounded Player Bar from Obsidian <3 */
|
/* Square Player Bar override — injected when floating is disabled */
|
||||||
|
|
||||||
/* MARKER: Floating Player Bar CSS */
|
/* MARKER: Floating Player Bar CSS */
|
||||||
|
|
||||||
[data-test="footer-player"] {
|
[data-test="footer-player"] {
|
||||||
position: absolute !important;
|
/* biome-ignore lint: Override native floating position */
|
||||||
backdrop-filter: blur(10px);
|
bottom: 0 !important;
|
||||||
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin, rgba(255, 255, 255, 0.1)) !important;
|
/* biome-ignore lint: Override native floating position */
|
||||||
|
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
@@ -28,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Enhanced lyrics styling with glow effects */
|
/* Enhanced lyrics styling with glow effects */
|
||||||
[class*="_lyricsText"] > div > span[data-current="true"] {
|
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"][class*="_current_"] {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*="_lyricsText"] > div > span {
|
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"] {
|
||||||
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(128, 128, 128, 0.4);
|
color: rgba(255, 255, 255, 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*="_lyricsText"] > div > span:hover {
|
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"]: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,31 +68,8 @@
|
|||||||
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 */
|
||||||
[class*="_lyricsText"] > div > span {
|
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"] {
|
||||||
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,
|
||||||
@@ -105,14 +82,11 @@
|
|||||||
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 */
|
||||||
[class^="_lyricsContainer"] > div > div > span {
|
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"] {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
opacity: 1;
|
/* biome-ignore lint: Must beat Tidal _hasCues_ opacity */
|
||||||
|
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;
|
||||||
@@ -121,6 +95,11 @@
|
|||||||
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 */
|
||||||
@@ -220,12 +199,14 @@
|
|||||||
animation-delay: var(--rl-line-delay, 0ms);
|
animation-delay: var(--rl-line-delay, 0ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Word span */
|
/* Word span — opacity override needed to beat Tidal's ._hasCues_ span { opacity:.35 } */
|
||||||
.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(128, 128, 128, 0.4);
|
color: rgba(255, 255, 255, 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;
|
||||||
@@ -302,7 +283,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(128, 128, 128, 0.4), rgba(128, 128, 128, 0.4));
|
linear-gradient(90deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.4));
|
||||||
background-size:
|
background-size:
|
||||||
0.75em 100%,
|
0.75em 100%,
|
||||||
0% 100%,
|
0% 100%,
|
||||||
@@ -379,7 +360,9 @@
|
|||||||
"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(128, 128, 128, 0.4);
|
color: rgba(255, 255, 255, 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;
|
||||||
}
|
}
|
||||||
@@ -396,7 +379,7 @@
|
|||||||
transition:
|
transition:
|
||||||
max-height 0.3s ease,
|
max-height 0.3s ease,
|
||||||
opacity 0.5s ease;
|
opacity 0.5s ease;
|
||||||
color: rgba(128, 128, 128, 0.4);
|
color: rgba(255, 255, 255, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rl-wbw-line.rl-wbw-line-active .rl-wbw-bg-container {
|
.rl-wbw-line.rl-wbw-line-active .rl-wbw-bg-container {
|
||||||
@@ -434,8 +417,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Reset glow when disabled */
|
/* Reset glow when disabled */
|
||||||
.lyrics-glow-disabled [class*="_lyricsText"] > div > span[data-current="true"],
|
.lyrics-glow-disabled [data-test="now-playing-lyrics"] span[data-test="lyrics-line"][class*="_current_"],
|
||||||
.lyrics-glow-disabled [class*="_lyricsText"] > div > span:hover {
|
.lyrics-glow-disabled [data-test="now-playing-lyrics"] span[data-test="lyrics-line"]: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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@
|
|||||||
/* 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;
|
||||||
}
|
}
|
||||||
@@ -20,18 +18,22 @@
|
|||||||
|
|
||||||
/* MARKER: HideUI CSS*/
|
/* MARKER: HideUI CSS*/
|
||||||
|
|
||||||
/* Only apply styles when UI is hidden */
|
/* Only apply styles when UI is hidden — hide toggle buttons */
|
||||||
.radiant-lyrics-ui-hidden [class*="tabItems"] {
|
.radiant-lyrics-ui-hidden [data-test="toggle-lyrics"],
|
||||||
|
.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 [class*="tabItems"]:hover {
|
.radiant-lyrics-ui-hidden [data-test="toggle-lyrics"]: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-container"] {
|
.radiant-lyrics-ui-hidden [data-test="header"] {
|
||||||
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;
|
||||||
@@ -79,8 +81,8 @@
|
|||||||
|
|
||||||
/* MARKER: Sticky Lyrics CSS */
|
/* MARKER: Sticky Lyrics CSS */
|
||||||
|
|
||||||
/* Lyrics tab */
|
/* Lyrics toggle button */
|
||||||
[data-test="tabs-lyrics"]:has(.sticky-lyrics-trigger) {
|
[data-test="toggle-lyrics"]:has(.sticky-lyrics-trigger) {
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
padding-right: 38px !important;
|
padding-right: 38px !important;
|
||||||
}
|
}
|
||||||
@@ -115,35 +117,41 @@
|
|||||||
transition: background 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* When Lyrics tab is active — show divider & make icon black*/
|
/* When Lyrics toggle is pressed — show divider & adjust icon */
|
||||||
[data-test="tabs-lyrics"][aria-selected="true"] .sticky-lyrics-trigger {
|
[data-test="toggle-lyrics"][aria-pressed="true"] .sticky-lyrics-trigger {
|
||||||
color: black;
|
color: rgb(30, 30, 30);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-test="tabs-lyrics"][aria-selected="true"] .sticky-lyrics-trigger::before {
|
[data-test="toggle-lyrics"][aria-pressed="true"] .sticky-lyrics-trigger::before {
|
||||||
background: rgba(0, 0, 0, 0.25);
|
background: rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-test="tabs-lyrics"][aria-selected="true"] .sticky-lyrics-trigger:hover {
|
[data-test="toggle-lyrics"][aria-pressed="true"] .sticky-lyrics-trigger:hover {
|
||||||
color: rgba(0, 0, 0, 0.6);
|
color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Square the Lyrics button bottom corners when dropdown is open */
|
/* Animate widening when dropdown opens */
|
||||||
[data-test="tabs-lyrics"].sticky-lyrics-open {
|
[data-test="toggle-lyrics"]:has(.sticky-lyrics-trigger) {
|
||||||
border-bottom-left-radius: 0 !important;
|
transition: min-width 0.12s ease-out;
|
||||||
border-bottom-right-radius: 0 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dropdown */
|
/* Reduce top rounding, square bottom, and kill hover tint when dropdown is open */
|
||||||
|
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: white;
|
background: rgb(255, 255, 255);
|
||||||
border-radius: 0 0 16px 16px;
|
border-radius: 0 0 12px 12px;
|
||||||
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.25);
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -151,11 +159,11 @@
|
|||||||
@keyframes stickyLyricsDropdownIn {
|
@keyframes stickyLyricsDropdownIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
clip-path: inset(0 0 100% 0);
|
transform: translateY(-4px);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
clip-path: inset(0 0 0 0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +178,7 @@
|
|||||||
.sticky-lyrics-label {
|
.sticky-lyrics-label {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: rgba(0, 0, 0, 1);
|
color: rgba(0, 0, 0, 0.8);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +204,7 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
}
|
}
|
||||||
@@ -211,15 +219,16 @@
|
|||||||
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.2);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sticky-lyrics-switch input:checked + .sticky-lyrics-slider {
|
.sticky-lyrics-switch input:checked + .sticky-lyrics-slider {
|
||||||
background-color: black;
|
background-color: rgb(30, 30, 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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) */
|
||||||
@@ -230,7 +239,7 @@
|
|||||||
|
|
||||||
.rl-seg-control {
|
.rl-seg-control {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: rgba(0, 0, 0, 0.08);
|
background: rgba(0, 0, 0, 0.06);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
@@ -241,7 +250,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: rgba(0, 0, 0, 0.5);
|
color: rgba(0, 0, 0, 0.4);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
@@ -253,99 +262,32 @@
|
|||||||
|
|
||||||
.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.05);
|
background: rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rl-seg-btn.rl-seg-active {
|
.rl-seg-btn.rl-seg-active {
|
||||||
background: white;
|
background: rgb(30, 30, 30);
|
||||||
color: black;
|
color: rgb(255, 255, 255);
|
||||||
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 */
|
||||||
|
|
||||||
/* Fixes the new Sticky Header Tidal added.. in the shittest jankiest way possible */
|
/* Remove max-width cap on now-playing content */
|
||||||
/* [class*="_stickyHeader"] {
|
[class*="_contentInner"] {
|
||||||
background: transparent !important;
|
max-width: none !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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[class*="_playQueueItems"]{
|
/* Round now-playing artwork corners */
|
||||||
border-radius: 2.5px 0 0 0 !important;
|
[data-test="now-playing-artwork"] {
|
||||||
|
/* biome-ignore lint: Override flat corners */
|
||||||
|
border-radius: 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-test="playqueue-sticky-clear-active-items"] {
|
/* Hide the Overlay Scrollbar (people just use mouse scroll) */
|
||||||
visibility: collapse !important;
|
.os-scrollbar {
|
||||||
width: 0px !important;
|
display: none !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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user