1 Commits

Author SHA1 Message Date
vMohammad24 24aabe67fd feat(audioVisualization): add spotify support and linear animations 2025-06-12 15:29:32 +03:00
34 changed files with 3863 additions and 10079 deletions
+4 -2
View File
@@ -1,4 +1,6 @@
node_modules/
dist/
Notes.md
/Reference/
dist/itzzexcel.oled-theme.json
dist/itzzexcel.oled-theme.mjs
dist/itzzexcel.oled-theme.mjs.map
dist/store.json
-4
View File
@@ -1,4 +0,0 @@
{
"snyk.advanced.organization": "5e35d31d-0df9-4f3c-b4b3-168a24114801",
"snyk.advanced.autoSelectOrganization": true
}
+13 -32
View File
@@ -4,13 +4,14 @@ A collection of Luna plugins for Tidal, ported from Neptune framework.
## Plugins
### 🎨 Obsidian
**Location:** `plugins/obsidian-theme-luna/`
### 🎨 OLED Theme
**Location:** `plugins/oled-theme-luna/`
A dark OLED-friendly theme that transforms Tidal Luna's appearance.
A dark OLED-friendly theme plugin that transforms Tidal Luna's appearance.
**Features:**
- Applies a dark, OLED-optimized theme
- Fetches the latest theme CSS from the GitHub repository
- Reduces battery consumption on OLED displays.. i guess <3
- Modern, sleek dark interface
@@ -33,16 +34,6 @@ Allows users to copy song lyrics by selecting them directly in the interface.
- Automatic clipboard copying of selected lyrics
- Smart lyric span detection
### 🧽 Element Hider
**Location:** `plugins/element-hider-luna/`
Allows users to hide/remove UI elements by right clicking on them.
**Features:**
- Remove/Hide ANY UI element
- Automagically saves hidden elements
- Allows for elements to be restored
### 🎶 Audio Visualizer
**Location:** `plugins/audio-visualizer-luna/`
@@ -58,21 +49,8 @@ Allows users to hide/remove UI elements by right clicking on them.
## Installation
### Batteries Required
1. [TidaLuna](https://github.com/Inrixia/TidaLuna) - Plugin Framework for Tidal (what these plugins are for)
2. Tidal - Streaming Service (if you are here and dont use tidal.. then just enjoy the read <3)
### Installing from Plugin Store (in TidaLuna)
1. Open Tidal (with Luna installed)
2. Navigate to Luna Settings (Top right of Tidal)
3. Click "Plugin Store" Tab
4. Scroll Down and just click on the plugins to install them
5. Naviagte to the "Plugins" Tab
6. And now your done and you can adjust the settings to your liking <3
### Installing from URL
### (They are in the store by default now)
1. Open TidaLuna after Building & Serving
1. Open TidalLuna after Building & Serving
2. Navigate to Luna Settings (Top right of Tidal)
3. Click "Plugin Store" Tab
4. Paste in the "Install from URL" Bar `https://github.com/meowarex/tidalluna-plugins/releases/download/latest/store.json`
@@ -85,7 +63,7 @@ Allows users to hide/remove UI elements by right clicking on them.
git clone https://github.com/meowarex/tidalluna-plugins
# Change Folder to the Repo
cd tidalluna-plugins
cd neptune-projects-fork
# Install dependencies
pnpm install
@@ -95,7 +73,7 @@ pnpm run watch
```
### Installing Plugins in TidalLuna
1. Open TidaLuna after Building & Serving
1. Open TidalLuna after Building & Serving
2. Navigate to Luna Settings (Top right of Tidal)
3. Click "Plugin Store" Tab
4. Click Install on the Plugins at the top Labeled with "[Dev]"
@@ -104,7 +82,7 @@ pnpm run watch
## Development
This project is made for:
- **[TidaLuna](https://github.com/Inrixia/TidaLuna)** - Modern plugin framework for Tidal | Inrixia
- **TidalLuna** - Modern plugin framework for Tidal | Inrixia
## GitHub Actions
@@ -112,7 +90,10 @@ This project is made for:
- **Release automation** for distributing plugins
- **Artifact uploads** for easy plugin distribution
## Based On <3
- **itzzexcel** - [GitHub](https://github.com/ItzzExcel)
## Credits
Inrixia | [TidalLuna](https://github.com/Inrixia/TidaLuna) Plugin Framework (The successor Neptune)
ItzzExcel | The Original Neptune version of "Radiant Lyrics" (Which was ported to Luna and Rewritten by me!)
Original Neptune versions by itzzexcel. Ported to Luna framework following the Luna plugin template structure by meowarex with help from Inrixia <3
-9
View File
@@ -1,9 +0,0 @@
{
"linter": {
"rules": {
"complexity": {
"useArrowFunction": "off"
}
}
}
}
-2356
View File
File diff suppressed because it is too large Load Diff
+1 -4
View File
@@ -18,8 +18,5 @@
"rimraf": "^6.0.1",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
},
"dependencies": {
"pnpm": "^10.14.0"
}
}
}
+1 -1
View File
@@ -8,4 +8,4 @@
},
"main": "./src/index.ts",
"type": "module"
}
}
+357 -421
View File
@@ -1,436 +1,372 @@
import { ReactiveStore } from "@luna/core";
import {
LunaSettings,
LunaNumberSetting,
LunaSwitchSetting,
LunaTextSetting,
} from "@luna/ui";
import { LunaNumberSetting, LunaSettings, LunaSwitchSetting } from "@luna/ui";
import React from "react";
export const settings = await ReactiveStore.getPluginStorage(
"AudioVisualizer",
{
barCount: 32,
barColor: "#ffffff",
barRounding: true,
customColors: [] as string[],
},
);
const isWindows = navigator.userAgent.includes("Windows"); // tidal changes it in reqs navigator supplies the default electron one
export const settings = await ReactiveStore.getPluginStorage("AudioVisualizer", {
barCount: 32,
barColor: "#ffffff",
barRounding: true,
customColors: [] as string[],
spotifyAPI: isWindows
});
export const Settings = () => {
const [barCount, setBarCount] = React.useState(settings.barCount);
const [barColor, setBarColor] = React.useState(settings.barColor);
const [barRounding, setBarRounding] = React.useState(settings.barRounding);
const [showColorPicker, setShowColorPicker] = React.useState(false);
const [isAnimatingIn, setIsAnimatingIn] = React.useState(false);
const [shouldRender, setShouldRender] = React.useState(false);
const [customInput, setCustomInput] = React.useState(settings.barColor);
const [customColors, setCustomColors] = React.useState(settings.customColors);
const [hoveredColorIndex, setHoveredColorIndex] = React.useState<
number | null
>(null);
const [barCount, setBarCount] = React.useState(settings.barCount);
const [barColor, setBarColor] = React.useState(settings.barColor);
const [barRounding, setBarRounding] = React.useState(settings.barRounding);
const [spotifyAPI, setSpotifyAPI] = React.useState(settings.spotifyAPI);
const [showColorPicker, setShowColorPicker] = React.useState(false);
const [isAnimatingIn, setIsAnimatingIn] = React.useState(false);
const [shouldRender, setShouldRender] = React.useState(false);
const [customInput, setCustomInput] = React.useState(settings.barColor);
const [customColors, setCustomColors] = React.useState(settings.customColors);
const [hoveredColorIndex, setHoveredColorIndex] = React.useState<number | null>(null);
const closeColorPicker = () => {
setIsAnimatingIn(false);
setTimeout(() => {
setShowColorPicker(false);
setShouldRender(false);
}, 200); // Wait for animation to complete because i need to
};
const closeColorPicker = () => {
setIsAnimatingIn(false);
setTimeout(() => {
setShowColorPicker(false);
setShouldRender(false);
}, 200); // Wait for animation to complete because i need to
};
const openColorPicker = () => {
setShowColorPicker(true);
setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10);
};
const openColorPicker = () => {
setShowColorPicker(true);
setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10);
};
React.useEffect(() => {
if (showColorPicker) {
setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10);
}
}, [showColorPicker]);
React.useEffect(() => {
if (showColorPicker) {
setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10);
}
}, [showColorPicker]);
// Common color presets for cool points :D
const colorPresets = [
"#ffffff",
"#ff0000",
"#00ff00",
"#0000ff",
"#ffff00",
"#ff00ff",
"#00ffff",
"#ff8800",
"#8800ff",
"#0088ff",
"#88ff00",
"#ff0088",
"#00ff88",
"#444444",
"#888888",
"#cccccc",
"#1db954",
"#e22134",
"#1976d2",
];
// Common color presets for cool points :D
const colorPresets = [
"#ffffff", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff",
"#ff8800", "#8800ff", "#0088ff", "#88ff00", "#ff0088", "#00ff88",
"#444444", "#888888", "#cccccc", "#1db954", "#e22134", "#1976d2"
];
const updateColor = (color: string) => {
setBarColor(color);
setCustomInput(color);
settings.barColor = color;
(window as any).updateAudioVisualizer?.();
};
const updateColor = (color: string) => {
setBarColor(color);
setCustomInput(color);
settings.barColor = color;
(window as any).updateAudioVisualizer?.();
};
const addCustomColor = () => {
if (customInput) {
// Trim whitespace and convert to lowercase
const trimmedInput = customInput.trim().toLowerCase();
const addCustomColor = () => {
if (customInput) {
// Trim whitespace and convert to lowercase
const trimmedInput = customInput.trim().toLowerCase();
// Validate hex color format
const hexColorRegex = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
if (hexColorRegex.test(trimmedInput) &&
!colorPresets.includes(trimmedInput) &&
!customColors.includes(trimmedInput)) {
const newCustomColors = [...customColors, trimmedInput];
setCustomColors(newCustomColors);
settings.customColors = newCustomColors;
}
}
};
// Validate hex color format
const hexColorRegex = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
const removeCustomColor = (colorToRemove: string) => {
const newCustomColors = customColors.filter(color => color !== colorToRemove);
setCustomColors(newCustomColors);
settings.customColors = newCustomColors;
// If the removed color was the selected color (reset to white)
if (barColor === colorToRemove) {
updateColor("#ffffff");
}
};
if (
hexColorRegex.test(trimmedInput) &&
!colorPresets.includes(trimmedInput) &&
!customColors.includes(trimmedInput)
) {
const newCustomColors = [...customColors, trimmedInput];
setCustomColors(newCustomColors);
settings.customColors = newCustomColors;
}
}
};
const allColors = [...colorPresets, ...customColors];
const removeCustomColor = (colorToRemove: string) => {
const newCustomColors = customColors.filter(
(color) => color !== colorToRemove,
);
setCustomColors(newCustomColors);
settings.customColors = newCustomColors;
return (
<LunaSettings> <LunaSwitchSetting
title="Spotify API"
desc="Use Spotify's audio analysis API instead of real-time audio data (Required for Windows)"
// @ts-expect-error no idea why this errosr wth
checked={spotifyAPI}
disabled={isWindows} // Disable on non-Windows platforms
// @ts-expect-error no idea why this errosr wth
onChange={(_, checked) => {
setSpotifyAPI(checked);
settings.spotifyAPI = checked;
(window as any).updateAudioVisualizer?.();
}}
/>
// If the removed color was the selected color (reset to white)
if (barColor === colorToRemove) {
updateColor("#ffffff");
}
};
<LunaSwitchSetting
title="Bar Roundness"
desc="Enable rounded corners on visualizer bars"
// @ts-expect-error no idea why this errosr wth
checked={barRounding}
// @ts-expect-error no idea why this errosr wth
onChange={(_, checked) => {
setBarRounding(checked);
settings.barRounding = checked;
(window as any).updateAudioVisualizer?.();
}}
/>
<LunaNumberSetting
title="Bar Count"
desc="Number of frequency bars to display"
min={8}
max={64}
step={1}
value={barCount}
onNumber={(value: number) => {
setBarCount(value);
settings.barCount = value;
(window as any).updateAudioVisualizer?.();
}}
/>
{/* YUP YOUR EYES WORK... we do be using React code in the settings..*/}
{/* I'm not sure if this is a good idea, but it works & looks amazing */}
{/* Sorry @Inrixia <3 */}
<div style={{
padding: "16px 0",
display: "flex",
justifyContent: "space-between",
alignItems: "center"
}}>
<div>
<div style={{ fontWeight: "normal", fontSize: "1.075rem", marginBottom: "4px" }}>Bar Color</div>
<div style={{ opacity: 0.7, fontSize: "14px" }}>Color of the visualizer bars</div>
</div>
<div style={{ display: "flex", gap: "8px", alignItems: "center", position: "relative" }}>
<button
onClick={() => showColorPicker ? closeColorPicker() : openColorPicker()}
style={{
width: "32px",
height: "32px",
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: "6px",
cursor: "pointer",
background: barColor,
backdropFilter: "blur(10px)",
WebkitBackdropFilter: "blur(10px)",
position: "relative",
overflow: "hidden"
}}
>
<div style={{
position: "absolute",
inset: 0,
background: "rgba(0,0,0,0.1)",
backdropFilter: "blur(2px)"
}} />
</button>
{/* Custom Color Picker Modal */}
{shouldRender && (
<>
{/* Backdrop */}
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0,0,0,0.6)",
zIndex: 1000,
opacity: isAnimatingIn ? 1 : 0,
transition: "opacity 0.2s ease"
}}
onClick={closeColorPicker}
/>
{/* Color Picker Panel */}
<div style={{
position: "fixed",
top: "50%",
left: "50%",
background: "rgba(20,20,20,0.98)",
backdropFilter: "blur(20px)",
WebkitBackdropFilter: "blur(20px)",
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: "16px",
padding: "20px",
minWidth: "320px",
maxWidth: "90vw",
maxHeight: "90vh",
zIndex: 1001,
boxShadow: "0 20px 40px rgba(0,0,0,0.7)",
opacity: isAnimatingIn ? 1 : 0,
transform: isAnimatingIn ? "translate(-50%, -50%) scale(1)" : "translate(-50%, -50%) scale(0.9)",
transition: "all 0.2s ease"
}}>
<div style={{ marginBottom: "12px", color: "#fff", fontWeight: "bold", fontSize: "14px" }}>
Choose Color
</div>
{/* Color Grid */}
<div style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: "8px",
marginBottom: "16px"
}}>
{allColors.map((color, index) => {
const isCustomColor = customColors.includes(color);
const isHovered = hoveredColorIndex === index;
return (
<div
key={index}
style={{
position: "relative",
width: "32px",
height: "32px",
cursor: "pointer"
}}
className="color-item"
onMouseEnter={() => setHoveredColorIndex(index)}
onMouseLeave={() => setHoveredColorIndex(null)}
>
<button
onClick={() => {
updateColor(color);
closeColorPicker();
}}
style={{
width: "100%",
height: "100%",
borderRadius: "6px",
border: barColor === color ? "2px solid #fff" : "1px solid rgba(255,255,255,0.2)",
background: color,
cursor: "pointer",
transition: "all 0.2s ease"
}}
/>
{isCustomColor && (
<button
onClick={(e) => {
e.stopPropagation();
removeCustomColor(color);
}}
style={{
position: "absolute",
top: "-4px",
right: "-4px",
width: "16px",
height: "16px",
borderRadius: "50%",
border: "1px solid rgba(255,255,255,0.8)",
background: "rgba(0,0,0,0.8)",
color: "#fff",
cursor: "pointer",
fontSize: "10px",
display: "flex",
alignItems: "center",
justifyContent: "center",
opacity: isHovered ? 1 : 0,
transition: "opacity 0.2s ease",
zIndex: 10
}}
className="remove-button"
>
×
</button>
)}
</div>
);
})}
</div>
{/* Custom Hex Input */}
<div style={{ marginBottom: "12px" }}>
<div style={{ color: "rgba(255,255,255,0.7)", fontSize: "12px", marginBottom: "6px" }}>
Add Custom Color
</div>
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
<input
type="text"
value={customInput}
onChange={(e) => setCustomInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
updateColor(customInput);
addCustomColor();
}
}}
placeholder="#ffffff"
style={{
flex: 1,
padding: "8px 12px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.1)",
color: "#fff",
fontSize: "14px",
fontFamily: "monospace",
boxSizing: "border-box"
}}
/>
<button
onClick={() => {
updateColor(customInput);
addCustomColor();
}}
style={{
width: "32px",
height: "32px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.3)",
background: "rgba(255,255,255,0.15)",
color: "#fff",
cursor: "pointer",
fontSize: "16px",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.2s ease"
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.25)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.15)";
}}
>
+
</button>
</div>
</div>
{/* Close Button (Done) - Also runs when color chosen*/}
<button
onClick={closeColorPicker}
style={{
width: "100%",
padding: "8px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.1)",
color: "#fff",
cursor: "pointer",
fontSize: "12px"
}}
>
Done
</button>
</div>
</>
)}
</div>
</div>
const allColors = [...colorPresets, ...customColors];
return (
<LunaSettings>
<LunaSwitchSetting
title="Bar Roundness"
desc="Enable rounded corners on visualizer bars"
checked={barRounding}
onChange={(_, checked) => {
setBarRounding(checked);
settings.barRounding = checked;
(window as any).updateAudioVisualizer?.();
}}
/>
<LunaNumberSetting
title="Bar Count"
desc="Number of frequency bars to display"
min={8}
max={64}
step={1}
value={barCount}
onNumber={(value: number) => {
setBarCount(value);
settings.barCount = value;
(window as any).updateAudioVisualizer?.();
}}
/>
{/* YUP YOUR EYES WORK... we do be using React code in the settings..*/}
{/* I'm not sure if this is a good idea, but it works & looks amazing */}
{/* Sorry @Inrixia <3 */}
<div
style={{
padding: "16px 0",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div>
<div
style={{
fontWeight: "normal",
fontSize: "1.075rem",
marginBottom: "4px",
}}
>
Bar Color
</div>
<div style={{ opacity: 0.7, fontSize: "14px" }}>
Color of the visualizer bars
</div>
</div>
<div
style={{
display: "flex",
gap: "8px",
alignItems: "center",
position: "relative",
}}
>
<button
onClick={() =>
showColorPicker ? closeColorPicker() : openColorPicker()
}
style={{
width: "32px",
height: "32px",
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: "6px",
cursor: "pointer",
background: barColor,
backdropFilter: "blur(10px)",
WebkitBackdropFilter: "blur(10px)",
position: "relative",
overflow: "hidden",
}}
>
<div
style={{
position: "absolute",
inset: 0,
background: "rgba(0,0,0,0.1)",
backdropFilter: "blur(2px)",
}}
/>
</button>
{/* Custom Color Picker Modal */}
{shouldRender && (
<>
{/* Backdrop */}
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0,0,0,0.6)",
zIndex: 1000,
opacity: isAnimatingIn ? 1 : 0,
transition: "opacity 0.2s ease",
}}
onClick={closeColorPicker}
/>
{/* Color Picker Panel */}
<div
style={{
position: "fixed",
top: "50%",
left: "50%",
background: "rgba(20,20,20,0.98)",
backdropFilter: "blur(20px)",
WebkitBackdropFilter: "blur(20px)",
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: "16px",
padding: "20px",
minWidth: "320px",
maxWidth: "90vw",
maxHeight: "90vh",
zIndex: 1001,
boxShadow: "0 20px 40px rgba(0,0,0,0.7)",
opacity: isAnimatingIn ? 1 : 0,
transform: isAnimatingIn
? "translate(-50%, -50%) scale(1)"
: "translate(-50%, -50%) scale(0.9)",
transition: "all 0.2s ease",
}}
>
<div
style={{
marginBottom: "12px",
color: "#fff",
fontWeight: "bold",
fontSize: "14px",
}}
>
Choose Color
</div>
{/* Color Grid */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: "8px",
marginBottom: "16px",
}}
>
{allColors.map((color, index) => {
const isCustomColor = customColors.includes(color);
const isHovered = hoveredColorIndex === index;
return (
<div
key={index}
style={{
position: "relative",
width: "32px",
height: "32px",
cursor: "pointer",
}}
className="color-item"
onMouseEnter={() => setHoveredColorIndex(index)}
onMouseLeave={() => setHoveredColorIndex(null)}
>
<button
onClick={() => {
updateColor(color);
closeColorPicker();
}}
style={{
width: "100%",
height: "100%",
borderRadius: "6px",
border:
barColor === color
? "2px solid #fff"
: "1px solid rgba(255,255,255,0.2)",
background: color,
cursor: "pointer",
transition: "all 0.2s ease",
}}
/>
{isCustomColor && (
<button
onClick={(e) => {
e.stopPropagation();
removeCustomColor(color);
}}
style={{
position: "absolute",
top: "-4px",
right: "-4px",
width: "16px",
height: "16px",
borderRadius: "50%",
border: "1px solid rgba(255,255,255,0.8)",
background: "rgba(0,0,0,0.8)",
color: "#fff",
cursor: "pointer",
fontSize: "10px",
display: "flex",
alignItems: "center",
justifyContent: "center",
opacity: isHovered ? 1 : 0,
transition: "opacity 0.2s ease",
zIndex: 10,
}}
className="remove-button"
>
×
</button>
)}
</div>
);
})}
</div>
{/* Custom Hex Input */}
<div style={{ marginBottom: "12px" }}>
<div
style={{
color: "rgba(255,255,255,0.7)",
fontSize: "12px",
marginBottom: "6px",
}}
>
Add Custom Color
</div>
<div
style={{
display: "flex",
gap: "8px",
alignItems: "center",
}}
>
<input
type="text"
value={customInput}
onChange={(e) => setCustomInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
updateColor(customInput);
addCustomColor();
}
}}
placeholder="#ffffff"
style={{
flex: 1,
padding: "8px 12px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.1)",
color: "#fff",
fontSize: "14px",
fontFamily: "monospace",
boxSizing: "border-box",
}}
/>
<button
onClick={() => {
updateColor(customInput);
addCustomColor();
}}
style={{
width: "32px",
height: "32px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.3)",
background: "rgba(255,255,255,0.15)",
color: "#fff",
cursor: "pointer",
fontSize: "16px",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.2s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background =
"rgba(255,255,255,0.25)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background =
"rgba(255,255,255,0.15)";
}}
>
+
</button>
</div>
</div>
{/* Close Button (Done) - Also runs when color chosen*/}
<button
onClick={closeColorPicker}
style={{
width: "100%",
padding: "8px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.1)",
color: "#fff",
cursor: "pointer",
fontSize: "12px",
}}
>
Done
</button>
</div>
</>
)}
</div>
</div>
</LunaSettings>
);
};
</LunaSettings>
);
};
File diff suppressed because it is too large Load Diff
+40 -34
View File
@@ -1,50 +1,56 @@
/* Audio Visualizer CSS */
/* Audio Visualizer CSS - Only applies to the Visualizer */
.audio-visualizer-container {
transition: all 0.3s ease-in-out;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
animation: av-fadeIn 0.5s ease-out;
#audio-visualizer-container {
transition: all 0.3s ease-in-out;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.audio-visualizer-container:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
#audio-visualizer-container:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.audio-visualizer-container canvas {
display: block;
transition: all 0.3s ease-in-out;
#audio-visualizer-container canvas {
display: block;
transition: all 0.3s ease-in-out;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.audio-visualizer-container {
margin: 4px;
padding: 2px;
}
.audio-visualizer-container canvas {
max-width: 150px;
max-height: 30px;
}
#audio-visualizer-container {
margin: 4px;
padding: 2px;
}
#audio-visualizer-container canvas {
max-width: 150px;
max-height: 30px;
}
}
.audio-visualizer-container.active {
box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
/* Where to put the thingy */
[class*="_searchField"] {
transition: all 0.3s ease-in-out;
}
@keyframes av-fadeIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
/* 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);
}
[data-type="search-field"] {
min-width: 220px !important;
/* Fade in animation */
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
#audio-visualizer-container {
animation: fadeIn 0.5s ease-out;
}
@@ -1,378 +0,0 @@
import { ReactiveStore } from "@luna/core";
import { LunaSettings, LunaSwitchSetting } from "@luna/ui";
import React from "react";
declare global {
interface Window {
applyColoramaLyrics?: () => void;
}
}
type SwitchChangeHandler = (
event: React.ChangeEvent<HTMLInputElement> | null,
checked: boolean,
) => void;
export const settings = await ReactiveStore.getPluginStorage("ColoramaLyrics", {
enabled: true,
singleColor: "#FFFFFF",
singleAlpha: 100,
customColors: [] as string[],
excludeInactive: false,
});
export const Settings = () => {
const [singleColor, setSingleColor] = React.useState(settings.singleColor);
const [singleAlpha, setSingleAlpha] = React.useState<number>(
settings.singleAlpha ?? 100,
);
const [customInput, setCustomInput] = React.useState(settings.singleColor);
const [customColors, setCustomColors] = React.useState(settings.customColors);
const [showPicker, setShowPicker] = React.useState(false);
const [isAnimatingIn, setIsAnimatingIn] = React.useState(false);
const [shouldRender, setShouldRender] = React.useState(false);
const [excludeInactive, setExcludeInactive] = React.useState(
settings.excludeInactive,
);
const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType<{
title: string;
desc?: string;
checked: boolean;
onChange: SwitchChangeHandler;
}>;
const normalizeToRGB = (
hex: string,
fallback: string = "#FFFFFF",
): string => {
let v = hex.trim().toLowerCase();
if (!v.startsWith("#")) v = `#${v}`;
if (/^#([0-9a-f]{3,4})$/.test(v)) {
const m = v.slice(1);
const r = m[0];
const g = m[1];
const b = m[2];
return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
}
if (/^#([0-9a-f]{8})$/.test(v)) {
const rrggbb = v.slice(3);
return `#${rrggbb}`.toUpperCase();
}
if (/^#([0-9a-f]{6})$/.test(v)) return v.toUpperCase();
return fallback;
};
const colorPresets = [
"#FFFFFF",
"#FF0000",
"#00FF00",
"#0000FF",
"#FFFF00",
"#FF00FF",
"#00FFFF",
"#FF8800",
"#8800FF",
"#0088FF",
"#88FF00",
"#FF0088",
"#00FF88",
"#444444",
"#888888",
"#CCCCCC",
"#1DB954",
"#E22134",
"#1976D2",
];
const openPicker = () => {
setShowPicker(true);
setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10);
};
const closePicker = () => {
setIsAnimatingIn(false);
setTimeout(() => {
setShowPicker(false);
setShouldRender(false);
}, 200);
};
const hexColorRegex = /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3,4})$/i;
const applyCustomInputColor = (raw: string, updateInput: boolean): void => {
const trimmed = raw.trim();
if (!hexColorRegex.test(trimmed)) return;
const next = normalizeToRGB(trimmed);
settings.singleColor = next;
setSingleColor(next);
if (updateInput) setCustomInput(next);
requestApply();
};
const addCustomColor = () => {
const trimmed = customInput.trim();
if (
hexColorRegex.test(trimmed) &&
!colorPresets.includes(trimmed) &&
!customColors.includes(normalizeToRGB(trimmed))
) {
const updated = [...customColors, normalizeToRGB(trimmed)];
setCustomColors(updated);
settings.customColors = updated;
}
};
const allColors = [...colorPresets, ...customColors];
const requestApply = () => {
window.applyColoramaLyrics?.();
};
return (
<LunaSettings>
{/* Single color picker button */}
<div
style={{
padding: "8px 0",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div>
<div
style={{
fontWeight: "normal",
fontSize: "1.075rem",
marginBottom: 4,
}}
>
Lyrics Color
</div>
<div style={{ opacity: 0.7, fontSize: 14 }}>Set lyrics color</div>
</div>
<div
style={{
display: "flex",
gap: 8,
alignItems: "center",
position: "relative",
}}
>
<button
type="button"
onClick={() => (showPicker ? closePicker() : openPicker())}
style={{
width: 32,
height: 32,
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: 6,
cursor: "pointer",
background: normalizeToRGB(singleColor),
}}
/>
</div>
</div>
{/* Color picker modal */}
{shouldRender && (
<>
<button
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0,0,0,0.6)",
zIndex: 1000,
opacity: isAnimatingIn ? 1 : 0,
transition: "opacity 0.2s ease",
}}
type="button"
aria-label="Close color picker"
onClick={closePicker}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === "Escape") closePicker();
}}
/>
<div
style={{
position: "fixed",
top: "50%",
left: "50%",
background: "rgba(20,20,20,0.98)",
backdropFilter: "blur(20px)",
WebkitBackdropFilter: "blur(20px)",
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: 16,
padding: 20,
minWidth: 320,
maxWidth: "90vw",
maxHeight: "90vh",
zIndex: 1001,
boxShadow: "0 20px 40px rgba(0,0,0,0.7)",
opacity: isAnimatingIn ? 1 : 0,
transform: isAnimatingIn
? "translate(-50%, -50%) scale(1)"
: "translate(-50%, -50%) scale(0.9)",
transition: "all 0.2s ease",
}}
>
<div
style={{
marginBottom: 12,
color: "#fff",
fontWeight: "bold",
fontSize: 14,
}}
>
Lyrics Color
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: 8,
marginBottom: 16,
}}
>
{allColors.map((color) => (
<button
key={color}
type="button"
onClick={() => {
const next = normalizeToRGB(color);
settings.singleColor = next;
setSingleColor(next);
setCustomInput(next);
requestApply();
}}
style={{
width: 32,
height: 32,
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.2)",
background: normalizeToRGB(color),
cursor: "pointer",
}}
/>
))}
</div>
<div style={{ marginBottom: 12 }}>
<div
style={{
color: "rgba(255,255,255,0.7)",
fontSize: 12,
marginBottom: 6,
}}
>
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();
}}
style={{
width: 32,
height: 32,
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.3)",
background: "rgba(255,255,255,0.15)",
color: "#fff",
cursor: "pointer",
fontSize: 16,
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.2s ease",
}}
type="button"
>
+
</button>
</div>
</div>
<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
onClick={closePicker}
style={{
width: "100%",
padding: 8,
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.1)",
color: "#fff",
cursor: "pointer",
fontSize: 12,
}}
type="button"
>
Done
</button>
</div>
</>
)}
<AnySwitch
title="Exclude Inactive"
desc="Apply color only to the currently active lyric line"
checked={excludeInactive}
onChange={(_event: React.ChangeEvent<HTMLInputElement> | null, checked: boolean) => {
settings.excludeInactive = checked;
setExcludeInactive(checked);
requestApply();
}}
/>
</LunaSettings>
);
};
-99
View File
@@ -1,99 +0,0 @@
import { LunaUnload, Tracer } from "@luna/core";
import { StyleTag } from "@luna/lib";
import { settings, Settings } from "./Settings";
import styles from "file://styles.css?minify";
export const { trace } = Tracer("[Colorama Lyrics]");
export { Settings };
export const unloads = new Set<LunaUnload>();
new StyleTag("ColoramaLyrics", unloads, styles);
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
let v = hex.trim();
if (!v.startsWith("#")) v = `#${v}`;
if (/^#([0-9a-fA-F]{3})$/.test(v)) {
const r = parseInt(v[1] + v[1], 16);
const g = parseInt(v[2] + v[2], 16);
const b = parseInt(v[3] + v[3], 16);
return { r, g, b };
}
if (/^#([0-9a-fA-F]{6})$/.test(v)) {
const r = parseInt(v.slice(1, 3), 16);
const g = parseInt(v.slice(3, 5), 16);
const b = parseInt(v.slice(5, 7), 16);
return { r, g, b };
}
if (/^#([0-9a-fA-F]{8})$/.test(v)) {
const r = parseInt(v.slice(3, 5), 16);
const g = parseInt(v.slice(5, 7), 16);
const b = parseInt(v.slice(7, 9), 16);
return { r, g, b };
}
return null;
}
function rgbaFromHexAndAlpha(
hex: string,
alphaPercent: number | undefined,
): string {
const rgb = hexToRgb(hex);
const a = Math.max(0.05, Math.min(100, alphaPercent ?? 100)) / 100;
if (!rgb) return `rgba(255,255,255,${a})`;
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`;
}
function applySingleColor(color: string) {
const alpha = (settings as any).singleAlpha ?? 100;
const rgba = rgbaFromHexAndAlpha(color, alpha);
document.documentElement.style.setProperty("--cl-lyrics-color", rgba);
document.documentElement.style.setProperty("--cl-glow1", rgba);
document.documentElement.style.setProperty("--cl-glow2", rgba);
document.body.classList.add("colorama-single");
}
function applyColoramaLyrics(): void {
if (!settings.enabled) {
document.body.classList.remove("colorama-single");
return;
}
if (settings.excludeInactive) {
document.body.classList.add("colorama-only-active");
} else {
document.body.classList.remove("colorama-only-active");
}
applySingleColor(settings.singleColor);
}
(window as any).applyColoramaLyrics = applyColoramaLyrics;
setTimeout(() => applyColoramaLyrics(), 200);
function hookRadiantUpdates(): void {
const w = window as any;
const wrap = (name: string) => {
const fn = w[name];
if (typeof fn === "function" && !fn.__coloramaPatched) {
const orig = fn.bind(w);
const patched = (...args: unknown[]) => {
const result = orig(...args);
try {
applyColoramaLyrics();
} catch {}
return result;
};
(patched as any).__coloramaPatched = true;
w[name] = patched;
}
};
wrap("updateRadiantLyricsStyles");
wrap("updateRadiantLyricsNowPlayingBackground");
wrap("updateRadiantLyricsGlobalBackground");
wrap("updateRadiantLyricsTextGlow");
}
setTimeout(() => hookRadiantUpdates(), 0);
-117
View File
@@ -1,117 +0,0 @@
/* Variables used by Colorama Lyrics */
:root {
--cl-lyrics-color: #ffffff;
--cl-glow1: #ffffff;
--cl-glow2: #ffffff;
}
/* Apply solid color to lyrics text */
.colorama-single [class*="_lyricsText"] > div > span,
.colorama-single [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-single [class^="_lyricsContainer"] > div > div > span,
.colorama-single
[class^="_lyricsContainer"]
> div
> div
> span[data-current="true"] {
color: var(--cl-lyrics-color) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
}
/* Color Radiant glow shadows using Colorama colors (respect RL sizes) */
.colorama-single [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-single
[class^="_lyricsContainer"]
> div
> div
> span[data-current="true"] {
text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
}
/* Hover: force glow color to match Colorama settings for inactive lines */
.colorama-single [class*="_lyricsText"] > div > span:hover,
.colorama-single [class^="_lyricsContainer"] > div > div > span:hover {
color: var(--cl-lyrics-color) !important;
text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
}
/* MARKER: Radiant WBW Lyrics Support */
/* Single color: active wbw words & syllable finished */
.colorama-single .rl-wbw-word.rl-wbw-active,
.colorama-single .rl-wbw-word.rl-syl-finished {
color: var(--cl-lyrics-color) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
}
/* Single color: glow on active wbw words */
.colorama-single .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 */
.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 {
color: var(--cl-lyrics-color) !important;
text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
}
/* Only-active: wbw words on inactive lines stay default */
body.colorama-only-active.colorama-single
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word {
color: rgba(128, 128, 128, 0.4) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
}
/* Only-active: hover on inactive wbw lines keeps default */
body.colorama-only-active.colorama-single
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover {
color: lightgray !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
}
/* Only color active line mode */
body.colorama-only-active.colorama-single [class*="_lyricsText"]
> div
> span:not([data-current="true"]) {
color: rgba(128, 128, 128, 0.4) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
}
/* In only-active mode, keep TIDAL defaults even on hover for inactive lines */
body.colorama-only-active.colorama-single [class*="_lyricsText"]
> div
> span:not([data-current="true"]):hover {
color: lightgray !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
}
+1 -1
View File
@@ -8,4 +8,4 @@
},
"main": "./src/index.ts",
"type": "module"
}
}
+84 -109
View File
@@ -1,4 +1,4 @@
import { type LunaUnload, Tracer } from "@luna/core";
import { LunaUnload, Tracer } from "@luna/core";
import { StyleTag } from "@luna/lib";
// Import CSS directly using Luna's file:// syntax - Took me a while to figure out <3
@@ -9,135 +9,110 @@ export const { trace } = Tracer("[Copy Lyrics]");
// clean up resources
export const unloads = new Set<LunaUnload>();
// Style injection via side effect
new StyleTag("Copy-Lyrics", unloads, unlockSelection);
// StyleTag for lyrics selection styling
const lyricsStyleTag = new StyleTag("Copy-Lyrics", unloads, unlockSelection);
function SetClipboard(text: string): void {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed"; // Avoid scrolling to bottom
document.body.appendChild(textarea);
textarea.select();
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed"; // Avoid scrolling to bottom
document.body.appendChild(textarea);
textarea.select();
try {
const success = document.execCommand("copy");
if (!success) throw new Error("Failed to copy text.");
} catch (err) {
trace.msg.err(err instanceof Error ? err.message : String(err));
} finally {
document.body.removeChild(textarea);
}
try {
const success = document.execCommand("copy");
if (!success) throw new Error("Failed to copy text.");
} catch (err) {
trace.msg.err(err instanceof Error ? err.message : String(err));
} finally {
document.body.removeChild(textarea);
}
}
let isSelecting = false;
const onMouseDown = (): void => {
isSelecting = true;
const onMouseDown = function (): void {
isSelecting = true;
};
const onMouseUp = (): void => {
if (isSelecting) {
const selection = window.getSelection();
if (selection?.toString().length > 0) {
const selectedSpans: HTMLSpanElement[] = [];
const range = selection.getRangeAt(0);
let container: Node | null = range.commonAncestorContainer;
const onMouseUp = function (event: MouseEvent): void {
if (isSelecting) {
const selection = window.getSelection();
if (selection && selection.toString().length > 0) {
const selectedSpans: HTMLSpanElement[] = [];
const range = selection.getRangeAt(0);
let container = range.commonAncestorContainer;
// If the container is NOT an element and a document, adjust it.
if (
container.nodeType !== Node.ELEMENT_NODE &&
container.nodeType !== Node.DOCUMENT_NODE
) {
// Get the parent element if it's a text node
const parentElement = container.parentElement;
if (parentElement && parentElement.hasAttribute("data-current")) {
let text_ = selection.toString().trim();
SetClipboard(text_);
trace.msg.log("Copied to clipboard!");
return;
}
}
// Normalize container: if it's a text node, use its parent element/node
if (container && container.nodeType === Node.TEXT_NODE) {
container = (container.parentElement ?? container.parentNode) as Node | null;
}
// Get all the spans inside the container.
const spans = (container as Element).getElementsByTagName("span");
for (let span of spans) {
if (selection.containsNode(span, true)) {
selectedSpans.push(span as HTMLSpanElement);
}
}
// If parent has data-current, treat as single-line copy case
if (
container &&
container.nodeType === Node.ELEMENT_NODE &&
(container as Element).hasAttribute("data-current")
) {
const text_ = selection.toString().trim();
SetClipboard(text_);
trace.msg.log("Copied to clipboard!");
return;
}
// Concat the text of the selected spans.
let hasCorrectAttribute = false;
let text = "";
selectedSpans.forEach((span) => {
if (span.hasAttribute("data-current")) {
hasCorrectAttribute = true;
text += span.textContent + "\n";
if ([...span.classList].some((className) => className.startsWith("endOfStanza--"))) {
text += "\n";
}
}
});
// Ensure we have an Element or Document before querying
if (
!container ||
(container.nodeType !== Node.ELEMENT_NODE &&
container.nodeType !== Node.DOCUMENT_NODE)
) {
isSelecting = false;
return;
}
text = text.trim();
// Get all the spans inside the container.
const spans = (container as Element | Document).getElementsByTagName(
"span",
);
for (const span of spans) {
if (selection.containsNode(span, true)) {
selectedSpans.push(span as HTMLSpanElement);
}
}
// Concat the text of the selected spans.
let hasCorrectAttribute = false;
let text = "";
selectedSpans.forEach((span) => {
if (span.hasAttribute("data-current")) {
hasCorrectAttribute = true;
text += span.textContent + "\n";
if (
[...span.classList].some((className) =>
className.startsWith("endOfStanza--"),
)
) {
text += "\n";
}
}
});
text = text.trim();
if (hasCorrectAttribute) {
SetClipboard(text);
trace.msg.log("Copied to clipboard!");
selection.removeAllRanges();
}
}
isSelecting = false;
}
if (hasCorrectAttribute) {
SetClipboard(text);
trace.msg.log("Copied to clipboard!");
selection.removeAllRanges();
}
}
isSelecting = false;
}
};
const onClickHooked = (event: MouseEvent): boolean | undefined => {
if (!isSelecting) return;
const onClickHooked = function (event: MouseEvent): boolean | void {
if (!isSelecting) return;
const target = event.target as HTMLElement;
if (
target.tagName.toLowerCase() === "span" &&
target.hasAttribute("data-current")
) {
// Prevent default behavior and stop event propagation
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
return false;
}
return undefined;
const target = event.target as HTMLElement;
if (target.tagName.toLowerCase() === "span" && target.hasAttribute("data-current")) {
// Prevent default behavior and stop event propagation
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
return false;
}
};
// Add event listener with capture phase to intercept events before they reach other handlers
document.addEventListener("click", onClickHooked, true);
document.addEventListener("mousedown", onMouseDown);
document.addEventListener("mouseup", onMouseUp);
// Add cleanup to unloads
unloads.add((): void => {
// Remove event listeners
document.removeEventListener("click", onClickHooked, true);
document.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mouseup", onMouseUp);
});
unloads.add(() => {
// Remove event listeners
document.removeEventListener("click", onClickHooked, true);
document.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mouseup", onMouseUp);
});
+6 -6
View File
@@ -1,9 +1,9 @@
[class^="_lyricsText"] > div > span {
user-select: text;
cursor: text;
[class^="_lyricsText"]>div>span {
user-select: text;
cursor: text;
}
::selection {
background: rgb(72, 0, 60);
color: rgb(255, 255, 255);
}
background: rgb(72, 0, 60);
color: rgb(255, 255, 255);
}
+1 -1
View File
@@ -8,4 +8,4 @@
},
"main": "./src/index.ts",
"type": "module"
}
}
+2 -2
View File
@@ -9,9 +9,9 @@ export const settings = await ReactiveStore.getPluginStorage("ElementHider", {
className: string;
textContent: string;
timestamp: number;
}>,
}>
});
export const Settings = () => {
return null;
};
};
+192 -259
View File
@@ -1,5 +1,5 @@
import { type LunaUnload, Tracer } from "@luna/core";
import { StyleTag } from "@luna/lib";
import { LunaUnload, Tracer } from "@luna/core";
import { StyleTag, ContextMenu } from "@luna/lib";
import { settings, Settings } from "./Settings";
// Import CSS directly using Luna's file:// syntax
@@ -13,8 +13,8 @@ export { Settings };
// Clean up resources
export const unloads = new Set<LunaUnload>();
// StyleTag for element hider (side-effect)
new StyleTag("Element-Hider", unloads, styles);
// StyleTag for element hider
const styleTag = new StyleTag("Element-Hider", unloads, styles);
// State management
let targetElement: HTMLElement | null = null;
@@ -30,54 +30,39 @@ function generateElementSelector(element: HTMLElement): string {
if (element.id) {
return `#${element.id}`;
}
// Priority 2: data-test attribute (very specific for Tidal <3)
const dataTest = element.getAttribute("data-test");
const dataTest = element.getAttribute('data-test');
if (dataTest) {
return `[data-test="${dataTest}"]`;
}
// Priority 3: Combination of tag + specific classes + position
let selector = element.tagName.toLowerCase();
// Get filtered classes (exclude our temporary classes)
const classes = element.className
? element.className
.trim()
.split(/\s+/)
.filter((cls) => {
return (
cls.length > 0 &&
!cls.startsWith("element-hider-") &&
cls !== "element-hider-target" &&
cls !== "element-hider-hiding" &&
cls !== "element-hider-hidden"
);
})
: [];
const classes = element.className ? element.className.trim().split(/\s+/).filter(cls => {
return cls.length > 0 &&
!cls.startsWith('element-hider-') &&
cls !== 'element-hider-target' &&
cls !== 'element-hider-hiding' &&
cls !== 'element-hider-hidden';
}) : [];
// Only use classes if we have them and they're not generic and dumb
if (classes.length > 0) {
// Use ALL classes to be very specific
selector += "." + classes.join(".");
selector += '.' + classes.join('.');
// Add parent context for extra specificity (for when the element is inside another element)
const parent = element.parentElement;
if (parent && parent.tagName !== "BODY" && parent.tagName !== "HTML") {
const parentClasses = parent.className
? parent.className
.trim()
.split(/\s+/)
.filter((cls) => {
return cls.length > 0 && !cls.startsWith("element-hider-");
})
: [];
if (parent && parent.tagName !== 'BODY' && parent.tagName !== 'HTML') {
const parentClasses = parent.className ? parent.className.trim().split(/\s+/).filter(cls => {
return cls.length > 0 && !cls.startsWith('element-hider-');
}) : [];
if (parentClasses.length > 0) {
const parentSelector =
parent.tagName.toLowerCase() +
"." +
parentClasses.slice(0, 2).join(".");
const parentSelector = parent.tagName.toLowerCase() + '.' + parentClasses.slice(0, 2).join('.');
selector = `${parentSelector} > ${selector}`;
}
}
@@ -85,36 +70,26 @@ function generateElementSelector(element: HTMLElement): string {
// If no useful classes, use position-based selector with parent context
const parent = element.parentElement;
if (parent) {
const siblings = Array.from(parent.children).filter(
(child) => child.tagName === element.tagName,
);
const siblings = Array.from(parent.children).filter(child => child.tagName === element.tagName);
const index = siblings.indexOf(element);
if (index >= 0) {
selector += `:nth-of-type(${index + 1})`;
// Add parent context
if (parent.tagName !== "BODY" && parent.tagName !== "HTML") {
const parentClasses = parent.className
? parent.className
.trim()
.split(/\s+/)
.filter((cls) => {
return cls.length > 0 && !cls.startsWith("element-hider-");
})
: [];
if (parent.tagName !== 'BODY' && parent.tagName !== 'HTML') {
const parentClasses = parent.className ? parent.className.trim().split(/\s+/).filter(cls => {
return cls.length > 0 && !cls.startsWith('element-hider-');
}) : [];
if (parentClasses.length > 0) {
const parentSelector =
parent.tagName.toLowerCase() +
"." +
parentClasses.slice(0, 2).join(".");
const parentSelector = parent.tagName.toLowerCase() + '.' + parentClasses.slice(0, 2).join('.');
selector = `${parentSelector} > ${selector}`;
}
}
}
}
}
trace.log(`Generated specific selector: ${selector}`);
return selector;
}
@@ -125,16 +100,16 @@ function saveHiddenElement(element: HTMLElement): void {
const elementInfo = {
selector: selector,
tagName: element.tagName,
className: element.className || "",
textContent: element.textContent?.substring(0, 100) || "",
timestamp: Date.now(),
className: element.className || '',
textContent: element.textContent?.substring(0, 100) || '',
timestamp: Date.now()
};
// Check if element is already saved
const existingIndex = settings.hiddenElements.findIndex(
(stored) => stored.selector === elementInfo.selector,
stored => stored.selector === elementInfo.selector
);
if (existingIndex === -1) {
settings.hiddenElements.push(elementInfo);
trace.log(`Saved element: ${elementInfo.selector}`);
@@ -144,18 +119,17 @@ function saveHiddenElement(element: HTMLElement): void {
}
}
// Remove hidden element from persistent storage (for unhiding) - currently unused
// function removeSavedElement(element: HTMLElement): void {
// const selector = generateElementSelector(element);
// const index = settings.hiddenElements.findIndex(
// (stored) => stored.selector === selector,
// );
// if (index !== -1) {
// settings.hiddenElements.splice(index, 1);
// trace.log(`Permanently removed: ${selector}`);
// trace.log(`Remaining stored: ${settings.hiddenElements.length}`);
// }
// }
// Remove hidden element from persistent storage (for unhiding)
function removeSavedElement(element: HTMLElement): void {
const selector = generateElementSelector(element);
const index = settings.hiddenElements.findIndex(stored => stored.selector === selector);
if (index !== -1) {
settings.hiddenElements.splice(index, 1);
trace.log(`Permanently removed: ${selector}`);
trace.log(`Remaining stored: ${settings.hiddenElements.length}`);
}
}
// Check if an element matches any stored selector (EXACT match only)
function matchesStoredSelector(element: HTMLElement): boolean {
@@ -169,67 +143,58 @@ function matchesStoredSelector(element: HTMLElement): boolean {
trace.warn(`Invalid selector: ${storedElement.selector}`, error);
}
}
return false;
}
// Hide element directly without animation
function hideElementDirectly(element: HTMLElement): void {
if (hiddenElements.has(element)) return;
element.classList.add("element-hider-hidden");
hiddenElements.add(element);
hiddenElementsArray.push(element);
trace.log(
`Hidden element: ${element.tagName}${element.className ? "." + element.className.split(" ")[0] : ""}`,
);
trace.log(`Hidden element: ${element.tagName}${element.className ? '.' + element.className.split(' ')[0] : ''}`);
}
// Hide the target element with animation
function hideTargetElement(): void {
if (!targetElement) return;
trace.log(
`Hiding with animation: ${targetElement.tagName}${targetElement.className ? "." + targetElement.className.split(" ")[0] : ""}`,
);
trace.log(`Hiding with animation: ${targetElement.tagName}${targetElement.className ? '.' + targetElement.className.split(' ')[0] : ''}`);
// Add hiding animation class
targetElement.classList.add("element-hider-hiding");
// Store reference to the element
const elementToHide = targetElement;
// Save to persistent storage
saveHiddenElement(elementToHide);
// Wait for animation to complete, then hide
setTimeout(() => {
elementToHide.classList.add("element-hider-hidden");
elementToHide.classList.remove(
"element-hider-hiding",
"element-hider-target",
);
elementToHide.classList.remove("element-hider-hiding", "element-hider-target");
hiddenElements.add(elementToHide);
hiddenElementsArray.push(elementToHide);
}, 300);
// Clear target reference
targetElement = null;
}
// Unhide all elements permanently (remove from storage)
function unhideAllElements(): void {
trace.log(
`Permanently unhiding ${settings.hiddenElements.length} saved elements`,
);
trace.log(`Permanently unhiding ${settings.hiddenElements.length} saved elements`);
// Show all currently hidden elements
hiddenElementsArray.forEach((element) => {
hiddenElementsArray.forEach(element => {
if (document.body.contains(element)) {
element.classList.remove("element-hider-hidden", "element-hider-hiding");
}
});
// Clear both storage and runtime collections
settings.hiddenElements = [];
hiddenElements = new WeakSet<HTMLElement>();
@@ -239,42 +204,36 @@ function unhideAllElements(): void {
// Process all elements in the document to hide matching ones (with strict matching)
function processAllElements(): void {
if (settings.hiddenElements.length === 0) return;
trace.log(
`Scanning document for ${settings.hiddenElements.length} stored selectors`,
);
trace.log(`Scanning document for ${settings.hiddenElements.length} stored selectors`);
let hiddenCount = 0;
// Use querySelectorAll for each stored selector with validation
settings.hiddenElements.forEach((storedElement, index) => {
try {
trace.log(`Searching for: ${storedElement.selector}`);
const elements = document.querySelectorAll(storedElement.selector);
trace.log(`Found ${elements.length} matches for selector ${index + 1}`);
// Limit to prevent over-hiding (safety check)
if (elements.length > 10) {
trace.warn(
`Selector too broad (${elements.length} matches), skipping: ${storedElement.selector}`,
);
trace.warn(`Selector too broad (${elements.length} matches), skipping: ${storedElement.selector}`);
return;
}
elements.forEach((element, elemIndex) => {
const htmlElement = element as HTMLElement;
if (!hiddenElements.has(htmlElement)) {
hideElementDirectly(htmlElement);
hiddenCount++;
trace.log(
`Hid element ${elemIndex + 1}/${elements.length} for selector ${index + 1}`,
);
trace.log(`Hid element ${elemIndex + 1}/${elements.length} for selector ${index + 1}`);
}
});
} catch (error) {
trace.warn(`Invalid selector: ${storedElement.selector}`, error);
}
});
if (hiddenCount > 0) {
trace.log(`Total elements hidden: ${hiddenCount}`);
}
@@ -282,19 +241,19 @@ function processAllElements(): void {
// Process new elements that are added to the DOM
function processNewElements(addedNodes: NodeList): void {
addedNodes.forEach((node) => {
addedNodes.forEach(node => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
const element = node as HTMLElement;
// Check the element itself
if (matchesStoredSelector(element)) {
hideElementDirectly(element);
}
// Check all descendant elements
const descendants = element.querySelectorAll("*");
descendants.forEach((descendant) => {
const descendants = element.querySelectorAll('*');
descendants.forEach(descendant => {
if (matchesStoredSelector(descendant as HTMLElement)) {
hideElementDirectly(descendant as HTMLElement);
}
@@ -305,33 +264,26 @@ function processNewElements(addedNodes: NodeList): void {
// Set up reactive element observer
function setupElementObserver(): void {
if (elementObserver) return;
elementObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
processNewElements(mutation.addedNodes);
}
});
});
elementObserver.observe(document.body, {
childList: true,
subtree: true,
subtree: true
});
trace.log(`Set up reactive element observer`);
}
// Global functions
declare global {
interface Window {
showAllElementsFromSettings?: () => void;
debugElementHider?: () => void;
}
}
window.showAllElementsFromSettings = unhideAllElements;
window.debugElementHider = () => {
(window as any).showAllElementsFromSettings = unhideAllElements;
(window as any).debugElementHider = () => {
trace.log(`=== Element Hider Debug Info ===`);
trace.log(`Stored elements: ${settings.hiddenElements.length}`);
trace.log(`Currently hidden elements: ${hiddenElementsArray.length}`);
@@ -345,19 +297,19 @@ window.debugElementHider = () => {
// Handle highlighting target element
function highlightElement(element: HTMLElement): void {
// Remove previous highlights
document.querySelectorAll(".element-hider-target").forEach((el) => {
el.classList.remove("element-hider-target");
document.querySelectorAll('.element-hider-target').forEach(el => {
el.classList.remove('element-hider-target');
});
// Highlight current element
element.classList.add("element-hider-target");
element.classList.add('element-hider-target');
targetElement = element;
}
// Remove highlight
function removeHighlight(): void {
if (targetElement) {
targetElement.classList.remove("element-hider-target");
targetElement.classList.remove('element-hider-target');
targetElement = null;
}
}
@@ -369,70 +321,59 @@ let contextMenuTimeout: number | null = null;
let waitingForBuiltInMenu = false;
// Listen for right-click events to capture the target for context menu
document.addEventListener(
"contextmenu",
(event: MouseEvent) => {
const target = event.target as HTMLElement;
// Don't interfere with native context menus on inputs, textareas, etc.
if (
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable
) {
currentContextElement = null;
return;
document.addEventListener('contextmenu', (event: MouseEvent) => {
const target = event.target as HTMLElement;
// Don't interfere with native context menus on inputs, textareas, etc.
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
currentContextElement = null;
return;
}
// Don't show menu on our own custom menu
if (target.closest(".element-hider-custom-menu")) {
return;
}
// Close any existing custom menu
closeCustomMenu();
// Store the right-clicked element for context menu
currentContextElement = target;
waitingForBuiltInMenu = true;
// Store event coordinates for potential custom menu
const eventX = event.clientX;
const eventY = event.clientY;
// Prevent default immediately if we plan to handle it
event.preventDefault();
// Wait to see if the built-in context menu appears
contextMenuTimeout = window.setTimeout(() => {
// If we're still waiting and no built-in menu appeared, show our custom menu
if (waitingForBuiltInMenu && currentContextElement) {
showCustomMenu(eventX, eventY);
}
// Don't show menu on our own custom menu
if (target.closest(".element-hider-custom-menu")) {
return;
}
// Close any existing custom menu
closeCustomMenu();
// Store the right-clicked element for context menu
currentContextElement = target;
waitingForBuiltInMenu = true;
// Store event coordinates for potential custom menu
const eventX = event.clientX;
const eventY = event.clientY;
// Allow native context menu by default; we'll show our custom menu only if needed
// Wait to see if the built-in context menu appears
contextMenuTimeout = window.setTimeout(() => {
// If we're still waiting and no built-in menu appeared, show our custom menu
if (waitingForBuiltInMenu && currentContextElement) {
showCustomMenu(eventX, eventY);
}
waitingForBuiltInMenu = false;
}, 150); // Wait 150ms for built-in menu
// Don't prevent default initially - let Luna try to handle the context menu
},
true,
);
waitingForBuiltInMenu = false;
}, 150); // Wait 150ms for built-in menu
// Don't prevent default initially - let Luna try to handle the context menu
}, true);
// Listen for clicks to close custom menu
document.addEventListener(
"click",
(event: MouseEvent) => {
const target = event.target as HTMLElement;
// If clicking outside our custom menu, close it
if (customMenu && !target.closest(".element-hider-custom-menu")) {
closeCustomMenu();
removeHighlight();
}
},
true,
);
document.addEventListener('click', (event: MouseEvent) => {
const target = event.target as HTMLElement;
// If clicking outside our custom menu, close it
if (customMenu && !target.closest(".element-hider-custom-menu")) {
closeCustomMenu();
removeHighlight();
}
}, true);
// Handle escape key to close custom menu and remove highlights
document.addEventListener("keydown", (event: KeyboardEvent) => {
document.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === "Escape") {
if (customMenu) {
closeCustomMenu();
@@ -445,7 +386,7 @@ document.addEventListener("keydown", (event: KeyboardEvent) => {
function createCustomMenu(): HTMLElement {
const menu = document.createElement("div");
menu.className = "element-hider-custom-menu";
// Hide Element option
const hideItem = document.createElement("button");
hideItem.className = "element-hider-menu-item";
@@ -457,18 +398,18 @@ function createCustomMenu(): HTMLElement {
closeCustomMenu();
}
});
// Add hover effects for highlighting
hideItem.addEventListener("mouseenter", () => {
if (currentContextElement) {
highlightElement(currentContextElement);
}
});
hideItem.addEventListener("mouseleave", () => {
removeHighlight();
});
// Unhide All Elements option
const unhideAllItem = document.createElement("button");
unhideAllItem.className = "element-hider-menu-item";
@@ -477,28 +418,28 @@ function createCustomMenu(): HTMLElement {
unhideAllElements();
closeCustomMenu();
});
menu.appendChild(hideItem);
menu.appendChild(unhideAllItem);
return menu;
}
// Show custom context menu
function showCustomMenu(x: number, y: number): void {
closeCustomMenu();
customMenu = createCustomMenu();
document.body.appendChild(customMenu);
// Position the menu
const rect = customMenu.getBoundingClientRect();
const finalX = Math.min(x, window.innerWidth - rect.width - 10);
const finalY = Math.min(y, window.innerHeight - rect.height - 10);
customMenu.style.left = `${finalX}px`;
customMenu.style.top = `${finalY}px`;
trace.log(`Context menu opened for: ${currentContextElement?.tagName}`);
}
@@ -508,7 +449,7 @@ function closeCustomMenu(): void {
customMenu.remove();
customMenu = null;
}
if (contextMenuTimeout) {
clearTimeout(contextMenuTimeout);
contextMenuTimeout = null;
@@ -521,18 +462,11 @@ const contextMenuObserver = new MutationObserver((mutations) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as HTMLElement;
// Look for Tidal's context menu
if (
element.matches('[data-test="contextmenu"]') ||
element.querySelector('[data-test="contextmenu"]')
) {
const contextMenu = element.matches('[data-test="contextmenu"]')
? element
: (element.querySelector(
'[data-test="contextmenu"]',
) as HTMLElement);
if (element.matches('[data-test="contextmenu"]') || element.querySelector('[data-test="contextmenu"]')) {
const contextMenu = element.matches('[data-test="contextmenu"]') ? element : element.querySelector('[data-test="contextmenu"]') as HTMLElement;
if (contextMenu && currentContextElement && waitingForBuiltInMenu) {
// Built-in menu appeared, cancel custom menu timeout
waitingForBuiltInMenu = false;
@@ -551,8 +485,8 @@ const contextMenuObserver = new MutationObserver((mutations) => {
// Add our options to the existing context menu
function addElementHiderOptions(contextMenu: HTMLElement): void {
// Create hide element button
const hideButton = document.createElement("button");
hideButton.className = "element-hider-menu-item";
const hideButton = document.createElement('button');
hideButton.className = 'element-hider-menu-item';
hideButton.style.cssText = `
display: flex;
align-items: center;
@@ -568,47 +502,46 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
font-size: 14px;
`;
hideButton.innerHTML = `Hide This Element`;
hideButton.addEventListener("click", () => {
hideButton.addEventListener('click', () => {
if (currentContextElement) {
targetElement = currentContextElement;
hideTargetElement();
}
});
// Add hover effects for highlighting
hideButton.addEventListener("mouseenter", () => {
hideButton.style.background = "var(--wave-color-background-hover, #3a3a3a)";
hideButton.addEventListener('mouseenter', () => {
hideButton.style.background = 'var(--wave-color-background-hover, #3a3a3a)';
if (currentContextElement) {
highlightElement(currentContextElement);
}
});
hideButton.addEventListener("mouseleave", () => {
hideButton.style.background = "transparent";
hideButton.addEventListener('mouseleave', () => {
hideButton.style.background = 'transparent';
removeHighlight();
});
// Create unhide all button
const unhideAllButton = document.createElement("button");
unhideAllButton.className = "element-hider-menu-item";
const unhideAllButton = document.createElement('button');
unhideAllButton.className = 'element-hider-menu-item';
unhideAllButton.style.cssText = hideButton.style.cssText;
unhideAllButton.innerHTML = `Unhide All Elements (${hiddenElementsArray.length})`;
unhideAllButton.addEventListener("click", unhideAllElements);
unhideAllButton.addEventListener('click', unhideAllElements);
// Add hover effects for unhide all button
unhideAllButton.addEventListener("mouseenter", () => {
unhideAllButton.style.background =
"var(--wave-color-background-hover, #3a3a3a)";
unhideAllButton.addEventListener('mouseenter', () => {
unhideAllButton.style.background = 'var(--wave-color-background-hover, #3a3a3a)';
});
unhideAllButton.addEventListener("mouseleave", () => {
unhideAllButton.style.background = "transparent";
unhideAllButton.addEventListener('mouseleave', () => {
unhideAllButton.style.background = 'transparent';
});
// Add a separator if the menu has other items
if (contextMenu.children.length > 0) {
const separator = document.createElement("div");
const separator = document.createElement('div');
separator.style.cssText = `
height: 1px;
background: var(--wave-color-border, #444);
@@ -616,7 +549,7 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
`;
contextMenu.appendChild(separator);
}
// Add our buttons
contextMenu.appendChild(hideButton);
contextMenu.appendChild(unhideAllButton);
@@ -625,28 +558,28 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
// Start observing for context menus
contextMenuObserver.observe(document.body, {
childList: true,
subtree: true,
subtree: true
});
// Initialize plugin
// Initialize plugin
function initializePlugin() {
trace.log("Initializing plugin...");
// Process immediately when DOM is ready
trace.log("Starting element processing...");
// Process existing elements
processAllElements();
// Set up reactive observer for new elements
setupElementObserver();
trace.log("Plugin fully initialized");
}
// Run initialization when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initializePlugin);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePlugin);
} else {
initializePlugin();
}
@@ -659,18 +592,18 @@ unloads.add(() => {
elementObserver = null;
}
contextMenuObserver.disconnect();
// Close any open custom menu
closeCustomMenu();
// Remove highlights
removeHighlight();
// Clean up global functions
window.showAllElementsFromSettings = undefined;
window.debugElementHider = undefined;
(window as any).showAllElementsFromSettings = undefined;
(window as any).debugElementHider = undefined;
trace.log("Plugin unloaded");
});
trace.log("Plugin loaded - Right-click any element to hide it!");
trace.log("Plugin loaded - Right-click any element to hide it!");
+35 -37
View File
@@ -2,64 +2,62 @@
/* Custom context menu for elements without built-in menu */
.element-hider-custom-menu {
position: fixed;
background: var(--wave-color-background-elevated, #2a2a2a);
border: 1px solid var(--wave-color-border, #444);
border-radius: 8px;
padding: 8px 0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 999999;
min-width: 180px;
font-family: inherit;
font-size: 14px;
position: fixed;
background: var(--wave-color-background-elevated, #2a2a2a);
border: 1px solid var(--wave-color-border, #444);
border-radius: 8px;
padding: 8px 0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 999999;
min-width: 180px;
font-family: inherit;
font-size: 14px;
}
.element-hider-menu-item {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
color: var(--wave-color-text, #ffffff);
background: transparent;
border: none;
width: 100%;
text-align: left;
transition: background-color 0.15s ease;
font-family: inherit;
font-size: 14px;
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
color: var(--wave-color-text, #ffffff);
background: transparent;
border: none;
width: 100%;
text-align: left;
transition: background-color 0.15s ease;
font-family: inherit;
font-size: 14px;
}
.element-hider-menu-item:hover {
background: var(--wave-color-background-hover, #3a3a3a);
background: var(--wave-color-background-hover, #3a3a3a);
}
.element-hider-menu-item:active {
background: var(--wave-color-background-active, #4a4a4a);
background: var(--wave-color-background-active, #4a4a4a);
}
.element-hider-menu-icon {
margin-right: 8px;
width: 16px;
height: 16px;
margin-right: 8px;
width: 16px;
height: 16px;
}
/* Highlight the target element */
.element-hider-target {
outline: 2px solid #ff6b6b !important;
outline-offset: 2px !important;
box-shadow: 0 0 10px rgba(255, 107, 107, 0.6) !important;
outline: 2px solid #ff6b6b !important;
outline-offset: 2px !important;
box-shadow: 0 0 10px rgba(255, 107, 107, 0.6) !important;
}
/* Hidden elements */
.element-hider-hidden {
display: none !important;
display: none !important;
}
/* Animation for hiding */
.element-hider-hiding {
transition:
opacity 0.3s ease,
transform 0.3s ease;
opacity: 0;
transform: scale(0.95);
}
transition: opacity 0.3s ease, transform 0.3s ease;
opacity: 0;
transform: scale(0.95);
}
@@ -1,6 +1,6 @@
{
"name": "@meowarex/colorama-lyrics",
"description": "Customize lyrics colors: single, gradient & auto from cover art",
"name": "@meowarex/oled-theme",
"description": "A dark OLED-friendly theme plugin that transforms Tidal Luna's appearance.",
"author": {
"name": "meowarex",
"url": "https://github.com/meowarex",
@@ -8,4 +8,4 @@
},
"main": "./src/index.ts",
"type": "module"
}
}
+59
View File
@@ -0,0 +1,59 @@
import { ReactiveStore } from "@luna/core";
import { LunaSettings, LunaSwitchSetting } from "@luna/ui";
import React from "react";
export const settings = await ReactiveStore.getPluginStorage("OLEDTheme", {
qualityColorMatchedSeekBar: true,
oledFriendlyButtons: true,
lightMode: false,
});
export const Settings = () => {
const [qualityColorMatchedSeekBar, setQualityColorMatchedSeekBar] = React.useState(settings.qualityColorMatchedSeekBar);
const [oledFriendlyButtons, setOledFriendlyButtons] = React.useState(settings.oledFriendlyButtons);
const [lightMode, setLightMode] = React.useState(settings.lightMode);
return (
<LunaSettings>
<LunaSwitchSetting
title="Quality Color Matched Seek Bar"
desc="Color the Seek/Progress Bar based on audio quality"
checked={qualityColorMatchedSeekBar}
onChange={(_, checked) => {
console.log("Quality Color Matched Seek Bar:", checked ? "enabled" : "disabled");
setQualityColorMatchedSeekBar((settings.qualityColorMatchedSeekBar = checked));
// Update styles immediately when setting changes
if ((window as any).updateOLEDThemeStyles) {
(window as any).updateOLEDThemeStyles();
}
}}
/>
<LunaSwitchSetting
title="OLED Friendly Buttons"
desc="Remove button styling from OLED theme to keep buttons with original Tidal appearance"
checked={oledFriendlyButtons}
onChange={(_, checked) => {
console.log("OLED Friendly Buttons:", checked ? "enabled" : "disabled");
setOledFriendlyButtons((settings.oledFriendlyButtons = checked));
// Update styles immediately when setting changes
if ((window as any).updateOLEDThemeStyles) {
(window as any).updateOLEDThemeStyles();
}
}}
/>
<LunaSwitchSetting
title="Light Mode | Experimental"
desc="Use the light theme instead of the dark theme. This is experimental and may not work as expected."
checked={lightMode}
onChange={(_, checked) => {
console.log("Light Mode:", checked ? "enabled" : "disabled");
setLightMode((settings.lightMode = checked));
// Update styles immediately when setting changes
if ((window as any).updateOLEDThemeStyles) {
(window as any).updateOLEDThemeStyles();
}
}}
/>
</LunaSettings>
);
};
+301
View File
@@ -0,0 +1,301 @@
/*
{
"name": "Abyss Neptune",
"author": "@itzzexcel",
"description": "Abyss Neptune: ShadowX Theme from Spicetify to TIDAL (17/Jan/2025)"
}
*/
::-webkit-scrollbar {
display: none;
}
:root {
--wave-color-solid-accent-fill: white;
--wave-color-solid-rainbow-yellow-fill: white;
--wave-color-solid-contrast-fill: white;
--wave-color-solid-base-brighter: black;
--wave-text-body-medium: white !important;
--track-vibrant-color: white !important;
--wave-color-opacity-contrast-fill-ultra-thin: #fffafa1a !important;
--wave-color-solid-rainbow-yellow-darkest: #fffafa1a !important;
--wave-color-solid-accent-dark: rgb(128, 128, 128);
}
/* Credits to https://github.com/surfbryce for the fonts */
@font-face {
font-family: "AbyssFont";
font-weight: 400;
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") format("woff2");
}
@font-face {
font-family: "AbyssFont";
font-weight: 500;
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2");
}
@font-face {
font-family: "AbyssFont";
font-weight: 600;
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2");
}
@font-face {
font-family: "AbyssFont";
font-weight: 700;
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2");
}
[class^="followingButton"],
[title="Unfollow"],
[title="Follow"],
[title="Unfollow"]>span,
[title="Follow"]>span {
background-color: var(--wave-color-solid-rainbow-yellow-fill) !important;
color: var(--wave-color-solid-base-brighter);
}
[class^="_wave-badge-color-max"] {
color: black !important;
background-color: var(--wave-color-solid-accent-fill);
border-radius: 3px;
}
[data-test="main-layout-sidebar-wrapper"] {
border-right: rgb(25, 25, 25) 1px solid;
}
[class^="_wave-badge"] {
background-color: var(--wave-color-solid-accent-fill);
border-radius: 4px;
color: black;
}
[class^="_progressBarWrapper"] {
color: var(--wave-color-solid-accent-fill) !important;
}
[class^="_sidebarItem"]>span {
color: var(--wave-color-solid-accent-dark);
}
[data-test="main-layout-header"] {
border-left: 0 !important;
}
[class^="_sidebarItem"]:hover span {
color: var(--wave-color-solid-contrast-fill);
}
[class^="_sidebarItem"] [class^="active"]>span {
color: var(--wave-color-solid-accent-dark) !important;
}
[class^="_active"] {
color: var(--wave-color-solid-accent-fill) !important;
}
[class^="ReactVirtualized__Grid"] {
border-radius: 10px;
margin: 5px;
}
[data-test="media-table"]>div>div>div {
border: 1px solid rgb(25, 25, 25) !important;
}
[class^="ReactVirtualized__Grid__innerScrollContainer"] {
border: none;
margin: 5px;
}
[class^="button"]>span {
color: black;
}
[class^="_explicitBadge"] {
color: var(--wave-color-solid-accent-fill);
}
[class^="viewAllButton"] {
border-radius: 4px;
display: grid;
place-items: center;
}
[data-test="current-media-imagery"] {
border: 0 !important;
margin: none;
}
[class^="_imageBorder"] {
display: none;
}
[class^="_headerButtons"]>button,
[class^="_headerButtons"]>button>span,
[data-test="toggle-picture-in-picture"] {
background-color: var(--wave-color-solid-accent-fill) !important;
color: black;
}
[class^="_container"]>[class^="_navigationArrows"] {
color: black;
background-color: var(--wave-color-solid-accent-fill) !important;
border-radius: 4px;
}
[class^="_buttons"]>button>span {
color: black !important;
}
[class^="_container"]>button {
border: 0px none;
}
[data-test="feed-sidebar"] {
margin-top: 10px;
}
[data-test="footer-player"] {
width: calc(100% - 20px);
bottom: 10px;
left: 10px;
border: 1px solid rgb(25, 25, 25);
border-radius: 4px !important;
position: absolute !important;
}
[class^="_tooltipContainer"]>button {
background-color: var(--wave-color-solid-accent-fill);
color: black;
}
[class^="_tooltipContainer"]>button:hover {
background-color: lightgray !important;
}
[class^="_tableRow"]:hover>*,
[data-test-is-playing="true"]>* {
color: var(--wave-color-solid-accent-fill) !important;
}
[class^="_tableRow"]>*,
[data-test-is-playing="false"]>* {
color: lightgray !important;
}
[class*="coverColumn"] {
padding-left: 5px !important;
}
[class^="actionList"] {
background-color: transparent;
margin: 0px;
border-radius: 5px;
}
button[data-test="request-fullscreen"],
button[data-test="close-now-playing"],
button[data-test="play-all"],
button[data-test="shuffle-all"] {
color: black;
background-color: var(--wave-color-solid-accent-fill);
border-radius: 12px;
}
button[data-test="request-fullscreen"]:hover,
button[data-test="close-now-playing"]:hover {
color: black;
background-color: lightgray !important;
}
.neptune-switch-checkbox:checked+.neptune-switch {
background-color: rgba(255, 255, 255, 0.1);
}
[data-test="navigation-arrows"]>button {
background-color: var(--wave-color-solid-accent-fill) !important;
color: black !important;
border-radius: 5px;
}
[data-test="navigation-arrows"]>button:disabled {
background-color: lightgray !important;
opacity: 1;
}
[data-test="main-layout-header"],
[data-test="feed-sidebar"],
[data-test="stream-metadata"],
[data-test="footer-player"] {
background-color: rgba(0, 0, 0, 0.8) !important;
backdrop-filter: blur(10px);
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important;
}
[data-wave-color=textUrl] {
color: var(--wave-color-solid-accent-fill);
}
[class^="_smallHeader"] {
margin-top: 7.5px;
}
[data-test="play-all"]>div>*,
[data-test="shuffle-all"]>div>*,
[data-test="play-all"],
[data-test="shuffle-all"] {
color: var(--wave-color-solid-accent-fill) !important;
background-color: transparent !important;
}
[class^="__NEPTUNE_PAGE"],
[data-test="main"] {
margin-top: 35px;
}
[data-test="button-desktop-release-notes"],
[data-test="button-release-notes"] {
background-color: white;
}
[data-test="button-desktop-release-notes"]:hover,
[data-test="button-release-notes"]:hover {
background-color: lightgray !important;
transition: none !important;
}
#playQueueSidebar {
top: 50px !important;
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin);
margin: 2px;
margin-right: -14px !important;
background-color: rgba(0, 0, 0, 0.8) !important;
backdrop-filter: blur(10px);
}
[class^="_bottomGradient"] {
visibility: hidden;
}
[data-test="settings-page"] {
padding-bottom: 60px !important;
}
[data-test="query-suggestions"],
[data-test="recent-searches-container"] {
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(10px);
}
[data-test="contextmenu"] {
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important;
}
[class^="_dataContainer_"]::before {
background-image: var(--img);
filter: blur(10px) brightness(0.4);
}
+128
View File
@@ -0,0 +1,128 @@
import { LunaUnload, Tracer } from "@luna/core";
import { StyleTag, observePromise, PlayState, Quality, type MediaItem } from "@luna/lib";
import { settings, Settings } from "./Settings";
// Import CSS files directly using Luna's file:// syntax - Took me a while to figure out <3
import darkTheme from "file://dark-theme.css?minify";
import oledFriendlyTheme from "file://oled-friendly.css?minify";
import lightTheme from "file://light-theme.css?minify";
export const { trace } = Tracer("[OLED Theme]");
export { Settings };
// called when plugin is unloaded.
// clean up resources
export const unloads = new Set<LunaUnload>();
// StyleTag instance for theme management
const themeStyleTag = new StyleTag("OLED-Theme", unloads);
// Quality color mapping
const QUALITY_COLORS = {
MAX: "#FED330", // Max/HiFi
HIGH: "#31FFEE", // High
LOW: "#FFFFFE" // Low
};
// Function to get quality color based on audio quality
const getQualityColor = (audioQuality: string): string => {
const quality = audioQuality?.toUpperCase();
if (quality?.includes("HI_RES_LOSSLESS")) {
return QUALITY_COLORS.MAX;
} else if (quality?.includes("LOSSLESS")) {
return QUALITY_COLORS.HIGH;
} else {
return QUALITY_COLORS.LOW;
}
};
// Function to Reset Seek Bar Color (if setting gets disabled while playing)
const resetSeekBarColor = async (): Promise<void> => {
try {
const progressBarWrapper = await observePromise<HTMLElement>(unloads, `[class^="_progressBarWrapper"]`);
if (!progressBarWrapper) return;
progressBarWrapper.style.removeProperty('color');
progressBarWrapper.querySelectorAll('[class*="progress"], [class*="bar"]').forEach(el => {
if (el instanceof HTMLElement) el.style.removeProperty('color');
});
} catch (error) {
trace.msg.err(`Failed to reset seek bar color: ${error}`);
}
};
// Function to apply quality-based seek bar coloring (if enabled)
const applyQualityColors = async (): Promise<void> => {
if (!settings.qualityColorMatchedSeekBar) return;
try {
const progressBarWrapper = await observePromise<HTMLElement>(unloads, `[class^="_progressBarWrapper"]`);
if (!progressBarWrapper) return;
const audioQuality = PlayState.playbackContext?.actualAudioQuality;
if (!audioQuality) return;
const qualityColor = getQualityColor(audioQuality);
progressBarWrapper.style.setProperty('color', qualityColor, 'important');
progressBarWrapper.querySelectorAll('[class*="progress"], [class*="bar"]').forEach(el => {
if (el instanceof HTMLElement) el.style.setProperty('color', qualityColor, 'important');
});
//trace.msg.log(`Applied quality color ${qualityColor}`);
} catch (error) {
trace.msg.err(`Failed to apply quality colors: ${error}`);
}
};
// Function to monitor track changes using track ID
const setupQualityMonitoring = (): void => {
let lastTrackId: string | null = null;
const interval = setInterval(() => {
if (!settings.qualityColorMatchedSeekBar) return;
const currentTrackId = PlayState.playbackContext?.actualProductId;
if (currentTrackId && currentTrackId !== lastTrackId) {
//trace.msg.log(`[OLED Theme] Track ID changed: ${lastTrackId} -> ${currentTrackId}`);
lastTrackId = currentTrackId;
applyQualityColors();
}
}, 250);
unloads.add(() => clearInterval(interval));
// Initial color application (if a track is already loaded)
const currentTrackId = PlayState.playbackContext?.actualProductId;
if (settings.qualityColorMatchedSeekBar && currentTrackId) {
lastTrackId = currentTrackId;
applyQualityColors();
}
};
// Function to apply theme styles based on current settings
const applyThemeStyles = function(): void {
// Choose the appropriate CSS file based on settings
let selectedStyle: string;
if (settings.lightMode) {
// Light mode - (OLED friendly doesn't apply to light theme)
selectedStyle = lightTheme;
} else {
// Dark mode
selectedStyle = settings.oledFriendlyButtons ? oledFriendlyTheme : darkTheme;
}
// Remove SeekBar coloring if Quality Color Matched Seek Bar is enabled
// This allows our manual coloring to take precedence
if (settings.qualityColorMatchedSeekBar) {
selectedStyle = selectedStyle.replace(/\[class\^="_progressBarWrapper"\]\s*\{[^}]*\}/g, '');
setupQualityMonitoring();
} else {
// If disabling, reset the seek bar color
resetSeekBarColor();
}
// Apply the selected theme using StyleTag
themeStyleTag.css = selectedStyle;
};
// Make this function available globally so Settings can call it
(window as any).updateOLEDThemeStyles = applyThemeStyles;
// Apply the OLED theme initially
applyThemeStyles();
+424
View File
@@ -0,0 +1,424 @@
/*
{
"name": "Abyss Neptune - Light",
"author": "@itzzexcel",
"description": "Abyss Neptune Light Theme for TIDAL (17/Jan/2025)"
}
*/
::-webkit-scrollbar {
display: none;
}
:root {
--wave-color-solid-accent-fill: #666666;
--wave-color-solid-rainbow-yellow-fill: #666666;
--wave-color-solid-contrast-fill: #666666;
--wave-color-solid-base-brighter: #666666;
--wave-text-body-medium: #333333 !important;
--track-vibrant-color: #666666 !important;
--wave-color-opacity-contrast-fill-ultra-thin: #c0c0c0 !important;
--wave-color-solid-rainbow-yellow-darkest: #c0c0c0 !important;
--wave-color-solid-accent-dark: #555555;
}
/* Credits to https://github.com/surfbryce for the fonts */
@font-face {
font-family: "AbyssFont";
font-weight: 400;
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") format("woff2");
}
@font-face {
font-family: "AbyssFont";
font-weight: 500;
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2");
}
@font-face {
font-family: "AbyssFont";
font-weight: 600;
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2");
}
@font-face {
font-family: "AbyssFont";
font-weight: 700;
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2");
}
[class^="followingButton"],
[title="Unfollow"],
[title="Follow"],
[title="Unfollow"]>span,
[title="Follow"]>span {
background-color: var(--wave-color-solid-rainbow-yellow-fill) !important;
color: var(--wave-color-solid-base-brighter);
}
[class^="_wave-badge-color-max"] {
color: #333333 !important;
background-color: var(--wave-color-solid-accent-fill);
border-radius: 3px;
}
[data-test="main-layout-sidebar-wrapper"] {
border-right: rgb(230, 230, 230) 1px solid;
background-color: rgba(250, 250, 250, 0.95) !important;
}
[class^="_wave-badge"] {
background-color: var(--wave-color-solid-accent-fill);
border-radius: 4px;
color: #333333;
}
[class^="_progressBarWrapper"] {
color: var(--wave-color-solid-accent-fill) !important;
}
[class^="_sidebarItem"]>span {
color: #666666;
}
[data-test="main-layout-header"] {
border-left: 0 !important;
}
[class^="_sidebarItem"]:hover span {
color: #333333;
}
[class^="_sidebarItem"] [class^="active"]>span {
color: #333333 !important;
}
/* Sidebar icons and text - ensure grey colors */
[data-test="main-layout-sidebar-wrapper"] svg,
[data-test="main-layout-sidebar-wrapper"] path,
[class^="_sidebarItem"] svg,
[class^="_sidebarItem"] path {
fill: #666666 !important;
color: #666666 !important;
}
[data-test="main-layout-sidebar-wrapper"] span,
[class^="_sidebarItem"] span {
color: #666666 !important;
}
[class^="_active"] {
color: var(--wave-color-solid-accent-fill) !important;
}
[class^="ReactVirtualized__Grid"] {
border-radius: 10px;
margin: 5px;
}
[data-test="media-table"]>div>div>div {
border: 1px solid rgb(230, 230, 230) !important;
}
[class^="ReactVirtualized__Grid__innerScrollContainer"] {
border: none;
margin: 5px;
}
[class^="button"]>span {
color: #333333;
}
[class^="_explicitBadge"] {
color: var(--wave-color-solid-accent-fill);
}
[class^="viewAllButton"] {
border-radius: 4px;
display: grid;
place-items: center;
}
[data-test="current-media-imagery"] {
border: 0 !important;
margin: none;
}
[class^="_imageBorder"] {
display: none;
}
[class^="_headerButtons"]>button,
[class^="_headerButtons"]>button>span,
[data-test="toggle-picture-in-picture"] {
background-color: var(--wave-color-solid-accent-fill) !important;
color: #333333;
}
[class^="_container"]>[class^="_navigationArrows"] {
color: #333333;
background-color: var(--wave-color-solid-accent-fill) !important;
border-radius: 4px;
}
[class^="_buttons"]>button>span {
color: #333333 !important;
}
[class^="_container"]>button {
border: 0px none;
}
[data-test="feed-sidebar"] {
margin-top: 10px;
}
[data-test="footer-player"] {
width: calc(100% - 20px);
bottom: 10px;
left: 10px;
border: 1px solid rgba(200, 200, 200, 0.7);
border-radius: 4px !important;
position: absolute !important;
}
[class^="_tooltipContainer"]>button {
background-color: var(--wave-color-solid-accent-fill);
color: #333333;
}
[class^="_tooltipContainer"]>button:hover {
background-color: #555555 !important;
}
[class^="_tableRow"]:hover>*,
[data-test-is-playing="true"]>* {
color: #333333 !important;
}
[class^="_tableRow"]>*,
[data-test-is-playing="false"]>* {
color: #333333 !important;
}
/* Track list text - ensure all text is dark */
[data-test="media-table"] *,
[class^="_trackTitle"],
[class^="_artistName"],
[class^="_albumTitle"],
[class^="_tableCell"] *,
[class^="_tableCellContent"] * {
color: #333333 !important;
}
[class*="coverColumn"] {
padding-left: 5px !important;
}
[class^="actionList"] {
background-color: transparent;
margin: 0px;
border-radius: 5px;
}
button[data-test="request-fullscreen"],
button[data-test="close-now-playing"],
button[data-test="play-all"],
button[data-test="shuffle-all"] {
color: #333333;
background-color: var(--wave-color-solid-accent-fill);
border-radius: 12px;
}
button[data-test="request-fullscreen"]:hover,
button[data-test="close-now-playing"]:hover {
color: #333333;
background-color: #aaaaaa !important;
}
.neptune-switch-checkbox:checked+.neptune-switch {
background-color: rgba(0, 0, 0, 0.1);
}
[data-test="navigation-arrows"]>button {
background-color: var(--wave-color-solid-accent-fill) !important;
color: #333333 !important;
border-radius: 5px;
}
[data-test="navigation-arrows"]>button:disabled {
background-color: #cccccc !important;
opacity: 1;
}
[data-test="main-layout-header"] {
background-color: rgba(235, 235, 235, 0.95) !important;
backdrop-filter: blur(10px);
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important;
}
[data-test="feed-sidebar"] {
background-color: rgba(225, 225, 225, 0.9) !important;
backdrop-filter: blur(10px);
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important;
}
[data-test="stream-metadata"] {
background-color: rgba(230, 230, 230, 0.92) !important;
backdrop-filter: blur(10px);
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important;
}
[data-test="footer-player"] {
background-color: rgba(255, 255, 255, 0.6) !important;
backdrop-filter: blur(15px);
border: 1px solid rgba(200, 200, 200, 0.7) !important;
}
[data-wave-color=textUrl] {
color: var(--wave-color-solid-accent-fill);
}
[class^="_smallHeader"] {
margin-top: 7.5px;
}
/* Button styling using proper light theme approach */
:root {
--button-light: #d9d9d9 !important;
--button-medium: #cbcbcb !important;
}
/*buttons*/
._activeTab_f47dafa {
background: #0000001c;
}
/*canvas nav buttons*/
.viewAllButton--Nb87U,
.css-7l8ggf {
background: #e0e0e0;
}
.viewAllButton--Nb87U:hover,
.css-7l8ggf:hover {
background: #cbcbcb;
}
/*tracks page*/
.variantPrimary--pjymy,
._button_3357ce6 {
background-color: var(--button-light);
}
._button_f1c7fcb {
background: var(--wave-color-solid-base-brighter);
}
._button_84b8ffe {
background-color: var(--wave-color-solid-base-brighter);
}
._button_84b8ffe:hover {
background-color: var(--wave-color-solid-base-brightest);
}
.button--_0I_t {
background-color: var(--button-light);
}
.button--_0I_t:hover {
background-color: var(--wave-color-opacity-contrast-fill-regular);
}
._button_94c5125 {
background: var(--wave-color-solid-base-brighter);
}
.primary--NLSX4 {
background-color: #d5d5d5;
}
.primary--NLSX4:hover {
background-color: var(--wave-color-opacity-contrast-fill-regular) !important;
}
.primary--NLSX4:disabled {
background-color: #e7e7e8;
}
.primary--NLSX4:disabled:hover {
background-color: #e7e7e8;
}
[class^="__NEPTUNE_PAGE"],
[data-test="main"] {
margin-top: 35px;
}
[data-test="button-desktop-release-notes"],
[data-test="button-release-notes"] {
background-color: #333333;
}
[data-test="button-desktop-release-notes"]:hover,
[data-test="button-release-notes"]:hover {
background-color: #555555 !important;
transition: none !important;
}
#playQueueSidebar {
top: 50px !important;
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin);
margin: 2px;
margin-right: -14px !important;
background-color: rgba(220, 220, 220, 0.9) !important;
backdrop-filter: blur(10px);
}
[class^="_bottomGradient"] {
visibility: hidden;
}
[data-test="settings-page"] {
padding-bottom: 60px !important;
}
[data-test="query-suggestions"],
[data-test="recent-searches-container"] {
background-color: rgba(227, 227, 227, 0.85);
backdrop-filter: blur(10px);
}
[data-test="contextmenu"] {
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important;
}
[class^="_dataContainer_"]::before {
background-image: var(--img);
filter: blur(10px) brightness(1.2);
}
/* Player bar text colors - fix white text issues */
[data-test="footer-player"] * {
color: #333333 !important;
}
[data-test="footer-player"] [class*="trackTitle"],
[data-test="footer-player"] [class*="artistName"],
[data-test="footer-player"] [class*="trackInfo"],
[data-test="footer-player"] [class*="duration"],
[data-test="footer-player"] [class*="time"],
[data-test="footer-player"] [class*="timestamp"] {
color: #333333 !important;
}
/* Main page background */
body,
[data-test="main"],
[class^="__NEPTUNE_PAGE"] {
background-color: #f5f5f5 !important;
}
@@ -0,0 +1,215 @@
/*
{
"name": "Abyss Neptune - OLED Friendly",
"author": "@itzzexcel",
"description": "Abyss Neptune theme without button styling for OLED displays"
}
*/
::-webkit-scrollbar {
display: none;
}
:root {
--wave-color-solid-accent-fill: white;
--wave-color-solid-rainbow-yellow-fill: white;
--wave-color-solid-contrast-fill: white;
--wave-color-solid-base-brighter: black;
--wave-text-body-medium: white !important;
--track-vibrant-color: white !important;
--wave-color-opacity-contrast-fill-ultra-thin: #fffafa1a !important;
--wave-color-solid-rainbow-yellow-darkest: #fffafa1a !important;
--wave-color-solid-accent-dark: rgb(128, 128, 128);
}
/* Credits to https://github.com/surfbryce for the fonts */
@font-face {
font-family: "AbyssFont";
font-weight: 400;
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") format("woff2");
}
@font-face {
font-family: "AbyssFont";
font-weight: 500;
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2");
}
@font-face {
font-family: "AbyssFont";
font-weight: 600;
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2");
}
@font-face {
font-family: "AbyssFont";
font-weight: 700;
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2");
}
[class^="followingButton"],
[title="Unfollow"],
[title="Follow"],
[title="Unfollow"]>span,
[title="Follow"]>span {
background-color: var(--wave-color-solid-rainbow-yellow-fill) !important;
color: var(--wave-color-solid-base-brighter);
}
[class^="_wave-badge-color-max"] {
color: black !important;
background-color: var(--wave-color-solid-accent-fill);
border-radius: 3px;
}
[data-test="main-layout-sidebar-wrapper"] {
border-right: rgb(25, 25, 25) 1px solid;
}
[class^="_wave-badge"] {
background-color: var(--wave-color-solid-accent-fill);
border-radius: 4px;
color: black;
}
[class^="_progressBarWrapper"] {
color: var(--wave-color-solid-accent-fill) !important;
}
[class^="_sidebarItem"]>span {
color: var(--wave-color-solid-accent-dark);
}
[data-test="main-layout-header"] {
border-left: 0 !important;
}
[class^="_sidebarItem"]:hover span {
color: var(--wave-color-solid-contrast-fill);
}
[class^="_sidebarItem"] [class^="active"]>span {
color: var(--wave-color-solid-accent-dark) !important;
}
[class^="_active"] {
color: var(--wave-color-solid-accent-fill) !important;
}
[class^="ReactVirtualized__Grid"] {
border-radius: 10px;
margin: 5px;
}
[data-test="media-table"]>div>div>div {
border: 1px solid rgb(25, 25, 25) !important;
}
[class^="ReactVirtualized__Grid__innerScrollContainer"] {
border: none;
margin: 5px;
}
[class^="_explicitBadge"] {
color: var(--wave-color-solid-accent-fill);
}
[data-test="current-media-imagery"] {
border: 0 !important;
margin: none;
}
[class^="_imageBorder"] {
display: none;
}
[data-test="feed-sidebar"] {
margin-top: 10px;
}
[data-test="footer-player"] {
width: calc(100% - 20px);
bottom: 10px;
left: 10px;
border: 1px solid rgb(25, 25, 25);
border-radius: 4px !important;
position: absolute !important;
}
[class^="_tableRow"]:hover>*,
[data-test-is-playing="true"]>* {
color: var(--wave-color-solid-accent-fill) !important;
}
[class^="_tableRow"]>*,
[data-test-is-playing="false"]>* {
color: lightgray !important;
}
[class*="coverColumn"] {
padding-left: 5px !important;
}
[class^="actionList"] {
background-color: transparent;
margin: 0px;
border-radius: 5px;
}
.neptune-switch-checkbox:checked+.neptune-switch {
background-color: rgba(255, 255, 255, 0.1);
}
[data-test="main-layout-header"],
[data-test="feed-sidebar"],
[data-test="stream-metadata"],
[data-test="footer-player"] {
background-color: rgba(0, 0, 0, 0.8) !important;
backdrop-filter: blur(10px);
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important;
}
[data-wave-color=textUrl] {
color: var(--wave-color-solid-accent-fill);
}
[class^="_smallHeader"] {
margin-top: 7.5px;
}
[class^="__NEPTUNE_PAGE"],
[data-test="main"] {
margin-top: 35px;
}
#playQueueSidebar {
top: 50px !important;
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin);
margin: 2px;
margin-right: -14px !important;
background-color: rgba(0, 0, 0, 0.8) !important;
backdrop-filter: blur(10px);
}
[class^="_bottomGradient"] {
visibility: hidden;
}
[data-test="settings-page"] {
padding-bottom: 60px !important;
}
[data-test="query-suggestions"],
[data-test="recent-searches-container"] {
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(10px);
}
[data-test="contextmenu"] {
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin) !important;
}
[class^="_dataContainer_"]::before {
background-image: var(--img);
filter: blur(10px) brightness(0.4);
}
+1 -1
View File
@@ -8,4 +8,4 @@
},
"main": "./src/index.ts",
"type": "module"
}
}
File diff suppressed because it is too large Load Diff
@@ -1,167 +1,159 @@
/* Global Spinning Background Styles - PERFORMANCE OPTIMIZED */
.global-background-container {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
z-index: -3;
pointer-events: none;
overflow: hidden;
/* Hardware acceleration */
transform: translateZ(0);
backface-visibility: hidden;
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
z-index: -3;
pointer-events: none;
overflow: hidden;
/* Hardware acceleration */
transform: translateZ(0);
backface-visibility: hidden;
}
.global-spinning-black-bg {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #000;
z-index: -2;
pointer-events: none;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #000;
z-index: -2;
pointer-events: none;
}
.global-spinning-image {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 150vw;
height: 150vh;
object-fit: cover;
z-index: -1;
filter: blur(80px) brightness(0.4) contrast(1.2) saturate(1);
opacity: 1;
animation: spinGlobal 45s linear infinite;
will-change: transform;
/* Hardware acceleration */
transform-origin: center center;
backface-visibility: hidden;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 150vw;
height: 150vh;
object-fit: cover;
z-index: -1;
filter: blur(80px) brightness(0.4) contrast(1.2) saturate(1);
opacity: 1;
animation: spinGlobal 45s linear infinite;
will-change: transform;
/* Hardware acceleration */
transform-origin: center center;
backface-visibility: hidden;
}
/* 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;
/* Performance mode optimizations - keep spinning but optimize other aspects */
.global-spinning-image.performance-mode-static {
/* Keep animation enabled in performance mode */
/* Lighter blur for performance */
filter: blur(20px) brightness(0.4) contrast(1.2) saturate(1) !important;
/* Smaller size for performance */
width: 120vw !important;
height: 120vh !important;
}
/* 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-image.performance-mode-static {
/* Keep animation enabled in performance mode */
/* Optimized size and effects for performance */
width: 80vw !important;
height: 80vh !important;
}
/* Now Playing Background Container Optimization */
.now-playing-background-container {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
overflow: hidden;
/* Hardware acceleration */
transform: translateZ(0);
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;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: -3;
pointer-events: none;
overflow: hidden;
/* Hardware acceleration */
transform: translateZ(0);
backface-visibility: hidden;
}
/* Optimized keyframe animations with GPU acceleration */
@keyframes spinGlobal {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
/* Reduced motion for users who prefer it */
@media (prefers-reduced-motion: reduce) {
.global-spinning-image,
.now-playing-background-image {
/* biome-ignore lint: Accessibility override needs priority */
animation: none !important;
/* biome-ignore lint: Accessibility override needs priority */
transform: translate(-50%, -50%) !important;
/* biome-ignore lint: Accessibility override needs priority */
will-change: auto !important;
}
.global-spinning-image,
.now-playing-background-image {
animation: none !important;
transform: translate(-50%, -50%) !important;
will-change: auto !important;
}
}
/* Performance mode: optimize effects but keep spinning */
.performance-mode .global-spinning-image,
.performance-mode .now-playing-background-image {
/* Keep animations but optimize filter effects */
/* biome-ignore lint: Intentional override of runtime styles */
filter: blur(10px) brightness(0.4) contrast(1.1) !important;
/* Keep animations but optimize filter effects */
filter: blur(10px) brightness(0.4) contrast(1.1) !important;
}
/* Make app chrome transparent for cover-everywhere background */
/* Make Notification Feed sidebar transparent */
body,
#wimp,
main,
[class^="_sidebarWrapper"],
[class^="_mainContainer"],
[class*="smallHeader"],
[data-test="main-layout-sidebar-wrapper"],
[data-test="main-layout-header"],
[data-test="feed-sidebar"],
[data-test="stream-metadata"],
[data-test="footer-player"],
/* Notification Feed sidebar specific container */
[class^="_feedSidebarVStack"],
[class^="_feedSidebarSpacer"],
[class^="_feedSidebarItem"],
[class^="_feedSidebarItemDiv"],
[class^="_cellContainer"] {
/* biome-ignore lint: Ensure background is fully cleared under theme CSS */
background: unset !important;
[class^="_cellContainer"],
[class^="_cellTextContainer"] {
background: unset !important;
}
/* Make sidebar semi-transparent with optimized backdrop-filter */
[data-test="main-layout-sidebar-wrapper"] {
/* biome-ignore lint: Must beat app inline styles for translucency */
background-color: rgba(0, 0, 0, 0.3) !important;
/* biome-ignore lint: Must beat app inline styles for translucency */
backdrop-filter: blur(10px) !important;
/* biome-ignore lint: Must beat app inline styles for translucency */
-webkit-backdrop-filter: blur(10px) !important;
/* Make sidebar and player bar semi-transparent with optimized backdrop-filter */
[data-test="footer-player"],
[data-test="main-layout-sidebar-wrapper"],
[class^="_bar"],
[class^="_sidebarItem"]:hover {
background-color: rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(10px) !important;
-webkit-backdrop-filter: blur(10px) !important;
}
/* Performance mode: reduce backdrop blur */
.performance-mode [data-test="main-layout-sidebar-wrapper"] {
/* biome-ignore lint: Performance mode style requires priority */
backdrop-filter: blur(5px) !important;
/* biome-ignore lint: Performance mode style requires priority */
-webkit-backdrop-filter: blur(5px) !important;
.performance-mode [data-test="footer-player"],
.performance-mode [data-test="main-layout-sidebar-wrapper"],
.performance-mode [class^="_bar"],
.performance-mode [class^="_sidebarItem"]:hover {
backdrop-filter: blur(5px) !important;
-webkit-backdrop-filter: blur(5px) !important;
}
/* Feed sidebar panel - black tint background for readability */
[data-test="feed-sidebar"] {
/* biome-ignore lint: Ensure readability over media */
background-color: rgba(0, 0, 0, 0.5) !important;
/* biome-ignore lint: Ensure readability over media */
backdrop-filter: blur(10px) !important;
/* biome-ignore lint: Ensure readability over media */
-webkit-backdrop-filter: blur(10px) !important;
background-color: rgba(0, 0, 0, 0.5) !important;
backdrop-filter: blur(10px) !important;
-webkit-backdrop-filter: blur(10px) !important;
}
/* Performance mode: reduce sidebar backdrop blur */
.performance-mode [data-test="feed-sidebar"] {
/* biome-ignore lint: Performance mode style requires priority */
backdrop-filter: blur(5px) !important;
/* biome-ignore lint: Performance mode style requires priority */
-webkit-backdrop-filter: blur(5px) !important;
backdrop-filter: blur(5px) !important;
-webkit-backdrop-filter: blur(5px) !important;
}
/* Feed sidebar items - transparent */
@@ -170,6 +162,10 @@ main,
[class*="_cellContainer"],
[data-test="feed-interval"],
[data-test="feed-item"] {
/* biome-ignore lint: Match theme transparency */
background-color: transparent !important;
background-color: transparent !important;
}
/* Remove bottom gradient */
[class^="_bottomGradient"] {
display: none !important;
}
@@ -1,22 +0,0 @@
/* Square Player Bar override — injected when floating is disabled */
/* MARKER: Floating Player Bar CSS */
[data-test="footer-player"] {
/* biome-ignore lint: Override native floating position */
bottom: 0 !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
+61 -422
View File
@@ -1,448 +1,87 @@
/* Font imports for lyrics */
@font-face {
font-family: "AbyssFont";
font-weight: 400;
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2")
format("woff2");
font-family: "AbyssFont";
font-weight: 400;
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") format("woff2");
}
@font-face {
font-family: "AbyssFont";
font-weight: 500;
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2")
format("woff2");
font-family: "AbyssFont";
font-weight: 500;
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2");
}
@font-face {
font-family: "AbyssFont";
font-weight: 600;
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2")
format("woff2");
font-family: "AbyssFont";
font-weight: 600;
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2");
}
@font-face {
font-family: "AbyssFont";
font-weight: 700;
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2")
format("woff2");
font-family: "AbyssFont";
font-weight: 700;
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2");
}
/* Enhanced lyrics styling with glow effects */
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"][class*="_current_"] {
text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #fff),
/* biome-ignore lint: Required to override app glow strength */
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #fff) !important;
padding-left: 20px;
transition-duration: 0.7s;
font-size: calc(55px * var(--rl-font-scale, 1));
/* biome-ignore lint: Active lyric uses Colorama color */
color: var(--cl-glow1, #fff) !important;
font-family:
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 700;
[class*="_lyricsText"] > div > span[data-current="true"] {
text-shadow: 0 0 2px #fff, 0 0 20px #fff !important;
padding-left: 20px;
transition-duration: 0.7s;
font-size: 55px;
color: white !important;
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-weight: 700;
}
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"] {
text-shadow:
0 0 0px transparent,
0 0 0px transparent;
transition-duration: 0.25s;
color: rgba(255, 255, 255, 0.4);
font-size: calc(40px * var(--rl-font-scale, 1));
font-family:
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 700;
[class*="_lyricsText"] > div > span {
text-shadow: 0 0 0px transparent, 0 0 0px transparent;
transition-duration: 0.25s;
color: rgba(128, 128, 128, 0.4);
font-size: 40px;
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-weight: 700;
}
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"]:hover {
text-shadow:
0 0 var(--rl-glow-inner, 2px) lightgray,
/* biome-ignore lint: Hover glow should override defaults */
0 0 var(--rl-glow-outer, 20px) lightgray !important;
/* biome-ignore lint: Hover color override */
color: lightgray !important;
padding-left: 20px;
transition-duration: 0.7s;
[class*="_lyricsText"] > div > span:hover {
text-shadow: 0 0 2px lightgray, 0 0 20px lightgray !important;
color: lightgray !important;
padding-left: 20px;
transition-duration: 0.7s;
}
/* Track title glow */
[data-test="now-playing-track-title"] {
text-shadow: 0 0 1px #fff, 0 0 30px #fff !important;
}
/* Current line transitions */
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"] {
transition:
text-shadow 0.7s ease-in-out,
color 0.7s ease-in-out,
/* biome-ignore lint: Transition priority needed */
padding 0.7s ease-in-out !important;
}
/* Glow-aware left padding so the glow doesn't clip | Thanks Aya <3*/
.rl-wbw-active {
padding-left: var(--rl-glow-outer) !important;
[class*="_lyricsText"] > div > span {
transition: text-shadow 0.7s ease-in-out, color 0.7s ease-in-out, padding 0.7s ease-in-out !important;
}
/* Lyrics container styling */
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"] {
margin-bottom: 2rem;
/* biome-ignore lint: Must beat Tidal _hasCues_ opacity */
opacity: 1 !important;
font-family:
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 700;
/* biome-ignore lint: Typography override for readability */
font-size: calc(38px * var(--rl-font-scale, 1)) !important;
[class^="_lyricsContainer"] > div > div > span {
margin-bottom: 2rem;
opacity: 1;
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-weight: 700;
font-size: 38px !important;
}
/* Hide the old Musixmatch attribution footer in the lyrics panel */
[data-test="now-playing-lyrics"] [class*="_footer_"] {
display: none !important;
}
/* MARKER: WBW lyrics CSS */
/* hide tidal spans for wbw */
.rl-wbw-active span[data-test="lyrics-line"] {
/* biome-ignore lint: Must hide original lines when word-by-word is on */
display: none !important;
}
/* Active line slide */
.rl-wbw-line {
text-align: left;
padding-left: 0;
padding-right: 0;
filter: none;
transform: translateZ(0);
transform-origin: left;
transition:
filter 0.4s ease,
padding-left 0.7s ease-in-out,
padding-right 0.7s ease-in-out;
overflow: visible;
}
.rl-wbw-line.rl-wbw-spacer {
filter: none;
}
/* Blur Inactive (opt-in via .rl-blur-active on container) */
.rl-blur-active .rl-wbw-line {
filter: blur(0.07em);
}
.rl-blur-active .rl-wbw-line.rl-pos-1 {
filter: blur(0.035em);
}
.rl-blur-active .rl-wbw-line.rl-pos-2 {
filter: blur(0.05em);
}
.rl-blur-active .rl-wbw-line.rl-pos-3 {
filter: blur(0.06em);
}
/* Active line overrides (MUST come after blur rules to win on equal specificity) */
.rl-wbw-line.rl-wbw-line-active,
.rl-blur-active .rl-wbw-line.rl-wbw-line-active {
padding-left: 20px;
filter: none;
}
/* Keep last-active line unblurred during instrumental gaps */
.rl-blur-active .rl-wbw-line.rl-gap-hold {
filter: none;
}
/* Bubbled Lyrics scale (opt-in via .rl-bubbled on container) */
.rl-bubbled .rl-wbw-line {
scale: 0.93 0.93 0.95;
transition:
scale 0.7s ease,
filter 0.4s ease,
padding-left 0.7s ease-in-out,
padding-right 0.7s ease-in-out;
will-change: scale, translate, filter;
}
.rl-bubbled .rl-wbw-line.rl-wbw-spacer {
scale: none;
}
.rl-bubbled .rl-wbw-line.rl-wbw-line-active {
scale: 1;
transition:
scale 0.5s ease,
filter 0.4s ease,
padding-left 0.7s ease-in-out,
padding-right 0.7s ease-in-out;
}
/* Staggered scroll bounce animation (part of Bubbled Lyrics) */
@keyframes rl-scroll-bounce {
from {
translate: 0 var(--rl-scroll-delta);
}
to {
translate: 0 0;
}
}
.rl-wbw-line:not(.rl-scroll-animate) {
animation: none;
}
.rl-scroll-animate {
animation: rl-scroll-bounce 400ms cubic-bezier(0.41, 0, 0.12, 0.99) both;
animation-delay: var(--rl-line-delay, 0ms);
}
/* Word span — opacity override needed to beat Tidal's ._hasCues_ span { opacity:.35 } */
.rl-wbw-word {
text-shadow:
0 0 0px transparent,
0 0 0px transparent;
color: rgba(255, 255, 255, 0.4);
/* biome-ignore lint: Must beat Tidal _hasCues_ opacity */
opacity: 1 !important;
transition:
text-shadow 0.15s ease-out,
color 0.15s ease-out;
}
/* Hover word (Grouped Syllables) */
.rl-wbw-line:not(.rl-wbw-line-active) > .rl-wbw-word:hover,
.rl-wbw-line:not(.rl-wbw-line-active) > .rl-wbw-word.rl-wbw-word-hover,
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-main .rl-wbw-word:hover,
.rl-wbw-line:not(.rl-wbw-line-active)
.rl-wbw-main
.rl-wbw-word.rl-wbw-word-hover {
text-shadow:
0 0 var(--rl-glow-inner, 2px) lightgray,
/* biome-ignore lint: Hover glow should override defaults */
0 0 var(--rl-glow-outer, 20px) lightgray !important;
/* biome-ignore lint: Hover color override */
color: lightgray !important;
cursor: pointer;
}
/* Active word */
.rl-wbw-word.rl-wbw-active {
text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #fff),
/* biome-ignore lint: Glow priority for active word */
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #fff) !important;
/* biome-ignore lint: Active word uses Colorama color */
color: var(--cl-glow1, #fff) !important;
}
/* MARKER: Syllable sweep animation CSS */
@keyframes rl-wipe {
from {
background-size:
0.75em 100%,
0% 100%,
100% 100%;
background-position:
-0.375em 0%,
left,
left;
}
to {
background-size:
0.75em 100%,
100% 100%,
100% 100%;
background-position:
calc(100% + 0.375em) 0%,
left,
left;
}
}
/* Syllable active: gradient sweep L-to-R via background-clip */
.rl-wbw-word.rl-syl-active {
/* biome-ignore lint: Kill base transitions so class swaps are instant */
transition: none !important;
/* biome-ignore lint: Transparent fill so gradient paints the text */
color: transparent !important;
/* biome-ignore lint: Clip gradient to text glyphs */
-webkit-background-clip: text !important;
/* biome-ignore lint: Clip gradient to text glyphs */
background-clip: text !important;
background-repeat: no-repeat;
background-image:
linear-gradient(
90deg,
transparent 0%,
var(--cl-glow1, #fff) 50%,
transparent 100%
),
linear-gradient(90deg, var(--cl-glow1, #fff) 100%, transparent 100%),
linear-gradient(90deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.4));
background-size:
0.75em 100%,
0% 100%,
100% 100%;
background-position:
-0.375em 0%,
left,
left;
/* biome-ignore lint: No glow for syllable mode */
text-shadow: none !important;
/* biome-ignore lint: No glow for syllable mode */
filter: none !important;
}
/* Syllable finished: word stays Colorama color, no glow */
.rl-wbw-word.rl-syl-finished {
/* biome-ignore lint: Kill base transitions so class swaps are instant */
transition: none !important;
/* biome-ignore lint: Finished syllable uses Colorama color */
color: var(--cl-glow1, #fff) !important;
/* biome-ignore lint: No glow for syllable mode */
text-shadow: none !important;
/* biome-ignore lint: No glow for syllable mode */
filter: none !important;
}
/* MARKER: Syllable animations CSS (WIP coming soon) */
/* syllableStyle: 0 = none, 1 = Pop!, 2 = Jump */
@keyframes rl-pop {
0%,
100% {
transform: scale(1);
}
25%,
35% {
transform: scale(1.03) translateY(-0.5%);
}
}
@keyframes rl-jump {
0% {
transform: translateY(8px);
}
50% {
transform: translateY(-3px);
}
100% {
transform: translateY(0);
}
}
/* Pop! for word mode */
.rl-syl-pop .rl-wbw-word.rl-wbw-active {
transform-origin: center bottom;
animation: rl-pop 0.6s ease-out;
}
/* Pop! for syllable mode */
.rl-syl-pop .rl-wbw-word.rl-syl-active {
transform-origin: center bottom;
}
/* Jump for word mode */
.rl-syl-jump .rl-wbw-word.rl-wbw-active {
animation: rl-jump 0.35s ease-out;
}
/* Tidals "..." at the top of the container */
.rl-wbw-active > span:not([data-test="lyrics-line"]) {
display: block;
font-size: calc(40px * var(--rl-font-scale, 1));
font-family:
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 700;
color: rgba(255, 255, 255, 0.4);
/* biome-ignore lint: Must beat Tidal _hasCues_ opacity */
opacity: 1 !important;
text-shadow: 0 0 0px transparent;
margin-bottom: 2rem;
}
/* MARKER: Context Aware Lyrics CSS */
/* Background vocal sub-container */
.rl-wbw-bg-container {
max-height: 0;
overflow: visible;
opacity: 0;
font-size: 0.55em;
padding-top: 0.15em;
transition:
max-height 0.3s ease,
opacity 0.5s ease;
color: rgba(255, 255, 255, 0.4);
}
.rl-wbw-line.rl-wbw-line-active .rl-wbw-bg-container {
max-height: 3em;
opacity: 1;
transition:
max-height 0.5s ease,
opacity 0.5s ease;
}
/* Singer duet positioning */
.rl-wbw-line.rl-singer-right {
text-align: end;
transform-origin: right;
}
.rl-dual-side .rl-wbw-line.rl-singer-left {
padding-right: 20%;
}
.rl-dual-side .rl-wbw-line.rl-singer-right {
padding-left: 20%;
}
.rl-dual-side .rl-wbw-line.rl-singer-right .rl-wbw-bg-container {
text-align: end;
}
.rl-dual-side .rl-wbw-line.rl-singer-right.rl-wbw-line-active {
padding-right: 20px;
}
.rl-dual-side .rl-wbw-line.rl-singer-left.rl-wbw-line-active {
padding-left: 20px;
}
/* Reset glow when disabled */
.lyrics-glow-disabled [data-test="now-playing-lyrics"] span[data-test="lyrics-line"][class*="_current_"],
.lyrics-glow-disabled [data-test="now-playing-lyrics"] span[data-test="lyrics-line"]:hover {
/* biome-ignore lint: Kill glow on active/hover lines */
text-shadow: none !important;
}
/* kill glow on active word */
.lyrics-glow-disabled .rl-wbw-word.rl-wbw-active {
/* biome-ignore lint: Kill glow on active word */
text-shadow: none !important;
}
/* kill glow on hovered word */
.lyrics-glow-disabled .rl-wbw-line:not(.rl-wbw-line-active)
> .rl-wbw-word:hover,
.lyrics-glow-disabled
.rl-wbw-line:not(.rl-wbw-line-active)
> .rl-wbw-word.rl-wbw-word-hover,
.lyrics-glow-disabled
.rl-wbw-line:not(.rl-wbw-line-active)
.rl-wbw-main
.rl-wbw-word:hover,
.lyrics-glow-disabled
.rl-wbw-line:not(.rl-wbw-line-active)
.rl-wbw-main
.rl-wbw-word.rl-wbw-word-hover {
/* biome-ignore lint: Kill glow on hovered word */
text-shadow: none !important;
}
/* Reset all lyrics styling when disabled */
.lyrics-glow-disabled [class*="_lyricsText"] > div > span[data-current="true"],
.lyrics-glow-disabled [class*="_lyricsText"] > div > span,
.lyrics-glow-disabled [class*="_lyricsText"] > div > span:hover,
.lyrics-glow-disabled [data-test="now-playing-track-title"],
.lyrics-glow-disabled [class^="_lyricsContainer"] > div > div > span {
text-shadow: none !important;
padding-left: 0 !important;
transition: none !important;
font-size: inherit !important;
color: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
margin-bottom: inherit !important;
opacity: inherit !important;
}
@@ -1,15 +1,15 @@
/* Hide player bar when setting is disabled, but show on hover - only when UI is hidden */
.radiant-lyrics-ui-hidden [data-test="footer-player"] {
opacity: 0 !important;
transition: opacity 0.5s ease-in-out !important;
opacity: 0 !important;
transition: opacity 0.5s ease-in-out !important;
}
.radiant-lyrics-ui-hidden [data-test="footer-player"]:hover {
opacity: 1 !important;
opacity: 1 !important;
}
/* Also show player bar when hovering over the bottom area - only when UI is hidden */
.radiant-lyrics-ui-hidden body.rl-footer-hover [data-test="footer-player"],
.radiant-lyrics-ui-hidden:has([data-test="footer-player"]:hover) [data-test="footer-player"],
.radiant-lyrics-ui-hidden [data-test="footer-player"]:hover {
opacity: 1 !important;
}
opacity: 1 !important;
}
+204 -273
View File
@@ -1,293 +1,224 @@
/* Sidebar */
[class*="_sidebar_"] {
background-color: transparent !important;
/* Only apply styles when UI is hidden */
.radiant-lyrics-ui-hidden [class*="tabItems"] {
opacity: 0 !important;
transition: opacity 0.4s ease-in-out;
}
/* Section header */
[class*="_sectionHeader_"] {
background-color: transparent !important;
/* Default state - visible */
[class*="tabItems"] {
transition: opacity 0.4s ease-in-out;
}
/* Rounded corners */
[class*="_thumbnail_"],
[class*="_imageWrapper_"],
[class*="_playButton_"] {
border-radius: 5px !important;
/* Tab items stay hidden - no hover functionality (if the song changes and it doesnt have lyrics.. and ya want them back.. you can unhide the UI <3) */
.radiant-lyrics-ui-hidden [data-test="header-container"]:not(:has(.hide-ui-button)) {
opacity: 0 !important;
transition: opacity 0.4s ease-in-out;
}
/* MARKER: HideUI CSS*/
/* Only apply styles when UI is hidden — hide toggle buttons */
.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;
transition: opacity 0.4s ease-in-out;
/* Keep header visible if it contains the Hide UI button, but hide its other children */
.radiant-lyrics-ui-hidden [data-test="header-container"]:has(.hide-ui-button) > *:not(.hide-ui-button) {
opacity: 0 !important;
transition: opacity 0.4s ease-in-out;
}
.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;
/* Default state for header */
[data-test="header-container"] {
transition: opacity 0.4s ease-in-out;
}
/* Hide header container (search, minimize, fullscreen) when UI is hidden */
.radiant-lyrics-ui-hidden [data-test="header"] {
opacity: 0 !important;
visibility: hidden !important;
transition: opacity 0.4s ease-in-out, visibility 0s linear 0.4s;
pointer-events: none !important;
/* Only prevent specific text elements in player bar from being affected by margin adjustments */
[data-test="footer-player"] [class*="_trackTitle"],
[data-test="footer-player"] [class*="_artistName"],
[data-test="footer-player"] [class*="_trackInfo"],
[data-test="footer-player"] [class*="_trackContainer"] {
margin-top: 0 !important;
transform: none !important;
}
/* Immediate hide class for unhide button */
/* Immediate hide class for unhide button with smooth transition (had issues with the fade out.. so I removed it) */
.hide-immediately {
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
}
/* Auto-fade styling for unhide button */
[class^="_bar"] {
background-color: transparent;
}
.radiant-lyrics-ui-hidden [class^="_bar"]>*:not(.hide-ui-button) {
opacity: 0 !important;
pointer-events: none !important;
transition: opacity 0.4s ease-in-out;
}
/* Default state for bar elements */
[class^="_bar"]>* {
transition: opacity 0.4s ease-in-out;
}
/* Hide search box and make it non-interactive */
.radiant-lyrics-ui-hidden [data-test="search-input"],
.radiant-lyrics-ui-hidden [class*="_searchInput"],
.radiant-lyrics-ui-hidden [class*="searchInput"],
.radiant-lyrics-ui-hidden [class*="_search"],
.radiant-lyrics-ui-hidden [class*="search"],
.radiant-lyrics-ui-hidden input[type="search"],
.radiant-lyrics-ui-hidden input[type="text"],
.radiant-lyrics-ui-hidden input[placeholder*="Search"],
.radiant-lyrics-ui-hidden input[placeholder*="search"],
.radiant-lyrics-ui-hidden [placeholder*="Search"],
.radiant-lyrics-ui-hidden [data-test="main-layout-header"] input,
.radiant-lyrics-ui-hidden [data-test="main-layout-header"] [class*="input"],
.radiant-lyrics-ui-hidden header input,
.radiant-lyrics-ui-hidden nav input {
pointer-events: none !important;
cursor: default !important;
user-select: none !important;
}
/* Hide bottom left controls completely - no hover functionality */
/* Exclude heart button in player bar and make sure hidden buttons can't be clicked */
.radiant-lyrics-ui-hidden [data-test="add-to-playlist"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [data-test="remove-from-playlist"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [data-test="like-toggle"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [data-test="dislike-toggle"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [data-test="favorite-toggle"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [data-test="heart-button"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [data-test="playlist-add"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [class*="_trackActions"],
.radiant-lyrics-ui-hidden [class*="_bottomLeftControls"],
.radiant-lyrics-ui-hidden [class*="_actionButtons"],
.radiant-lyrics-ui-hidden [class*="_favoriteButton"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [class*="_addToPlaylist"],
.radiant-lyrics-ui-hidden [class*="_lowerLeft"],
.radiant-lyrics-ui-hidden [class*="_bottomActions"],
.radiant-lyrics-ui-hidden [class*="_mediaControls"] > div:first-child,
.radiant-lyrics-ui-hidden button[title*="Add to"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[title*="Remove from"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[title*="Like"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[title*="Favorite"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[title*="Heart"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[aria-label*="Add to"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[aria-label*="Remove from"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[aria-label*="Like"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[aria-label*="Favorite"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[aria-label*="Heart"]:not([data-test="footer-player"] *),
/* Target buttons in bottom left area specifically - (idk if this is needed.. but it's here) */
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] button[class*="_button"]:not(.unhide-ui-button),
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] [class*="_iconButton"]:not(.unhide-ui-button),
/* Additional catch-all for bottom left area buttons - (idk if this is needed.. but it's here) */
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] > div > div:first-child button:not(.unhide-ui-button),
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] > div:first-child button:not(.unhide-ui-button) {
opacity: 0 !important;
pointer-events: none !important;
transition: opacity 0.5s ease-in-out !important;
}
/* No hover functionality in Hide UI Mode - buttons stay hidden.. yea thats right, you heard me */
/* Default state for control buttons */
[data-test="add-to-playlist"],
[data-test="remove-from-playlist"],
[data-test="like-toggle"],
[data-test="dislike-toggle"],
[data-test="favorite-toggle"],
[data-test="heart-button"],
[data-test="playlist-add"],
[class*="_trackActions"],
[class*="_bottomLeftControls"],
[class*="_actionButtons"],
[class*="_favoriteButton"],
[class*="_addToPlaylist"],
[class*="_lowerLeft"],
[class*="_bottomActions"],
[class*="_mediaControls"] > div:first-child,
button[title*="Add to"],
button[title*="Remove from"],
button[title*="Like"],
button[title*="Favorite"],
button[title*="Heart"],
button[aria-label*="Add to"],
button[aria-label*="Remove from"],
button[aria-label*="Like"],
button[aria-label*="Favorite"],
button[aria-label*="Heart"],
[class*="_nowPlayingContainer"] button[class*="_button"]:not(.unhide-ui-button),
[class*="_nowPlayingContainer"] [class*="_iconButton"]:not(.unhide-ui-button),
[class*="_nowPlayingContainer"] > div > div:first-child button:not(.unhide-ui-button),
[class*="_nowPlayingContainer"] > div:first-child button:not(.unhide-ui-button) {
transition: opacity 0.5s ease-in-out;
}
/* Smooth cover art movement when UI is hidden */
[class*="_albumImage"],
[class*="_coverArt"],
figure[class*="_albumImage"] {
transition: transform 0.6s ease-in-out;
}
.radiant-lyrics-ui-hidden [class*="_albumImage"],
.radiant-lyrics-ui-hidden [class*="_coverArt"],
.radiant-lyrics-ui-hidden figure[class*="_albumImage"] {
transform: translateX(80px) !important;
}
/* Smooth track info wrapper movement when UI is hidden (Arists & Track Title) */
[class*="_infoWrapper"],
[class*="_textContainer"] {
transition: transform 0.6s ease-in-out;
}
.radiant-lyrics-ui-hidden [class*="_infoWrapper"],
.radiant-lyrics-ui-hidden [class*="_textContainer"] {
transform: translateX(40px) !important;
}
/* Move parent containers instead of lyrics container directly to preserve gradient fade */
[data-test="stream-metadata"],
[class*="_rightColumn"],
[class*="_rightSide"],
[class*="_contentRight"],
[class*="_sidePanel"],
[class*="_lyricsSection"],
[class*="_lyricsWrapper"] {
transition: transform 0.6s ease-in-out;
}
.radiant-lyrics-ui-hidden [data-test="stream-metadata"],
.radiant-lyrics-ui-hidden [class*="_rightColumn"],
.radiant-lyrics-ui-hidden [class*="_rightSide"],
.radiant-lyrics-ui-hidden [class*="_contentRight"],
.radiant-lyrics-ui-hidden [class*="_sidePanel"],
.radiant-lyrics-ui-hidden [class*="_lyricsSection"],
.radiant-lyrics-ui-hidden [class*="_lyricsWrapper"] {
transform: translateX(60px) translateY(-70px) !important;
}
/* Hide UI button base styling - just the transition */
.hide-ui-button {
transition: opacity 0.5s ease-in-out, visibility 0.5s ease-in-out, background-color 0.2s ease-in-out, transform 0.2s ease-in-out !important;
}
/* Auto-fade styling for unhide button - (Keeps Text Visible, just not full opacity) | Cheers @Zyhn for the idea*/
.unhide-ui-button.auto-faded {
background-color: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
color: rgba(255, 255, 255, 0.4) !important;
transition:
background-color 0.8s ease-in-out,
border-color 0.8s ease-in-out,
box-shadow 0.8s ease-in-out,
backdrop-filter 0.8s ease-in-out,
color 0.8s ease-in-out !important;
background-color: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
color: rgba(255, 255, 255, 0.8) !important;
transition: background-color 0.8s ease-in-out, border-color 0.8s ease-in-out, box-shadow 0.8s ease-in-out, backdrop-filter 0.8s ease-in-out, color 0.8s ease-in-out;
}
/* Restore button styling on hover */
.unhide-ui-button.auto-faded:hover {
background-color: rgba(255, 255, 255, 0.2) !important;
border-color: rgba(255, 255, 255, 0.3) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(10px) !important;
-webkit-backdrop-filter: blur(10px) !important;
color: white !important;
transition:
background-color 0.3s ease-in-out,
border-color 0.3s ease-in-out,
box-shadow 0.3s ease-in-out,
backdrop-filter 0.3s ease-in-out,
color 0.3s ease-in-out !important;
}
/* MARKER: Sticky Lyrics CSS */
/* Lyrics toggle button */
[data-test="toggle-lyrics"]:has(.sticky-lyrics-trigger) {
position: relative !important;
padding-right: 38px !important;
}
/* Trigger */
.sticky-lyrics-trigger {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 38px;
display: flex;
align-items: center;
justify-content: center;
padding-left: 5px;
padding-right: 0px;
box-sizing: border-box;
cursor: default;
color: #CCCCD1;
transition: color 0.2s ease;
}
/* Divider line */
.sticky-lyrics-trigger::before {
content: "";
position: absolute;
left: 5px;
top: 4px;
bottom: 4px;
width: 1px;
background: transparent;
transition: background 0.2s ease;
}
/* When Lyrics toggle is pressed — show divider & adjust icon */
[data-test="toggle-lyrics"][aria-pressed="true"] .sticky-lyrics-trigger {
color: rgb(30, 30, 30);
cursor: pointer;
}
[data-test="toggle-lyrics"][aria-pressed="true"] .sticky-lyrics-trigger::before {
background: rgba(0, 0, 0, 0.15);
}
[data-test="toggle-lyrics"][aria-pressed="true"] .sticky-lyrics-trigger:hover {
color: rgba(0, 0, 0, 0.5);
}
/* Animate widening when dropdown opens */
[data-test="toggle-lyrics"]:has(.sticky-lyrics-trigger) {
transition: min-width 0.12s ease-out;
}
/* 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 {
position: fixed;
background: rgb(255, 255, 255);
border-radius: 0 0 12px 12px;
padding: 8px 12px 10px;
box-sizing: border-box;
z-index: 10000;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
clip-path: inset(0 -20px -20px -20px);
animation: stickyLyricsDropdownIn 0.12s ease-out;
}
@keyframes stickyLyricsDropdownIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Row containing label + toggle */
.sticky-lyrics-dropdown-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.sticky-lyrics-label {
font-size: 11px;
font-weight: 600;
color: rgba(0, 0, 0, 0.8);
white-space: nowrap;
}
/* Toggle switch */
.sticky-lyrics-switch {
position: relative;
display: inline-block;
width: 34px;
height: 18px;
flex-shrink: 0;
}
.sticky-lyrics-switch input {
opacity: 0;
width: 0;
height: 0;
}
.sticky-lyrics-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.15);
transition: 0.3s;
border-radius: 18px;
}
.sticky-lyrics-slider::before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 2px;
bottom: 2px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
.sticky-lyrics-switch input:checked + .sticky-lyrics-slider {
background-color: rgb(30, 30, 30);
}
.sticky-lyrics-switch input:checked + .sticky-lyrics-slider::before {
transform: translateX(16px);
background-color: rgb(255, 255, 255);
}
/* Segmented control (Line | Word | Syllable) */
.rl-style-row {
justify-content: center;
margin-top: 6px;
}
.rl-seg-control {
display: flex;
background: rgba(0, 0, 0, 0.06);
border-radius: 10px;
padding: 2px;
gap: 2px;
width: 100%;
}
.rl-seg-btn {
flex: 1;
border: none;
background: transparent;
color: rgba(0, 0, 0, 0.4);
font-size: 10px;
font-weight: 600;
padding: 5px 0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.rl-seg-btn:hover {
color: rgba(0, 0, 0, 0.7);
background: rgba(0, 0, 0, 0.06);
}
.rl-seg-btn.rl-seg-active {
background: rgb(30, 30, 30);
color: rgb(255, 255, 255);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
/* MARKER: PATCHES (Random Fixes for Tidals Changes) */
/* These change allot so i gave them their own section */
/* Remove max-width cap on now-playing content */
[class*="_contentInner"] {
max-width: none !important;
}
/* Round now-playing artwork corners */
[data-test="now-playing-artwork"] {
/* biome-ignore lint: Override flat corners */
border-radius: 10px !important;
}
/* Hide the Overlay Scrollbar (people just use mouse scroll) */
.os-scrollbar {
display: none !important;
pointer-events: none !important;
background-color: rgba(255, 255, 255, 0.2) !important;
border-color: rgba(255, 255, 255, 0.3) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(10px) !important;
-webkit-backdrop-filter: blur(10px) !important;
color: white !important;
transition: background-color 0.3s ease-in-out, border-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out, backdrop-filter 0.3s ease-in-out, color 0.3s ease-in-out;
}