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 3796 additions and 10578 deletions
+4 -2
View File
@@ -1,4 +1,6 @@
node_modules/ node_modules/
dist/ dist/
Notes.md dist/itzzexcel.oled-theme.json
/Reference/ 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 ## Plugins
### 🎨 Obsidian ### 🎨 OLED Theme
**Location:** `plugins/obsidian-theme-luna/` **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:** **Features:**
- Applies a dark, OLED-optimized theme - Applies a dark, OLED-optimized theme
- Fetches the latest theme CSS from the GitHub repository
- Reduces battery consumption on OLED displays.. i guess <3 - Reduces battery consumption on OLED displays.. i guess <3
- Modern, sleek dark interface - 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 - Automatic clipboard copying of selected lyrics
- Smart lyric span detection - 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 ### 🎶 Audio Visualizer
**Location:** `plugins/audio-visualizer-luna/` **Location:** `plugins/audio-visualizer-luna/`
@@ -58,21 +49,8 @@ Allows users to hide/remove UI elements by right clicking on them.
## Installation ## 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 ### Installing from URL
### (They are in the store by default now) 1. Open TidalLuna after Building & Serving
1. Open TidaLuna after Building & Serving
2. Navigate to Luna Settings (Top right of Tidal) 2. Navigate to Luna Settings (Top right of Tidal)
3. Click "Plugin Store" Tab 3. Click "Plugin Store" Tab
4. Paste in the "Install from URL" Bar `https://github.com/meowarex/tidalluna-plugins/releases/download/latest/store.json` 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 git clone https://github.com/meowarex/tidalluna-plugins
# Change Folder to the Repo # Change Folder to the Repo
cd tidalluna-plugins cd neptune-projects-fork
# Install dependencies # Install dependencies
pnpm install pnpm install
@@ -95,7 +73,7 @@ pnpm run watch
``` ```
### Installing Plugins in TidalLuna ### 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) 2. Navigate to Luna Settings (Top right of Tidal)
3. Click "Plugin Store" Tab 3. Click "Plugin Store" Tab
4. Click Install on the Plugins at the top Labeled with "[Dev]" 4. Click Install on the Plugins at the top Labeled with "[Dev]"
@@ -104,7 +82,7 @@ pnpm run watch
## Development ## Development
This project is made for: 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 ## GitHub Actions
@@ -112,7 +90,10 @@ This project is made for:
- **Release automation** for distributing plugins - **Release automation** for distributing plugins
- **Artifact uploads** for easy plugin distribution - **Artifact uploads** for easy plugin distribution
## Based On <3
- **itzzexcel** - [GitHub](https://github.com/ItzzExcel)
## Credits ## Credits
Inrixia | [TidalLuna](https://github.com/Inrixia/TidaLuna) Plugin Framework (The successor Neptune) Original Neptune versions by itzzexcel. Ported to Luna framework following the Luna plugin template structure by meowarex with help from Inrixia <3
ItzzExcel | The Original Neptune version of "Radiant Lyrics" (Which was ported to Luna and Rewritten by me!)
-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
+347 -411
View File
@@ -1,436 +1,372 @@
import { ReactiveStore } from "@luna/core"; import { ReactiveStore } from "@luna/core";
import { import { LunaNumberSetting, LunaSettings, LunaSwitchSetting } from "@luna/ui";
LunaSettings,
LunaNumberSetting,
LunaSwitchSetting,
LunaTextSetting,
} from "@luna/ui";
import React from "react"; import React from "react";
const isWindows = navigator.userAgent.includes("Windows"); // tidal changes it in reqs navigator supplies the default electron one
export const settings = await ReactiveStore.getPluginStorage( export const settings = await ReactiveStore.getPluginStorage("AudioVisualizer", {
"AudioVisualizer", barCount: 32,
{ barColor: "#ffffff",
barCount: 32, barRounding: true,
barColor: "#ffffff", customColors: [] as string[],
barRounding: true, spotifyAPI: isWindows
customColors: [] as string[], });
},
);
export const Settings = () => { export const Settings = () => {
const [barCount, setBarCount] = React.useState(settings.barCount); const [barCount, setBarCount] = React.useState(settings.barCount);
const [barColor, setBarColor] = React.useState(settings.barColor); const [barColor, setBarColor] = React.useState(settings.barColor);
const [barRounding, setBarRounding] = React.useState(settings.barRounding); const [barRounding, setBarRounding] = React.useState(settings.barRounding);
const [showColorPicker, setShowColorPicker] = React.useState(false); const [spotifyAPI, setSpotifyAPI] = React.useState(settings.spotifyAPI);
const [isAnimatingIn, setIsAnimatingIn] = React.useState(false); const [showColorPicker, setShowColorPicker] = React.useState(false);
const [shouldRender, setShouldRender] = React.useState(false); const [isAnimatingIn, setIsAnimatingIn] = React.useState(false);
const [customInput, setCustomInput] = React.useState(settings.barColor); const [shouldRender, setShouldRender] = React.useState(false);
const [customColors, setCustomColors] = React.useState(settings.customColors); const [customInput, setCustomInput] = React.useState(settings.barColor);
const [hoveredColorIndex, setHoveredColorIndex] = React.useState< const [customColors, setCustomColors] = React.useState(settings.customColors);
number | null const [hoveredColorIndex, setHoveredColorIndex] = React.useState<number | null>(null);
>(null);
const closeColorPicker = () => { const closeColorPicker = () => {
setIsAnimatingIn(false); setIsAnimatingIn(false);
setTimeout(() => { setTimeout(() => {
setShowColorPicker(false); setShowColorPicker(false);
setShouldRender(false); setShouldRender(false);
}, 200); // Wait for animation to complete because i need to }, 200); // Wait for animation to complete because i need to
}; };
const openColorPicker = () => { const openColorPicker = () => {
setShowColorPicker(true); setShowColorPicker(true);
setShouldRender(true); setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10); setTimeout(() => setIsAnimatingIn(true), 10);
}; };
React.useEffect(() => { React.useEffect(() => {
if (showColorPicker) { if (showColorPicker) {
setShouldRender(true); setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10); setTimeout(() => setIsAnimatingIn(true), 10);
} }
}, [showColorPicker]); }, [showColorPicker]);
// Common color presets for cool points :D // Common color presets for cool points :D
const colorPresets = [ const colorPresets = [
"#ffffff", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff",
"#ff0000", "#ff8800", "#8800ff", "#0088ff", "#88ff00", "#ff0088", "#00ff88",
"#00ff00", "#444444", "#888888", "#cccccc", "#1db954", "#e22134", "#1976d2"
"#0000ff", ];
"#ffff00",
"#ff00ff",
"#00ffff",
"#ff8800",
"#8800ff",
"#0088ff",
"#88ff00",
"#ff0088",
"#00ff88",
"#444444",
"#888888",
"#cccccc",
"#1db954",
"#e22134",
"#1976d2",
];
const updateColor = (color: string) => { const updateColor = (color: string) => {
setBarColor(color); setBarColor(color);
setCustomInput(color); setCustomInput(color);
settings.barColor = color; settings.barColor = color;
(window as any).updateAudioVisualizer?.(); (window as any).updateAudioVisualizer?.();
}; };
const addCustomColor = () => { const addCustomColor = () => {
if (customInput) { if (customInput) {
// Trim whitespace and convert to lowercase // Trim whitespace and convert to lowercase
const trimmedInput = customInput.trim().toLowerCase(); const trimmedInput = customInput.trim().toLowerCase();
// Validate hex color format // Validate hex color format
const hexColorRegex = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i; const hexColorRegex = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
if ( if (hexColorRegex.test(trimmedInput) &&
hexColorRegex.test(trimmedInput) && !colorPresets.includes(trimmedInput) &&
!colorPresets.includes(trimmedInput) && !customColors.includes(trimmedInput)) {
!customColors.includes(trimmedInput) const newCustomColors = [...customColors, trimmedInput];
) { setCustomColors(newCustomColors);
const newCustomColors = [...customColors, trimmedInput]; settings.customColors = newCustomColors;
setCustomColors(newCustomColors); }
settings.customColors = newCustomColors; }
} };
}
};
const removeCustomColor = (colorToRemove: string) => { const removeCustomColor = (colorToRemove: string) => {
const newCustomColors = customColors.filter( const newCustomColors = customColors.filter(color => color !== colorToRemove);
(color) => color !== colorToRemove, setCustomColors(newCustomColors);
); settings.customColors = newCustomColors;
setCustomColors(newCustomColors);
settings.customColors = newCustomColors;
// If the removed color was the selected color (reset to white) // If the removed color was the selected color (reset to white)
if (barColor === colorToRemove) { if (barColor === colorToRemove) {
updateColor("#ffffff"); updateColor("#ffffff");
} }
}; };
const allColors = [...colorPresets, ...customColors]; const allColors = [...colorPresets, ...customColors];
return ( return (
<LunaSettings> <LunaSettings> <LunaSwitchSetting
<LunaSwitchSetting title="Spotify API"
title="Bar Roundness" desc="Use Spotify's audio analysis API instead of real-time audio data (Required for Windows)"
desc="Enable rounded corners on visualizer bars" // @ts-expect-error no idea why this errosr wth
checked={barRounding} checked={spotifyAPI}
onChange={(_, checked) => { disabled={isWindows} // Disable on non-Windows platforms
setBarRounding(checked); // @ts-expect-error no idea why this errosr wth
settings.barRounding = checked; onChange={(_, checked) => {
(window as any).updateAudioVisualizer?.(); setSpotifyAPI(checked);
}} settings.spotifyAPI = checked;
/> (window as any).updateAudioVisualizer?.();
}}
/>
<LunaNumberSetting <LunaSwitchSetting
title="Bar Count" title="Bar Roundness"
desc="Number of frequency bars to display" desc="Enable rounded corners on visualizer bars"
min={8} // @ts-expect-error no idea why this errosr wth
max={64} checked={barRounding}
step={1} // @ts-expect-error no idea why this errosr wth
value={barCount} onChange={(_, checked) => {
onNumber={(value: number) => { setBarRounding(checked);
setBarCount(value); settings.barRounding = checked;
settings.barCount = value; (window as any).updateAudioVisualizer?.();
(window as any).updateAudioVisualizer?.(); }}
}} />
/>
{/* YUP YOUR EYES WORK... we do be using React code in the settings..*/} <LunaNumberSetting
{/* I'm not sure if this is a good idea, but it works & looks amazing */} title="Bar Count"
{/* Sorry @Inrixia <3 */} 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?.();
}}
/>
<div {/* YUP YOUR EYES WORK... we do be using React code in the settings..*/}
style={{ {/* I'm not sure if this is a good idea, but it works & looks amazing */}
padding: "16px 0", {/* Sorry @Inrixia <3 */}
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 */} <div style={{
{shouldRender && ( padding: "16px 0",
<> display: "flex",
{/* Backdrop */} justifyContent: "space-between",
<div alignItems: "center"
style={{ }}>
position: "fixed", <div>
top: 0, <div style={{ fontWeight: "normal", fontSize: "1.075rem", marginBottom: "4px" }}>Bar Color</div>
left: 0, <div style={{ opacity: 0.7, fontSize: "14px" }}>Color of the visualizer bars</div>
right: 0, </div>
bottom: 0, <div style={{ display: "flex", gap: "8px", alignItems: "center", position: "relative" }}>
background: "rgba(0,0,0,0.6)", <button
zIndex: 1000, onClick={() => showColorPicker ? closeColorPicker() : openColorPicker()}
opacity: isAnimatingIn ? 1 : 0, style={{
transition: "opacity 0.2s ease", width: "32px",
}} height: "32px",
onClick={closeColorPicker} 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>
{/* Color Picker Panel */} {/* Custom Color Picker Modal */}
<div {shouldRender && (
style={{ <>
position: "fixed", {/* Backdrop */}
top: "50%", <div
left: "50%", style={{
background: "rgba(20,20,20,0.98)", position: "fixed",
backdropFilter: "blur(20px)", top: 0,
WebkitBackdropFilter: "blur(20px)", left: 0,
border: "1px solid rgba(255,255,255,0.15)", right: 0,
borderRadius: "16px", bottom: 0,
padding: "20px", background: "rgba(0,0,0,0.6)",
minWidth: "320px", zIndex: 1000,
maxWidth: "90vw", opacity: isAnimatingIn ? 1 : 0,
maxHeight: "90vh", transition: "opacity 0.2s ease"
zIndex: 1001, }}
boxShadow: "0 20px 40px rgba(0,0,0,0.7)", onClick={closeColorPicker}
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 */} {/* Color Picker Panel */}
<div <div style={{
style={{ position: "fixed",
display: "grid", top: "50%",
gridTemplateColumns: "repeat(7, 1fr)", left: "50%",
gap: "8px", background: "rgba(20,20,20,0.98)",
marginBottom: "16px", backdropFilter: "blur(20px)",
}} WebkitBackdropFilter: "blur(20px)",
> border: "1px solid rgba(255,255,255,0.15)",
{allColors.map((color, index) => { borderRadius: "16px",
const isCustomColor = customColors.includes(color); padding: "20px",
const isHovered = hoveredColorIndex === index; minWidth: "320px",
return ( maxWidth: "90vw",
<div maxHeight: "90vh",
key={index} zIndex: 1001,
style={{ boxShadow: "0 20px 40px rgba(0,0,0,0.7)",
position: "relative", opacity: isAnimatingIn ? 1 : 0,
width: "32px", transform: isAnimatingIn ? "translate(-50%, -50%) scale(1)" : "translate(-50%, -50%) scale(0.9)",
height: "32px", transition: "all 0.2s ease"
cursor: "pointer", }}>
}} <div style={{ marginBottom: "12px", color: "#fff", fontWeight: "bold", fontSize: "14px" }}>
className="color-item" Choose Color
onMouseEnter={() => setHoveredColorIndex(index)} </div>
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 */} {/* Color Grid */}
<div style={{ marginBottom: "12px" }}> <div style={{
<div display: "grid",
style={{ gridTemplateColumns: "repeat(7, 1fr)",
color: "rgba(255,255,255,0.7)", gap: "8px",
fontSize: "12px", marginBottom: "16px"
marginBottom: "6px", }}>
}} {allColors.map((color, index) => {
> const isCustomColor = customColors.includes(color);
Add Custom Color const isHovered = hoveredColorIndex === index;
</div> return (
<div <div
style={{ key={index}
display: "flex", style={{
gap: "8px", position: "relative",
alignItems: "center", width: "32px",
}} height: "32px",
> cursor: "pointer"
<input }}
type="text" className="color-item"
value={customInput} onMouseEnter={() => setHoveredColorIndex(index)}
onChange={(e) => setCustomInput(e.target.value)} onMouseLeave={() => setHoveredColorIndex(null)}
onKeyDown={(e) => { >
if (e.key === "Enter") { <button
updateColor(customInput); onClick={() => {
addCustomColor(); updateColor(color);
} closeColorPicker();
}} }}
placeholder="#ffffff" style={{
style={{ width: "100%",
flex: 1, height: "100%",
padding: "8px 12px", borderRadius: "6px",
borderRadius: "6px", border: barColor === color ? "2px solid #fff" : "1px solid rgba(255,255,255,0.2)",
border: "1px solid rgba(255,255,255,0.2)", background: color,
background: "rgba(255,255,255,0.1)", cursor: "pointer",
color: "#fff", transition: "all 0.2s ease"
fontSize: "14px", }}
fontFamily: "monospace", />
boxSizing: "border-box", {isCustomColor && (
}} <button
/> onClick={(e) => {
<button e.stopPropagation();
onClick={() => { removeCustomColor(color);
updateColor(customInput); }}
addCustomColor(); style={{
}} position: "absolute",
style={{ top: "-4px",
width: "32px", right: "-4px",
height: "32px", width: "16px",
borderRadius: "6px", height: "16px",
border: "1px solid rgba(255,255,255,0.3)", borderRadius: "50%",
background: "rgba(255,255,255,0.15)", border: "1px solid rgba(255,255,255,0.8)",
color: "#fff", background: "rgba(0,0,0,0.8)",
cursor: "pointer", color: "#fff",
fontSize: "16px", cursor: "pointer",
display: "flex", fontSize: "10px",
alignItems: "center", display: "flex",
justifyContent: "center", alignItems: "center",
transition: "all 0.2s ease", justifyContent: "center",
}} opacity: isHovered ? 1 : 0,
onMouseEnter={(e) => { transition: "opacity 0.2s ease",
e.currentTarget.style.background = zIndex: 10
"rgba(255,255,255,0.25)"; }}
}} className="remove-button"
onMouseLeave={(e) => { >
e.currentTarget.style.background = ×
"rgba(255,255,255,0.15)"; </button>
}} )}
> </div>
+ );
</button> })}
</div> </div>
</div>
{/* Close Button (Done) - Also runs when color chosen*/} {/* Custom Hex Input */}
<button <div style={{ marginBottom: "12px" }}>
onClick={closeColorPicker} <div style={{ color: "rgba(255,255,255,0.7)", fontSize: "12px", marginBottom: "6px" }}>
style={{ Add Custom Color
width: "100%", </div>
padding: "8px", <div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
borderRadius: "6px", <input
border: "1px solid rgba(255,255,255,0.2)", type="text"
background: "rgba(255,255,255,0.1)", value={customInput}
color: "#fff", onChange={(e) => setCustomInput(e.target.value)}
cursor: "pointer", onKeyDown={(e) => {
fontSize: "12px", if (e.key === 'Enter') {
}} updateColor(customInput);
> addCustomColor();
Done }
</button> }}
</div> placeholder="#ffffff"
</> style={{
)} flex: 1,
</div> padding: "8px 12px",
</div> borderRadius: "6px",
</LunaSettings> 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>
);
}; };
File diff suppressed because it is too large Load Diff
+26 -30
View File
@@ -1,60 +1,56 @@
/* Audio Visualizer CSS - Only applies to the Visualizer */ /* Audio Visualizer CSS - Only applies to the Visualizer */
#audio-visualizer-container { #audio-visualizer-container {
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
} }
#audio-visualizer-container:hover { #audio-visualizer-container:hover {
transform: scale(1.02); transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
} }
#audio-visualizer-container canvas { #audio-visualizer-container canvas {
display: block; display: block;
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
} }
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
#audio-visualizer-container { #audio-visualizer-container {
margin: 4px; margin: 4px;
padding: 2px; padding: 2px;
} }
#audio-visualizer-container canvas { #audio-visualizer-container canvas {
max-width: 150px; max-width: 150px;
max-height: 30px; max-height: 30px;
} }
} }
/* Where to put the thingy */ /* Where to put the thingy */
[class*="_searchField"] { [class*="_searchField"] {
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
}
[data-type="search-field"] {
min-width: 220px !important;
} }
/* Shadow when active - doesnt seem to only apply when active but thats better */ /* Shadow when active - doesnt seem to only apply when active but thats better */
#audio-visualizer-container.active { #audio-visualizer-container.active {
box-shadow: 0 0 20px rgba(255, 255, 255, 0.3); box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
} }
/* Fade in animation */ /* Fade in animation */
@keyframes fadeIn { @keyframes fadeIn {
from { from {
opacity: 0; opacity: 0;
transform: scale(0.8); transform: scale(0.8);
} }
to { to {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }
} }
#audio-visualizer-container { #audio-visualizer-container {
animation: fadeIn 0.5s ease-out; animation: fadeIn 0.5s ease-out;
} }
@@ -1,813 +0,0 @@
import { ReactiveStore } from "@luna/core";
import { LunaSettings, LunaSwitchSetting } from "@luna/ui";
import React from "react";
declare global {
interface Window {
applyColoramaLyrics?: () => void;
}
}
// Define a typed onChange signature for the switch
type SwitchChangeHandler = (
event: React.ChangeEvent<HTMLInputElement> | null,
checked: boolean,
) => void;
export type ColoramaMode =
| "single"
| "gradient-experimental"
| "cover"
| "cover-gradient";
export const settings = await ReactiveStore.getPluginStorage("ColoramaLyrics", {
enabled: true,
mode: "single" as ColoramaMode,
// Store colors as RGB hex (#RRGGBB) and opacity separately (0-100)
singleColor: "#FFFFFF",
singleAlpha: 100,
gradientStart: "#FFFFFF",
gradientStartAlpha: 100,
gradientEnd: "#AAFFFF",
gradientEndAlpha: 100,
gradientAngle: 0,
customColors: [] as string[],
excludeInactive: false,
});
export const Settings = () => {
// const [enabled, setEnabled] = React.useState(settings.enabled);
const [mode, setMode] = React.useState<ColoramaMode>(settings.mode);
const [singleColor, setSingleColor] = React.useState(settings.singleColor);
const [singleAlpha, setSingleAlpha] = React.useState<number>(
settings.singleAlpha ?? 100,
);
const [gradientStart, setGradientStart] = React.useState(
settings.gradientStart,
);
const [gradientStartAlpha, setGradientStartAlpha] = React.useState<number>(
settings.gradientStartAlpha ?? 100,
);
const [gradientEnd, setGradientEnd] = React.useState(settings.gradientEnd);
const [gradientEndAlpha, setGradientEndAlpha] = React.useState<number>(
settings.gradientEndAlpha ?? 100,
);
const [gradientAngle, setGradientAngle] = React.useState(
settings.gradientAngle,
);
const [customInput, setCustomInput] = React.useState(settings.singleColor);
const [customColors, setCustomColors] = React.useState(settings.customColors);
const [showPicker, setShowPicker] = React.useState(false);
const [isAnimatingIn, setIsAnimatingIn] = React.useState(false);
const [shouldRender, setShouldRender] = React.useState(false);
const [excludeInactive, setExcludeInactive] = React.useState(
settings.excludeInactive,
);
const [activeEndpoint, setActiveEndpoint] = React.useState<
"single" | "start" | "end"
>("single");
const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType<{
title: string;
desc?: string;
checked: boolean;
onChange: SwitchChangeHandler;
}>;
// Helper for HEX normalization
const normalizeToRGB = (
hex: string,
fallback: string = "#FFFFFF",
): string => {
let v = hex.trim().toLowerCase();
if (!v.startsWith("#")) v = `#${v}`;
// #rgb or #rgba -> expand
if (/^#([0-9a-f]{3,4})$/.test(v)) {
const m = v.slice(1);
const r = m[0];
const g = m[1];
const b = m[2];
// ignore alpha if provided (#rgba)
return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
}
// #aarrggbb -> strip alpha
if (/^#([0-9a-f]{8})$/.test(v)) {
const rrggbb = v.slice(3);
return `#${rrggbb}`.toUpperCase();
}
// #rrggbb
if (/^#([0-9a-f]{6})$/.test(v)) return v.toUpperCase();
return fallback;
};
const colorPresets = [
"#FFFFFF",
"#FF0000",
"#00FF00",
"#0000FF",
"#FFFF00",
"#FF00FF",
"#00FFFF",
"#FF8800",
"#8800FF",
"#0088FF",
"#88FF00",
"#FF0088",
"#00FF88",
"#444444",
"#888888",
"#CCCCCC",
"#1DB954",
"#E22134",
"#1976D2",
];
const openPicker = (endpoint: "single" | "start" | "end" = "single") => {
setActiveEndpoint(endpoint);
setShowPicker(true);
setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10);
};
const closePicker = () => {
setIsAnimatingIn(false);
setTimeout(() => {
setShowPicker(false);
setShouldRender(false);
}, 200);
};
const hexColorRegex = /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3,4})$/i;
const applyCustomInputColor = (raw: string, updateInput: boolean): void => {
const trimmed = raw.trim();
if (!hexColorRegex.test(trimmed)) return;
if (mode === "single") {
const next = normalizeToRGB(trimmed);
settings.singleColor = next;
setSingleColor(next);
if (updateInput) setCustomInput(next);
} else if (mode === "gradient-experimental") {
const next = normalizeToRGB(trimmed);
if (activeEndpoint === "end") {
settings.gradientEnd = next;
setGradientEnd(next);
} else {
settings.gradientStart = next;
setGradientStart(next);
}
if (updateInput) setCustomInput(next);
}
requestApply();
};
const addCustomColor = () => {
const trimmed = customInput.trim();
if (
hexColorRegex.test(trimmed) &&
!colorPresets.includes(trimmed) &&
!customColors.includes(normalizeToRGB(trimmed))
) {
const updated = [...customColors, normalizeToRGB(trimmed)];
setCustomColors(updated);
settings.customColors = updated;
}
};
// const removeCustomColor = (color: string) => {
// const updated = customColors.filter((c) => c !== color);
// setCustomColors(updated);
// settings.customColors = updated;
// };
const allColors = [...colorPresets, ...customColors];
const requestApply = () => {
window.applyColoramaLyrics?.();
};
return (
<LunaSettings>
{/* Mode selection via dropdown (aligned right) */}
<div
style={{
padding: "8px 0",
display: "flex",
alignItems: "center",
gap: 12,
}}
>
<div style={{ display: "flex", flexDirection: "column" }}>
<div style={{ fontWeight: "normal", fontSize: "1.075rem" }}>Mode</div>
<div style={{ opacity: 0.7, fontSize: 14 }}>
Choose how lyrics are colored
</div>
</div>
<select
value={mode}
onChange={(e) => {
const next = e.target.value as ColoramaMode;
settings.mode = next;
setMode(next);
requestApply();
}}
style={{
padding: "6px 10px",
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.08)",
color: "#fff",
cursor: "pointer",
marginLeft: "auto",
minWidth: 180,
}}
>
<option value="single" style={{ color: "#000", background: "#fff" }}>
Single
</option>
<option
value="gradient-experimental"
style={{ color: "#000", background: "#fff" }}
>
Gradient - Experimental
</option>
<option value="cover" style={{ color: "#000", background: "#fff" }}>
Cover - Experimental
</option>
<option
value="cover-gradient"
style={{ color: "#000", background: "#fff" }}
>
Cover (Gradient) - Experimental
</option>
</select>
</div>
{/* Single color */}
<div
style={{
padding: "8px 0",
display: mode === "single" ? "flex" : "none",
justifyContent: "space-between",
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("single"))}
style={{
width: 32,
height: 32,
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: 6,
cursor: "pointer",
background: normalizeToRGB(singleColor),
}}
/>
</div>
</div>
{/* Gradient controls (open picker) */}
<div
style={{
padding: "8px 0",
display: mode === "gradient-experimental" ? "flex" : "none",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div>
<div
style={{
fontWeight: "normal",
fontSize: "1.075rem",
marginBottom: 4,
}}
>
Gradient (Experimental)
</div>
<div style={{ opacity: 0.7, fontSize: 14 }}>Set colors & angle</div>
</div>
<button
type="button"
onClick={() => {
setCustomInput(gradientStart);
openPicker("start");
}}
style={{
padding: "8px 12px",
borderRadius: 8,
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.08)",
color: "#fff",
cursor: "pointer",
}}
>
Configure
</button>
</div>
{/* Cover gradient controls (open picker for angle) */}
<div
style={{
padding: "8px 0",
display: mode === "cover-gradient" ? "flex" : "none",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div>
<div
style={{
fontWeight: "normal",
fontSize: "1.075rem",
marginBottom: 4,
}}
>
Cover (Gradient) - Experimental
</div>
<div style={{ opacity: 0.7, fontSize: 14 }}>Set angle</div>
</div>
<button
type="button"
onClick={() => openPicker("start")}
style={{
padding: "8px 12px",
borderRadius: 8,
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.08)",
color: "#fff",
cursor: "pointer",
}}
>
Configure
</button>
</div>
{/* Modal for picking and managing colors (reused) */}
{shouldRender && (
<>
<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,
}}
>
{mode === "single" ? "Single Color" : "Gradient Colors"}
</div>
{mode === "gradient-experimental" && (
<div
style={{
display: "flex",
gap: 8,
alignItems: "center",
marginBottom: 12,
}}
>
<div style={{ color: "rgba(255,255,255,0.7)", fontSize: 12 }}>
Editing
</div>
<button
onClick={() => {
setActiveEndpoint("start");
setCustomInput(gradientStart);
}}
style={{
display: "flex",
alignItems: "center",
gap: 8,
padding: "6px 10px",
borderRadius: 8,
border:
activeEndpoint === "start"
? "1px solid rgba(255,255,255,0.5)"
: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.05)",
color: "#fff",
cursor: "pointer",
}}
type="button"
>
<span
style={{
width: 14,
height: 14,
borderRadius: 3,
background: normalizeToRGB(gradientStart),
border: "1px solid rgba(255,255,255,0.3)",
}}
/>
<span style={{ fontSize: 12 }}>Start</span>
</button>
<button
type="button"
onClick={() => {
setActiveEndpoint("end");
setCustomInput(gradientEnd);
}}
style={{
display: "flex",
alignItems: "center",
gap: 8,
padding: "6px 10px",
borderRadius: 8,
border:
activeEndpoint === "end"
? "1px solid rgba(255,255,255,0.5)"
: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.05)",
color: "#fff",
cursor: "pointer",
}}
>
<span
style={{
width: 14,
height: 14,
borderRadius: 3,
background: normalizeToRGB(gradientEnd),
border: "1px solid rgba(255,255,255,0.3)",
}}
/>
<span style={{ fontSize: 12 }}>End</span>
</button>
</div>
)}
{mode !== "cover-gradient" && (
<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);
if (mode === "single") {
settings.singleColor = next;
setSingleColor(next);
} else if (mode === "gradient-experimental") {
if (activeEndpoint === "end") {
settings.gradientEnd = next;
setGradientEnd(next);
} else {
settings.gradientStart = next;
setGradientStart(next);
}
}
setCustomInput(next);
requestApply();
}}
style={{
width: 32,
height: 32,
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.2)",
background: normalizeToRGB(color),
cursor: "pointer",
}}
/>
))}
</div>
)}
{mode !== "cover-gradient" && (
<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>
)}
{/* Sliders inside picker based on mode */}
{mode === "single" && (
<div style={{ marginBottom: 16 }}>
<div
style={{
color: "rgba(255,255,255,0.8)",
fontSize: 12,
marginBottom: 6,
}}
>
Alpha
</div>
<input
type="range"
min={0}
max={100}
step={1}
value={singleAlpha}
onChange={(e) => {
const value = Number(e.target.value);
settings.singleAlpha = value;
setSingleAlpha(value);
requestApply();
}}
style={{ width: "100%" }}
/>
</div>
)}
{mode === "gradient-experimental" && (
<div style={{ marginBottom: 16, display: "grid", gap: 16 }}>
<div>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
marginBottom: 6,
}}
>
<div
style={{
width: 12,
height: 12,
borderRadius: 3,
background: normalizeToRGB(gradientStart),
border: "1px solid rgba(255,255,255,0.3)",
}}
/>
<div
style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}
>
Start Alpha
</div>
</div>
<input
type="range"
min={0}
max={100}
step={1}
value={gradientStartAlpha}
onChange={(e) => {
const value = Number(e.target.value);
settings.gradientStartAlpha = value;
setGradientStartAlpha(value);
requestApply();
}}
style={{ width: "100%" }}
/>
</div>
<div>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
marginBottom: 6,
}}
>
<div
style={{
width: 12,
height: 12,
borderRadius: 3,
background: normalizeToRGB(gradientEnd),
border: "1px solid rgba(255,255,255,0.3)",
}}
/>
<div
style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}
>
End Alpha
</div>
</div>
<input
type="range"
min={0}
max={100}
step={1}
value={gradientEndAlpha}
onChange={(e) => {
const value = Number(e.target.value);
settings.gradientEndAlpha = value;
setGradientEndAlpha(value);
requestApply();
}}
style={{ width: "100%" }}
/>
</div>
<div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 6,
}}
>
<div
style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}
>
Angle
</div>
<div
style={{ color: "rgba(255,255,255,0.6)", fontSize: 12 }}
>
{gradientAngle}°
</div>
</div>
<input
type="range"
min={0}
max={360}
step={1}
value={gradientAngle}
onChange={(e) => {
const value = Number(e.target.value);
settings.gradientAngle = value;
setGradientAngle(value);
requestApply();
}}
style={{ width: "100%" }}
/>
</div>
</div>
)}
{mode === "cover-gradient" && (
<div style={{ marginBottom: 16 }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 6,
}}
>
<div style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}>
Angle
</div>
<div style={{ color: "rgba(255,255,255,0.6)", fontSize: 12 }}>
{gradientAngle}°
</div>
</div>
<input
type="range"
min={0}
max={360}
step={1}
value={gradientAngle}
onChange={(e) => {
const value = Number(e.target.value);
settings.gradientAngle = value;
setGradientAngle(value);
requestApply();
}}
style={{ width: "100%" }}
/>
</div>
)}
<button
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/gradient only to the currently active lyric line"
checked={excludeInactive}
onChange={(_event: React.ChangeEvent<HTMLInputElement> | null, checked: boolean) => {
settings.excludeInactive = checked;
setExcludeInactive(checked);
requestApply();
}}
/>
</LunaSettings>
);
};
-235
View File
@@ -1,235 +0,0 @@
import { LunaUnload, Tracer } from "@luna/core";
import { StyleTag, PlayState } 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);
// Simple dominant color extraction from current cover art
async function getCoverArtElement(): Promise<HTMLImageElement | null> {
const img = document.querySelector(
'figure[class*="_albumImage"] > div > div > div > img',
) as HTMLImageElement | null;
if (img) return img;
const video = document.querySelector(
'figure[class*="_albumImage"] > div > div > div > video',
) as HTMLVideoElement | null;
if (video) {
const poster = video.getAttribute("poster");
if (!poster) return null;
const tempImg = new Image();
tempImg.crossOrigin = "anonymous";
tempImg.src = poster;
await new Promise<void>((resolve) => {
tempImg.onload = () => resolve();
tempImg.onerror = () => resolve();
});
return tempImg as unknown as HTMLImageElement;
}
return null;
}
function getDominantColorsFromImage(
img: HTMLImageElement,
count: number = 2,
): string[] {
try {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) return ["#ffffff", "#88aaff"]; // fallback
const w = 64;
const h = 64;
canvas.width = w;
canvas.height = h;
ctx.drawImage(img, 0, 0, w, h);
const data = ctx.getImageData(0, 0, w, h).data;
// Simple k-means-ish binning into 16 buckets per channel
const buckets = new Map<string, number>();
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const key = `${Math.round(r / 16)},${Math.round(g / 16)},${Math.round(b / 16)}`;
buckets.set(key, (buckets.get(key) ?? 0) + 1);
}
const sorted = [...buckets.entries()].sort((a, b) => b[1] - a[1]);
const picked = sorted.slice(0, Math.max(1, count)).map(([key]) => {
const [r, g, b] = key.split(",").map((v) => parseInt(v, 10) * 16);
return `#${[r, g, b].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")}`;
});
return picked;
} catch {
return ["#ffffff", "#88aaff"]; // fallback
}
}
// build rgba() from hex + alpha percentage
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
let v = hex.trim();
if (!v.startsWith("#")) v = `#${v}`;
if (/^#([0-9a-fA-F]{3})$/.test(v)) {
const r = parseInt(v[1] + v[1], 16);
const g = parseInt(v[2] + v[2], 16);
const b = parseInt(v[3] + v[3], 16);
return { r, g, b };
}
if (/^#([0-9a-fA-F]{6})$/.test(v)) {
const r = parseInt(v.slice(1, 3), 16);
const g = parseInt(v.slice(3, 5), 16);
const b = parseInt(v.slice(5, 7), 16);
return { r, g, b };
}
// 8-digit hex expects #AARRGGBB. Indices 1-3 are the alpha byte (ignored here),
// so r/g/b are extracted from v.slice(3,5), v.slice(5,7), v.slice(7,9) respectively.
if (/^#([0-9a-fA-F]{8})$/.test(v)) {
const r = parseInt(v.slice(3, 5), 16);
const g = parseInt(v.slice(5, 7), 16);
const b = parseInt(v.slice(7, 9), 16);
return { r, g, b };
}
return null;
}
function rgbaFromHexAndAlpha(
hex: string,
alphaPercent: number | undefined,
): string {
const rgb = hexToRgb(hex);
const a = Math.max(0.05, Math.min(100, alphaPercent ?? 100)) / 100;
if (!rgb) return `rgba(255,255,255,${a})`;
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`;
}
function applySingleColor(color: string) {
const alpha = (settings as any).singleAlpha ?? 100;
const rgba = rgbaFromHexAndAlpha(color, alpha);
document.documentElement.style.setProperty("--cl-lyrics-color", rgba);
document.documentElement.style.setProperty("--cl-glow1", rgba);
document.documentElement.style.setProperty("--cl-glow2", rgba);
document.documentElement.style.removeProperty("--cl-grad-start");
document.documentElement.style.removeProperty("--cl-grad-end");
document.documentElement.style.removeProperty("--cl-grad-angle");
document.body.classList.remove("colorama-gradient");
document.body.classList.add("colorama-single");
}
function applyGradient(start: string, end: string, angle: number) {
const startAlpha = (settings as any).gradientStartAlpha ?? 100;
const endAlpha = (settings as any).gradientEndAlpha ?? 100;
const startRgba = rgbaFromHexAndAlpha(start, startAlpha);
const endRgba = rgbaFromHexAndAlpha(end, endAlpha);
document.documentElement.style.setProperty("--cl-grad-start", startRgba);
document.documentElement.style.setProperty("--cl-grad-end", endRgba);
document.documentElement.style.setProperty("--cl-grad-angle", `${angle}deg`);
document.documentElement.style.setProperty("--cl-glow1", startRgba);
document.documentElement.style.setProperty("--cl-glow2", endRgba);
document.body.classList.remove("colorama-single");
document.body.classList.add("colorama-gradient");
}
function resetModeClasses(): void {
document.body.classList.remove("colorama-single", "colorama-gradient");
}
async function applyCoverColors(gradient: boolean) {
const img = await getCoverArtElement();
if (!img) return;
const colors = getDominantColorsFromImage(img, gradient ? 2 : 1);
if (gradient) {
const start = colors[0] ?? settings.gradientStart;
const end = colors[1] ?? settings.gradientEnd;
applyGradient(start, end, settings.gradientAngle);
} else {
const color = colors[0] ?? settings.singleColor;
applySingleColor(color);
}
}
function applyColoramaLyrics(): void {
if (!settings.enabled) {
document.body.classList.remove("colorama-single", "colorama-gradient");
return;
}
// Toggle only-active-line mode class
if (settings.excludeInactive) {
document.body.classList.add("colorama-only-active");
} else {
document.body.classList.remove("colorama-only-active");
}
resetModeClasses();
switch (settings.mode) {
case "single":
applySingleColor(settings.singleColor);
break;
case "gradient-experimental":
applyGradient(
settings.gradientStart,
settings.gradientEnd,
settings.gradientAngle,
);
break;
case "cover":
applyCoverColors(false);
break;
case "cover-gradient":
applyCoverColors(true);
break;
}
}
(window as any).applyColoramaLyrics = applyColoramaLyrics;
// Re-apply on track changes (for auto modes)
function observeTrackChanges(): void {
let lastTrackId: string | null = null;
const check = () => {
const currentTrackId = PlayState.playbackContext?.actualProductId;
if (currentTrackId && currentTrackId !== lastTrackId) {
lastTrackId = currentTrackId;
if (settings.mode === "cover" || settings.mode === "cover-gradient") {
setTimeout(() => applyColoramaLyrics(), 200);
}
}
};
const interval = setInterval(check, 500);
unloads.add(() => clearInterval(interval));
check();
}
// Initial apply and observers
setTimeout(() => applyColoramaLyrics(), 200);
observeTrackChanges();
// for some reason, re-apply after Radiant updates its styles/backgrounds
function hookRadiantUpdates(): void {
const w = window as any;
const wrap = (name: string) => {
const fn = w[name];
if (typeof fn === "function" && !fn.__coloramaPatched) {
const orig = fn.bind(w);
const patched = (...args: unknown[]) => {
const result = orig(...args);
try {
applyColoramaLyrics();
} catch {}
return result;
};
(patched as any).__coloramaPatched = true;
w[name] = patched;
}
};
wrap("updateRadiantLyricsStyles");
wrap("updateRadiantLyricsNowPlayingBackground");
wrap("updateRadiantLyricsGlobalBackground");
wrap("updateRadiantLyricsTextGlow");
}
setTimeout(() => hookRadiantUpdates(), 0);
-230
View File
@@ -1,230 +0,0 @@
/* Variables used by Colorama Lyrics */
:root {
--cl-lyrics-color: #ffffff;
--cl-grad-start: #ffffff;
--cl-grad-end: #88aaff;
--cl-grad-angle: 0deg;
--cl-glow1: #ffffff;
--cl-glow2: #ffffff;
}
/* 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;
}
/* Apply gradient to lyrics text */
.colorama-gradient [class*="_lyricsText"] > div > span,
.colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-gradient [class^="_lyricsContainer"] > div > div > span,
.colorama-gradient
[class^="_lyricsContainer"]
> div
> div
> span[data-current="true"] {
background: linear-gradient(
var(--cl-grad-angle),
var(--cl-grad-start),
var(--cl-grad-end)
) !important;
-webkit-background-clip: text !important;
background-clip: text !important;
color: transparent !important;
-webkit-text-fill-color: transparent !important;
}
/* Only-active: apply container class only on the active line via JS */
/* Slight emphasis on current line (uniform to single mode) */
.colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-gradient
[class^="_lyricsContainer"]
> div
> div
> span[data-current="true"] {
filter: brightness(1.1) !important;
}
/* Keep song title color unchanged; its glow is controlled in Radiant CSS */
/* Color Radiant glow shadows using Colorama colors (respect RL sizes) */
.colorama-single [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-single
[class^="_lyricsContainer"]
> div
> div
> span[data-current="true"],
.colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-gradient
[class^="_lyricsContainer"]
> div
> div
> span[data-current="true"],
.colorama-gradient
[class^="_lyricsContainer"]
> div
> div
> span[data-current="true"] {
text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
}
/* 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;
}
.colorama-gradient [class*="_lyricsText"] > div > span:hover,
.colorama-gradient [class^="_lyricsContainer"] > div > div > span:hover {
background: linear-gradient(
var(--cl-grad-angle),
var(--cl-grad-start),
var(--cl-grad-end)
) !important;
-webkit-background-clip: text !important;
background-clip: text !important;
color: transparent !important;
-webkit-text-fill-color: transparent !important;
/* Do not increase glow strength on hover for gradients */
}
/* MARKER: Radiant WBW Lyrics Support */
/* 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;
}
/* Gradient: active wbw words */
.colorama-gradient .rl-wbw-word.rl-wbw-active {
background: linear-gradient(
var(--cl-grad-angle),
var(--cl-grad-start),
var(--cl-grad-end)
) !important;
-webkit-background-clip: text !important;
background-clip: text !important;
color: transparent !important;
-webkit-text-fill-color: transparent !important;
}
/* Gradient: syllable finished (solid color — gradient conflicts with sweep animation) */
.colorama-gradient .rl-wbw-word.rl-syl-finished {
color: var(--cl-glow1, #ffffff) !important;
}
/* Gradient: active wbw word glow */
.colorama-gradient .rl-wbw-word.rl-wbw-active {
text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
}
/* Hover: wbw words pick up Colorama colors */
.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;
}
.colorama-gradient .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover,
.colorama-gradient .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word.rl-wbw-word-hover {
background: linear-gradient(
var(--cl-grad-angle),
var(--cl-grad-start),
var(--cl-grad-end)
) !important;
-webkit-background-clip: text !important;
background-clip: text !important;
color: transparent !important;
-webkit-text-fill-color: transparent !important;
}
/* Only-active: wbw words on inactive lines stay default */
body.colorama-only-active.colorama-single
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word,
body.colorama-only-active.colorama-gradient
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word {
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,
body.colorama-only-active.colorama-gradient
.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"]),
body.colorama-only-active.colorama-gradient
[class*="_lyricsText"]
> div
> span:not([data-current="true"]) {
/* Match Radiant inactive styling */
color: rgba(128, 128, 128, 0.4) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
}
/* In only-active mode, keep TIDAL defaults even on hover for inactive lines */
body.colorama-only-active.colorama-single [class*="_lyricsText"]
> div
> span:not([data-current="true"]):hover,
body.colorama-only-active.colorama-gradient
[class*="_lyricsText"]
> div
> span:not([data-current="true"]):hover {
color: lightgray !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
}
+82 -107
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 { StyleTag } from "@luna/lib";
// Import CSS directly using Luna's file:// syntax - Took me a while to figure out <3 // Import CSS directly using Luna's file:// syntax - Took me a while to figure out <3
@@ -9,135 +9,110 @@ export const { trace } = Tracer("[Copy Lyrics]");
// clean up resources // clean up resources
export const unloads = new Set<LunaUnload>(); export const unloads = new Set<LunaUnload>();
// Style injection via side effect // StyleTag for lyrics selection styling
new StyleTag("Copy-Lyrics", unloads, unlockSelection); const lyricsStyleTag = new StyleTag("Copy-Lyrics", unloads, unlockSelection);
function SetClipboard(text: string): void { function SetClipboard(text: string): void {
const textarea = document.createElement("textarea"); const textarea = document.createElement("textarea");
textarea.value = text; textarea.value = text;
textarea.style.position = "fixed"; // Avoid scrolling to bottom textarea.style.position = "fixed"; // Avoid scrolling to bottom
document.body.appendChild(textarea); document.body.appendChild(textarea);
textarea.select(); textarea.select();
try { try {
const success = document.execCommand("copy"); const success = document.execCommand("copy");
if (!success) throw new Error("Failed to copy text."); if (!success) throw new Error("Failed to copy text.");
} catch (err) { } catch (err) {
trace.msg.err(err instanceof Error ? err.message : String(err)); trace.msg.err(err instanceof Error ? err.message : String(err));
} finally { } finally {
document.body.removeChild(textarea); document.body.removeChild(textarea);
} }
} }
let isSelecting = false; let isSelecting = false;
const onMouseDown = (): void => { const onMouseDown = function (): void {
isSelecting = true; isSelecting = true;
}; };
const onMouseUp = (): void => { const onMouseUp = function (event: MouseEvent): void {
if (isSelecting) { if (isSelecting) {
const selection = window.getSelection(); const selection = window.getSelection();
if (selection?.toString().length > 0) { if (selection && selection.toString().length > 0) {
const selectedSpans: HTMLSpanElement[] = []; const selectedSpans: HTMLSpanElement[] = [];
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
let container: Node | null = range.commonAncestorContainer; let container = range.commonAncestorContainer;
// Normalize container: if it's a text node, use its parent element/node // If the container is NOT an element and a document, adjust it.
if (container && container.nodeType === Node.TEXT_NODE) { if (
container = (container.parentElement ?? container.parentNode) as Node | null; container.nodeType !== Node.ELEMENT_NODE &&
} container.nodeType !== Node.DOCUMENT_NODE
) {
// Get the parent element if it's a text node
const parentElement = container.parentElement;
if (parentElement && parentElement.hasAttribute("data-current")) {
let text_ = selection.toString().trim();
SetClipboard(text_);
trace.msg.log("Copied to clipboard!");
return;
}
}
// If parent has data-current, treat as single-line copy case // Get all the spans inside the container.
if ( const spans = (container as Element).getElementsByTagName("span");
container && for (let span of spans) {
container.nodeType === Node.ELEMENT_NODE && if (selection.containsNode(span, true)) {
(container as Element).hasAttribute("data-current") selectedSpans.push(span as HTMLSpanElement);
) { }
const text_ = selection.toString().trim(); }
SetClipboard(text_);
trace.msg.log("Copied to clipboard!");
return;
}
// Ensure we have an Element or Document before querying // Concat the text of the selected spans.
if ( let hasCorrectAttribute = false;
!container || let text = "";
(container.nodeType !== Node.ELEMENT_NODE && selectedSpans.forEach((span) => {
container.nodeType !== Node.DOCUMENT_NODE) if (span.hasAttribute("data-current")) {
) { hasCorrectAttribute = true;
isSelecting = false; text += span.textContent + "\n";
return; if ([...span.classList].some((className) => className.startsWith("endOfStanza--"))) {
} text += "\n";
}
}
});
// Get all the spans inside the container. text = text.trim();
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. if (hasCorrectAttribute) {
let hasCorrectAttribute = false; SetClipboard(text);
let text = ""; trace.msg.log("Copied to clipboard!");
selectedSpans.forEach((span) => { selection.removeAllRanges();
if (span.hasAttribute("data-current")) { }
hasCorrectAttribute = true; }
text += span.textContent + "\n"; isSelecting = false;
if ( }
[...span.classList].some((className) =>
className.startsWith("endOfStanza--"),
)
) {
text += "\n";
}
}
});
text = text.trim();
if (hasCorrectAttribute) {
SetClipboard(text);
trace.msg.log("Copied to clipboard!");
selection.removeAllRanges();
}
}
isSelecting = false;
}
}; };
const onClickHooked = (event: MouseEvent): boolean | undefined => { const onClickHooked = function (event: MouseEvent): boolean | void {
if (!isSelecting) return; if (!isSelecting) return;
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
if ( if (target.tagName.toLowerCase() === "span" && target.hasAttribute("data-current")) {
target.tagName.toLowerCase() === "span" && // Prevent default behavior and stop event propagation
target.hasAttribute("data-current") event.preventDefault();
) { event.stopPropagation();
// Prevent default behavior and stop event propagation event.stopImmediatePropagation();
event.preventDefault(); return false;
event.stopPropagation(); }
event.stopImmediatePropagation();
return false;
}
return undefined;
}; };
// Add event listener with capture phase to intercept events before they reach other handlers // Add event listener with capture phase to intercept events before they reach other handlers
document.addEventListener("click", onClickHooked, true); document.addEventListener("click", onClickHooked, true);
document.addEventListener("mousedown", onMouseDown); document.addEventListener("mousedown", onMouseDown);
document.addEventListener("mouseup", onMouseUp); document.addEventListener("mouseup", onMouseUp);
// Add cleanup to unloads // Add cleanup to unloads
unloads.add((): void => { unloads.add(() => {
// Remove event listeners // Remove event listeners
document.removeEventListener("click", onClickHooked, true); document.removeEventListener("click", onClickHooked, true);
document.removeEventListener("mousedown", onMouseDown); document.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mouseup", onMouseUp); document.removeEventListener("mouseup", onMouseUp);
}); });
+5 -5
View File
@@ -1,9 +1,9 @@
[class^="_lyricsText"] > div > span { [class^="_lyricsText"]>div>span {
user-select: text; user-select: text;
cursor: text; cursor: text;
} }
::selection { ::selection {
background: rgb(72, 0, 60); background: rgb(72, 0, 60);
color: rgb(255, 255, 255); color: rgb(255, 255, 255);
} }
+1 -1
View File
@@ -9,7 +9,7 @@ export const settings = await ReactiveStore.getPluginStorage("ElementHider", {
className: string; className: string;
textContent: string; textContent: string;
timestamp: number; timestamp: number;
}>, }>
}); });
export const Settings = () => { export const Settings = () => {
+126 -193
View File
@@ -1,5 +1,5 @@
import { type LunaUnload, Tracer } from "@luna/core"; import { LunaUnload, Tracer } from "@luna/core";
import { StyleTag } from "@luna/lib"; import { StyleTag, ContextMenu } from "@luna/lib";
import { settings, Settings } from "./Settings"; import { settings, Settings } from "./Settings";
// Import CSS directly using Luna's file:// syntax // Import CSS directly using Luna's file:// syntax
@@ -13,8 +13,8 @@ export { Settings };
// Clean up resources // Clean up resources
export const unloads = new Set<LunaUnload>(); export const unloads = new Set<LunaUnload>();
// StyleTag for element hider (side-effect) // StyleTag for element hider
new StyleTag("Element-Hider", unloads, styles); const styleTag = new StyleTag("Element-Hider", unloads, styles);
// State management // State management
let targetElement: HTMLElement | null = null; let targetElement: HTMLElement | null = null;
@@ -32,7 +32,7 @@ function generateElementSelector(element: HTMLElement): string {
} }
// Priority 2: data-test attribute (very specific for Tidal <3) // Priority 2: data-test attribute (very specific for Tidal <3)
const dataTest = element.getAttribute("data-test"); const dataTest = element.getAttribute('data-test');
if (dataTest) { if (dataTest) {
return `[data-test="${dataTest}"]`; return `[data-test="${dataTest}"]`;
} }
@@ -41,43 +41,28 @@ function generateElementSelector(element: HTMLElement): string {
let selector = element.tagName.toLowerCase(); let selector = element.tagName.toLowerCase();
// Get filtered classes (exclude our temporary classes) // Get filtered classes (exclude our temporary classes)
const classes = element.className const classes = element.className ? element.className.trim().split(/\s+/).filter(cls => {
? element.className return cls.length > 0 &&
.trim() !cls.startsWith('element-hider-') &&
.split(/\s+/) cls !== 'element-hider-target' &&
.filter((cls) => { cls !== 'element-hider-hiding' &&
return ( cls !== 'element-hider-hidden';
cls.length > 0 && }) : [];
!cls.startsWith("element-hider-") &&
cls !== "element-hider-target" &&
cls !== "element-hider-hiding" &&
cls !== "element-hider-hidden"
);
})
: [];
// Only use classes if we have them and they're not generic and dumb // Only use classes if we have them and they're not generic and dumb
if (classes.length > 0) { if (classes.length > 0) {
// Use ALL classes to be very specific // Use ALL classes to be very specific
selector += "." + classes.join("."); selector += '.' + classes.join('.');
// Add parent context for extra specificity (for when the element is inside another element) // Add parent context for extra specificity (for when the element is inside another element)
const parent = element.parentElement; const parent = element.parentElement;
if (parent && parent.tagName !== "BODY" && parent.tagName !== "HTML") { if (parent && parent.tagName !== 'BODY' && parent.tagName !== 'HTML') {
const parentClasses = parent.className const parentClasses = parent.className ? parent.className.trim().split(/\s+/).filter(cls => {
? parent.className return cls.length > 0 && !cls.startsWith('element-hider-');
.trim() }) : [];
.split(/\s+/)
.filter((cls) => {
return cls.length > 0 && !cls.startsWith("element-hider-");
})
: [];
if (parentClasses.length > 0) { if (parentClasses.length > 0) {
const parentSelector = const parentSelector = parent.tagName.toLowerCase() + '.' + parentClasses.slice(0, 2).join('.');
parent.tagName.toLowerCase() +
"." +
parentClasses.slice(0, 2).join(".");
selector = `${parentSelector} > ${selector}`; selector = `${parentSelector} > ${selector}`;
} }
} }
@@ -85,29 +70,19 @@ function generateElementSelector(element: HTMLElement): string {
// If no useful classes, use position-based selector with parent context // If no useful classes, use position-based selector with parent context
const parent = element.parentElement; const parent = element.parentElement;
if (parent) { if (parent) {
const siblings = Array.from(parent.children).filter( const siblings = Array.from(parent.children).filter(child => child.tagName === element.tagName);
(child) => child.tagName === element.tagName,
);
const index = siblings.indexOf(element); const index = siblings.indexOf(element);
if (index >= 0) { if (index >= 0) {
selector += `:nth-of-type(${index + 1})`; selector += `:nth-of-type(${index + 1})`;
// Add parent context // Add parent context
if (parent.tagName !== "BODY" && parent.tagName !== "HTML") { if (parent.tagName !== 'BODY' && parent.tagName !== 'HTML') {
const parentClasses = parent.className const parentClasses = parent.className ? parent.className.trim().split(/\s+/).filter(cls => {
? parent.className return cls.length > 0 && !cls.startsWith('element-hider-');
.trim() }) : [];
.split(/\s+/)
.filter((cls) => {
return cls.length > 0 && !cls.startsWith("element-hider-");
})
: [];
if (parentClasses.length > 0) { if (parentClasses.length > 0) {
const parentSelector = const parentSelector = parent.tagName.toLowerCase() + '.' + parentClasses.slice(0, 2).join('.');
parent.tagName.toLowerCase() +
"." +
parentClasses.slice(0, 2).join(".");
selector = `${parentSelector} > ${selector}`; selector = `${parentSelector} > ${selector}`;
} }
} }
@@ -125,14 +100,14 @@ function saveHiddenElement(element: HTMLElement): void {
const elementInfo = { const elementInfo = {
selector: selector, selector: selector,
tagName: element.tagName, tagName: element.tagName,
className: element.className || "", className: element.className || '',
textContent: element.textContent?.substring(0, 100) || "", textContent: element.textContent?.substring(0, 100) || '',
timestamp: Date.now(), timestamp: Date.now()
}; };
// Check if element is already saved // Check if element is already saved
const existingIndex = settings.hiddenElements.findIndex( const existingIndex = settings.hiddenElements.findIndex(
(stored) => stored.selector === elementInfo.selector, stored => stored.selector === elementInfo.selector
); );
if (existingIndex === -1) { if (existingIndex === -1) {
@@ -144,18 +119,17 @@ function saveHiddenElement(element: HTMLElement): void {
} }
} }
// Remove hidden element from persistent storage (for unhiding) - currently unused // Remove hidden element from persistent storage (for unhiding)
// function removeSavedElement(element: HTMLElement): void { function removeSavedElement(element: HTMLElement): void {
// const selector = generateElementSelector(element); const selector = generateElementSelector(element);
// const index = settings.hiddenElements.findIndex( const index = settings.hiddenElements.findIndex(stored => stored.selector === selector);
// (stored) => stored.selector === selector,
// ); if (index !== -1) {
// if (index !== -1) { settings.hiddenElements.splice(index, 1);
// settings.hiddenElements.splice(index, 1); trace.log(`Permanently removed: ${selector}`);
// trace.log(`Permanently removed: ${selector}`); trace.log(`Remaining stored: ${settings.hiddenElements.length}`);
// trace.log(`Remaining stored: ${settings.hiddenElements.length}`); }
// } }
// }
// Check if an element matches any stored selector (EXACT match only) // Check if an element matches any stored selector (EXACT match only)
function matchesStoredSelector(element: HTMLElement): boolean { function matchesStoredSelector(element: HTMLElement): boolean {
@@ -180,18 +154,14 @@ function hideElementDirectly(element: HTMLElement): void {
element.classList.add("element-hider-hidden"); element.classList.add("element-hider-hidden");
hiddenElements.add(element); hiddenElements.add(element);
hiddenElementsArray.push(element); hiddenElementsArray.push(element);
trace.log( trace.log(`Hidden element: ${element.tagName}${element.className ? '.' + element.className.split(' ')[0] : ''}`);
`Hidden element: ${element.tagName}${element.className ? "." + element.className.split(" ")[0] : ""}`,
);
} }
// Hide the target element with animation // Hide the target element with animation
function hideTargetElement(): void { function hideTargetElement(): void {
if (!targetElement) return; if (!targetElement) return;
trace.log( trace.log(`Hiding with animation: ${targetElement.tagName}${targetElement.className ? '.' + targetElement.className.split(' ')[0] : ''}`);
`Hiding with animation: ${targetElement.tagName}${targetElement.className ? "." + targetElement.className.split(" ")[0] : ""}`,
);
// Add hiding animation class // Add hiding animation class
targetElement.classList.add("element-hider-hiding"); targetElement.classList.add("element-hider-hiding");
@@ -205,10 +175,7 @@ function hideTargetElement(): void {
// Wait for animation to complete, then hide // Wait for animation to complete, then hide
setTimeout(() => { setTimeout(() => {
elementToHide.classList.add("element-hider-hidden"); elementToHide.classList.add("element-hider-hidden");
elementToHide.classList.remove( elementToHide.classList.remove("element-hider-hiding", "element-hider-target");
"element-hider-hiding",
"element-hider-target",
);
hiddenElements.add(elementToHide); hiddenElements.add(elementToHide);
hiddenElementsArray.push(elementToHide); hiddenElementsArray.push(elementToHide);
}, 300); }, 300);
@@ -219,12 +186,10 @@ function hideTargetElement(): void {
// Unhide all elements permanently (remove from storage) // Unhide all elements permanently (remove from storage)
function unhideAllElements(): void { function unhideAllElements(): void {
trace.log( trace.log(`Permanently unhiding ${settings.hiddenElements.length} saved elements`);
`Permanently unhiding ${settings.hiddenElements.length} saved elements`,
);
// Show all currently hidden elements // Show all currently hidden elements
hiddenElementsArray.forEach((element) => { hiddenElementsArray.forEach(element => {
if (document.body.contains(element)) { if (document.body.contains(element)) {
element.classList.remove("element-hider-hidden", "element-hider-hiding"); element.classList.remove("element-hider-hidden", "element-hider-hiding");
} }
@@ -240,9 +205,7 @@ function unhideAllElements(): void {
function processAllElements(): void { function processAllElements(): void {
if (settings.hiddenElements.length === 0) return; if (settings.hiddenElements.length === 0) return;
trace.log( trace.log(`Scanning document for ${settings.hiddenElements.length} stored selectors`);
`Scanning document for ${settings.hiddenElements.length} stored selectors`,
);
let hiddenCount = 0; let hiddenCount = 0;
// Use querySelectorAll for each stored selector with validation // Use querySelectorAll for each stored selector with validation
@@ -254,9 +217,7 @@ function processAllElements(): void {
// Limit to prevent over-hiding (safety check) // Limit to prevent over-hiding (safety check)
if (elements.length > 10) { if (elements.length > 10) {
trace.warn( trace.warn(`Selector too broad (${elements.length} matches), skipping: ${storedElement.selector}`);
`Selector too broad (${elements.length} matches), skipping: ${storedElement.selector}`,
);
return; return;
} }
@@ -265,9 +226,7 @@ function processAllElements(): void {
if (!hiddenElements.has(htmlElement)) { if (!hiddenElements.has(htmlElement)) {
hideElementDirectly(htmlElement); hideElementDirectly(htmlElement);
hiddenCount++; hiddenCount++;
trace.log( trace.log(`Hid element ${elemIndex + 1}/${elements.length} for selector ${index + 1}`);
`Hid element ${elemIndex + 1}/${elements.length} for selector ${index + 1}`,
);
} }
}); });
} catch (error) { } catch (error) {
@@ -282,7 +241,7 @@ function processAllElements(): void {
// Process new elements that are added to the DOM // Process new elements that are added to the DOM
function processNewElements(addedNodes: NodeList): void { function processNewElements(addedNodes: NodeList): void {
addedNodes.forEach((node) => { addedNodes.forEach(node => {
if (node.nodeType !== Node.ELEMENT_NODE) return; if (node.nodeType !== Node.ELEMENT_NODE) return;
const element = node as HTMLElement; const element = node as HTMLElement;
@@ -293,8 +252,8 @@ function processNewElements(addedNodes: NodeList): void {
} }
// Check all descendant elements // Check all descendant elements
const descendants = element.querySelectorAll("*"); const descendants = element.querySelectorAll('*');
descendants.forEach((descendant) => { descendants.forEach(descendant => {
if (matchesStoredSelector(descendant as HTMLElement)) { if (matchesStoredSelector(descendant as HTMLElement)) {
hideElementDirectly(descendant as HTMLElement); hideElementDirectly(descendant as HTMLElement);
} }
@@ -308,7 +267,7 @@ function setupElementObserver(): void {
elementObserver = new MutationObserver((mutations) => { elementObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
processNewElements(mutation.addedNodes); processNewElements(mutation.addedNodes);
} }
}); });
@@ -316,22 +275,15 @@ function setupElementObserver(): void {
elementObserver.observe(document.body, { elementObserver.observe(document.body, {
childList: true, childList: true,
subtree: true, subtree: true
}); });
trace.log(`Set up reactive element observer`); trace.log(`Set up reactive element observer`);
} }
// Global functions // Global functions
declare global { (window as any).showAllElementsFromSettings = unhideAllElements;
interface Window { (window as any).debugElementHider = () => {
showAllElementsFromSettings?: () => void;
debugElementHider?: () => void;
}
}
window.showAllElementsFromSettings = unhideAllElements;
window.debugElementHider = () => {
trace.log(`=== Element Hider Debug Info ===`); trace.log(`=== Element Hider Debug Info ===`);
trace.log(`Stored elements: ${settings.hiddenElements.length}`); trace.log(`Stored elements: ${settings.hiddenElements.length}`);
trace.log(`Currently hidden elements: ${hiddenElementsArray.length}`); trace.log(`Currently hidden elements: ${hiddenElementsArray.length}`);
@@ -345,19 +297,19 @@ window.debugElementHider = () => {
// Handle highlighting target element // Handle highlighting target element
function highlightElement(element: HTMLElement): void { function highlightElement(element: HTMLElement): void {
// Remove previous highlights // Remove previous highlights
document.querySelectorAll(".element-hider-target").forEach((el) => { document.querySelectorAll('.element-hider-target').forEach(el => {
el.classList.remove("element-hider-target"); el.classList.remove('element-hider-target');
}); });
// Highlight current element // Highlight current element
element.classList.add("element-hider-target"); element.classList.add('element-hider-target');
targetElement = element; targetElement = element;
} }
// Remove highlight // Remove highlight
function removeHighlight(): void { function removeHighlight(): void {
if (targetElement) { if (targetElement) {
targetElement.classList.remove("element-hider-target"); targetElement.classList.remove('element-hider-target');
targetElement = null; targetElement = null;
} }
} }
@@ -369,70 +321,59 @@ let contextMenuTimeout: number | null = null;
let waitingForBuiltInMenu = false; let waitingForBuiltInMenu = false;
// Listen for right-click events to capture the target for context menu // Listen for right-click events to capture the target for context menu
document.addEventListener( document.addEventListener('contextmenu', (event: MouseEvent) => {
"contextmenu", const target = event.target as HTMLElement;
(event: MouseEvent) => {
const target = event.target as HTMLElement;
// Don't interfere with native context menus on inputs, textareas, etc. // Don't interfere with native context menus on inputs, textareas, etc.
if ( if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
target.tagName === "INPUT" || currentContextElement = null;
target.tagName === "TEXTAREA" || return;
target.isContentEditable }
) {
currentContextElement = null; // Don't show menu on our own custom menu
return; if (target.closest(".element-hider-custom-menu")) {
return;
}
// Close any existing custom menu
closeCustomMenu();
// Store the right-clicked element for context menu
currentContextElement = target;
waitingForBuiltInMenu = true;
// Store event coordinates for potential custom menu
const eventX = event.clientX;
const eventY = event.clientY;
// Prevent default immediately if we plan to handle it
event.preventDefault();
// Wait to see if the built-in context menu appears
contextMenuTimeout = window.setTimeout(() => {
// If we're still waiting and no built-in menu appeared, show our custom menu
if (waitingForBuiltInMenu && currentContextElement) {
showCustomMenu(eventX, eventY);
} }
waitingForBuiltInMenu = false;
}, 150); // Wait 150ms for built-in menu
// Don't show menu on our own custom menu // Don't prevent default initially - let Luna try to handle the context menu
if (target.closest(".element-hider-custom-menu")) { }, true);
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,
);
// Listen for clicks to close custom menu // Listen for clicks to close custom menu
document.addEventListener( document.addEventListener('click', (event: MouseEvent) => {
"click", const target = event.target as HTMLElement;
(event: MouseEvent) => {
const target = event.target as HTMLElement;
// If clicking outside our custom menu, close it // If clicking outside our custom menu, close it
if (customMenu && !target.closest(".element-hider-custom-menu")) { if (customMenu && !target.closest(".element-hider-custom-menu")) {
closeCustomMenu(); closeCustomMenu();
removeHighlight(); removeHighlight();
} }
}, }, true);
true,
);
// Handle escape key to close custom menu and remove highlights // Handle escape key to close custom menu and remove highlights
document.addEventListener("keydown", (event: KeyboardEvent) => { document.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === "Escape") { if (event.key === "Escape") {
if (customMenu) { if (customMenu) {
closeCustomMenu(); closeCustomMenu();
@@ -523,15 +464,8 @@ const contextMenuObserver = new MutationObserver((mutations) => {
const element = node as HTMLElement; const element = node as HTMLElement;
// Look for Tidal's context menu // Look for Tidal's context menu
if ( if (element.matches('[data-test="contextmenu"]') || element.querySelector('[data-test="contextmenu"]')) {
element.matches('[data-test="contextmenu"]') || const contextMenu = element.matches('[data-test="contextmenu"]') ? element : element.querySelector('[data-test="contextmenu"]') as HTMLElement;
element.querySelector('[data-test="contextmenu"]')
) {
const contextMenu = element.matches('[data-test="contextmenu"]')
? element
: (element.querySelector(
'[data-test="contextmenu"]',
) as HTMLElement);
if (contextMenu && currentContextElement && waitingForBuiltInMenu) { if (contextMenu && currentContextElement && waitingForBuiltInMenu) {
// Built-in menu appeared, cancel custom menu timeout // Built-in menu appeared, cancel custom menu timeout
@@ -551,8 +485,8 @@ const contextMenuObserver = new MutationObserver((mutations) => {
// Add our options to the existing context menu // Add our options to the existing context menu
function addElementHiderOptions(contextMenu: HTMLElement): void { function addElementHiderOptions(contextMenu: HTMLElement): void {
// Create hide element button // Create hide element button
const hideButton = document.createElement("button"); const hideButton = document.createElement('button');
hideButton.className = "element-hider-menu-item"; hideButton.className = 'element-hider-menu-item';
hideButton.style.cssText = ` hideButton.style.cssText = `
display: flex; display: flex;
align-items: center; align-items: center;
@@ -569,7 +503,7 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
`; `;
hideButton.innerHTML = `Hide This Element`; hideButton.innerHTML = `Hide This Element`;
hideButton.addEventListener("click", () => { hideButton.addEventListener('click', () => {
if (currentContextElement) { if (currentContextElement) {
targetElement = currentContextElement; targetElement = currentContextElement;
hideTargetElement(); hideTargetElement();
@@ -577,38 +511,37 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
}); });
// Add hover effects for highlighting // Add hover effects for highlighting
hideButton.addEventListener("mouseenter", () => { hideButton.addEventListener('mouseenter', () => {
hideButton.style.background = "var(--wave-color-background-hover, #3a3a3a)"; hideButton.style.background = 'var(--wave-color-background-hover, #3a3a3a)';
if (currentContextElement) { if (currentContextElement) {
highlightElement(currentContextElement); highlightElement(currentContextElement);
} }
}); });
hideButton.addEventListener("mouseleave", () => { hideButton.addEventListener('mouseleave', () => {
hideButton.style.background = "transparent"; hideButton.style.background = 'transparent';
removeHighlight(); removeHighlight();
}); });
// Create unhide all button // Create unhide all button
const unhideAllButton = document.createElement("button"); const unhideAllButton = document.createElement('button');
unhideAllButton.className = "element-hider-menu-item"; unhideAllButton.className = 'element-hider-menu-item';
unhideAllButton.style.cssText = hideButton.style.cssText; unhideAllButton.style.cssText = hideButton.style.cssText;
unhideAllButton.innerHTML = `Unhide All Elements (${hiddenElementsArray.length})`; unhideAllButton.innerHTML = `Unhide All Elements (${hiddenElementsArray.length})`;
unhideAllButton.addEventListener("click", unhideAllElements); unhideAllButton.addEventListener('click', unhideAllElements);
// Add hover effects for unhide all button // Add hover effects for unhide all button
unhideAllButton.addEventListener("mouseenter", () => { unhideAllButton.addEventListener('mouseenter', () => {
unhideAllButton.style.background = unhideAllButton.style.background = 'var(--wave-color-background-hover, #3a3a3a)';
"var(--wave-color-background-hover, #3a3a3a)";
}); });
unhideAllButton.addEventListener("mouseleave", () => { unhideAllButton.addEventListener('mouseleave', () => {
unhideAllButton.style.background = "transparent"; unhideAllButton.style.background = 'transparent';
}); });
// Add a separator if the menu has other items // Add a separator if the menu has other items
if (contextMenu.children.length > 0) { if (contextMenu.children.length > 0) {
const separator = document.createElement("div"); const separator = document.createElement('div');
separator.style.cssText = ` separator.style.cssText = `
height: 1px; height: 1px;
background: var(--wave-color-border, #444); background: var(--wave-color-border, #444);
@@ -625,10 +558,10 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
// Start observing for context menus // Start observing for context menus
contextMenuObserver.observe(document.body, { contextMenuObserver.observe(document.body, {
childList: true, childList: true,
subtree: true, subtree: true
}); });
// Initialize plugin // Initialize plugin
function initializePlugin() { function initializePlugin() {
trace.log("Initializing plugin..."); trace.log("Initializing plugin...");
@@ -645,8 +578,8 @@ function initializePlugin() {
} }
// Run initialization when DOM is ready // Run initialization when DOM is ready
if (document.readyState === "loading") { if (document.readyState === 'loading') {
document.addEventListener("DOMContentLoaded", initializePlugin); document.addEventListener('DOMContentLoaded', initializePlugin);
} else { } else {
initializePlugin(); initializePlugin();
} }
@@ -667,8 +600,8 @@ unloads.add(() => {
removeHighlight(); removeHighlight();
// Clean up global functions // Clean up global functions
window.showAllElementsFromSettings = undefined; (window as any).showAllElementsFromSettings = undefined;
window.debugElementHider = undefined; (window as any).debugElementHider = undefined;
trace.log("Plugin unloaded"); trace.log("Plugin unloaded");
}); });
+34 -36
View File
@@ -2,64 +2,62 @@
/* Custom context menu for elements without built-in menu */ /* Custom context menu for elements without built-in menu */
.element-hider-custom-menu { .element-hider-custom-menu {
position: fixed; position: fixed;
background: var(--wave-color-background-elevated, #2a2a2a); background: var(--wave-color-background-elevated, #2a2a2a);
border: 1px solid var(--wave-color-border, #444); border: 1px solid var(--wave-color-border, #444);
border-radius: 8px; border-radius: 8px;
padding: 8px 0; padding: 8px 0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 999999; z-index: 999999;
min-width: 180px; min-width: 180px;
font-family: inherit; font-family: inherit;
font-size: 14px; font-size: 14px;
} }
.element-hider-menu-item { .element-hider-menu-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 8px 16px; padding: 8px 16px;
cursor: pointer; cursor: pointer;
color: var(--wave-color-text, #ffffff); color: var(--wave-color-text, #ffffff);
background: transparent; background: transparent;
border: none; border: none;
width: 100%; width: 100%;
text-align: left; text-align: left;
transition: background-color 0.15s ease; transition: background-color 0.15s ease;
font-family: inherit; font-family: inherit;
font-size: 14px; font-size: 14px;
} }
.element-hider-menu-item:hover { .element-hider-menu-item:hover {
background: var(--wave-color-background-hover, #3a3a3a); background: var(--wave-color-background-hover, #3a3a3a);
} }
.element-hider-menu-item:active { .element-hider-menu-item:active {
background: var(--wave-color-background-active, #4a4a4a); background: var(--wave-color-background-active, #4a4a4a);
} }
.element-hider-menu-icon { .element-hider-menu-icon {
margin-right: 8px; margin-right: 8px;
width: 16px; width: 16px;
height: 16px; height: 16px;
} }
/* Highlight the target element */ /* Highlight the target element */
.element-hider-target { .element-hider-target {
outline: 2px solid #ff6b6b !important; outline: 2px solid #ff6b6b !important;
outline-offset: 2px !important; outline-offset: 2px !important;
box-shadow: 0 0 10px rgba(255, 107, 107, 0.6) !important; box-shadow: 0 0 10px rgba(255, 107, 107, 0.6) !important;
} }
/* Hidden elements */ /* Hidden elements */
.element-hider-hidden { .element-hider-hidden {
display: none !important; display: none !important;
} }
/* Animation for hiding */ /* Animation for hiding */
.element-hider-hiding { .element-hider-hiding {
transition: transition: opacity 0.3s ease, transform 0.3s ease;
opacity 0.3s ease, opacity: 0;
transform 0.3s ease; transform: scale(0.95);
opacity: 0;
transform: scale(0.95);
} }
@@ -1,6 +1,6 @@
{ {
"name": "@meowarex/colorama-lyrics", "name": "@meowarex/oled-theme",
"description": "Customize lyrics colors: single, gradient & auto from cover art", "description": "A dark OLED-friendly theme plugin that transforms Tidal Luna's appearance.",
"author": { "author": {
"name": "meowarex", "name": "meowarex",
"url": "https://github.com/meowarex", "url": "https://github.com/meowarex",
+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);
}
File diff suppressed because it is too large Load Diff
@@ -1,92 +1,105 @@
/* Global Spinning Background Styles - PERFORMANCE OPTIMIZED */ /* Global Spinning Background Styles - PERFORMANCE OPTIMIZED */
.global-background-container { .global-background-container {
position: fixed; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
z-index: -3; z-index: -3;
pointer-events: none; pointer-events: none;
overflow: hidden; overflow: hidden;
/* Hardware acceleration */ /* Hardware acceleration */
transform: translateZ(0); transform: translateZ(0);
backface-visibility: hidden; backface-visibility: hidden;
} }
.global-spinning-black-bg { .global-spinning-black-bg {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: #000; background: #000;
z-index: -2; z-index: -2;
pointer-events: none; pointer-events: none;
} }
.global-spinning-image { .global-spinning-image {
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 150vw; width: 150vw;
height: 150vh; height: 150vh;
object-fit: cover; object-fit: cover;
z-index: -1; z-index: -1;
filter: blur(80px) brightness(0.4) contrast(1.2) saturate(1); filter: blur(80px) brightness(0.4) contrast(1.2) saturate(1);
opacity: 1; opacity: 1;
animation: spinGlobal 45s linear infinite; animation: spinGlobal 45s linear infinite;
will-change: transform; will-change: transform;
/* Hardware acceleration */ /* Hardware acceleration */
transform-origin: center center; transform-origin: center center;
backface-visibility: hidden; backface-visibility: hidden;
}
/* Performance mode optimizations - keep spinning but optimize other aspects */
.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;
}
.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 Optimization */
.now-playing-background-container { .now-playing-background-container {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: -3; z-index: -3;
pointer-events: none; pointer-events: none;
overflow: hidden; overflow: hidden;
/* Hardware acceleration */ /* Hardware acceleration */
transform: translateZ(0); transform: translateZ(0);
backface-visibility: hidden; backface-visibility: hidden;
} }
/* Optimized keyframe animations with GPU acceleration */ /* Optimized keyframe animations with GPU acceleration */
@keyframes spinGlobal { @keyframes spinGlobal {
from { from {
transform: translate(-50%, -50%) rotate(0deg); transform: translate(-50%, -50%) rotate(0deg);
} }
to { to {
transform: translate(-50%, -50%) rotate(360deg); transform: translate(-50%, -50%) rotate(360deg);
} }
} }
/* Reduced motion for users who prefer it */ /* Reduced motion for users who prefer it */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.global-spinning-image, .global-spinning-image,
.now-playing-background-image { .now-playing-background-image {
/* biome-ignore lint: Accessibility override needs priority */ animation: none !important;
animation: none !important; transform: translate(-50%, -50%) !important;
/* biome-ignore lint: Accessibility override needs priority */ will-change: auto !important;
transform: translate(-50%, -50%) !important; }
/* biome-ignore lint: Accessibility override needs priority */
will-change: auto !important;
}
} }
/* Performance mode: optimize effects but keep spinning */ /* Performance mode: optimize effects but keep spinning */
.performance-mode .global-spinning-image, .performance-mode .global-spinning-image,
.performance-mode .now-playing-background-image { .performance-mode .now-playing-background-image {
/* Keep animations but optimize filter effects */ /* Keep animations but optimize filter effects */
/* biome-ignore lint: Intentional override of runtime styles */ filter: blur(10px) brightness(0.4) contrast(1.1) !important;
filter: blur(10px) brightness(0.4) contrast(1.1) !important;
} }
/* Make Notification Feed sidebar transparent */ /* Make Notification Feed sidebar transparent */
@@ -102,14 +115,13 @@ main,
[data-test="stream-metadata"], [data-test="stream-metadata"],
[data-test="footer-player"], [data-test="footer-player"],
/* Notification Feed sidebar specific container */ /* Notification Feed sidebar specific container */
[class^="_feedSidebarVStack"], [class^="_feedSidebarVStack"],
[class^="_feedSidebarSpacer"], [class^="_feedSidebarSpacer"],
[class^="_feedSidebarItem"], [class^="_feedSidebarItem"],
[class^="_feedSidebarItemDiv"], [class^="_feedSidebarItemDiv"],
[class^="_cellContainer"], [class^="_cellContainer"],
[class^="_cellTextContainer"] { [class^="_cellTextContainer"] {
/* biome-ignore lint: Ensure background is fully cleared under theme CSS */ background: unset !important;
background: unset !important;
} }
/* Make sidebar and player bar semi-transparent with optimized backdrop-filter */ /* Make sidebar and player bar semi-transparent with optimized backdrop-filter */
@@ -117,12 +129,9 @@ main,
[data-test="main-layout-sidebar-wrapper"], [data-test="main-layout-sidebar-wrapper"],
[class^="_bar"], [class^="_bar"],
[class^="_sidebarItem"]:hover { [class^="_sidebarItem"]:hover {
/* biome-ignore lint: Must beat app inline styles for translucency */ background-color: rgba(0, 0, 0, 0.3) !important;
background-color: rgba(0, 0, 0, 0.3) !important; backdrop-filter: blur(10px) !important;
/* biome-ignore lint: Must beat app inline styles for translucency */ -webkit-backdrop-filter: blur(10px) !important;
backdrop-filter: blur(10px) !important;
/* biome-ignore lint: Must beat app inline styles for translucency */
-webkit-backdrop-filter: blur(10px) !important;
} }
/* Performance mode: reduce backdrop blur */ /* Performance mode: reduce backdrop blur */
@@ -130,28 +139,21 @@ main,
.performance-mode [data-test="main-layout-sidebar-wrapper"], .performance-mode [data-test="main-layout-sidebar-wrapper"],
.performance-mode [class^="_bar"], .performance-mode [class^="_bar"],
.performance-mode [class^="_sidebarItem"]:hover { .performance-mode [class^="_sidebarItem"]:hover {
/* biome-ignore lint: Performance mode style requires priority */ backdrop-filter: blur(5px) !important;
backdrop-filter: blur(5px) !important; -webkit-backdrop-filter: blur(5px) !important;
/* biome-ignore lint: Performance mode style requires priority */
-webkit-backdrop-filter: blur(5px) !important;
} }
/* Feed sidebar panel - black tint background for readability */ /* Feed sidebar panel - black tint background for readability */
[data-test="feed-sidebar"] { [data-test="feed-sidebar"] {
/* biome-ignore lint: Ensure readability over media */ background-color: rgba(0, 0, 0, 0.5) !important;
background-color: rgba(0, 0, 0, 0.5) !important; backdrop-filter: blur(10px) !important;
/* biome-ignore lint: Ensure readability over media */ -webkit-backdrop-filter: blur(10px) !important;
backdrop-filter: blur(10px) !important;
/* biome-ignore lint: Ensure readability over media */
-webkit-backdrop-filter: blur(10px) !important;
} }
/* Performance mode: reduce sidebar backdrop blur */ /* Performance mode: reduce sidebar backdrop blur */
.performance-mode [data-test="feed-sidebar"] { .performance-mode [data-test="feed-sidebar"] {
/* biome-ignore lint: Performance mode style requires priority */ backdrop-filter: blur(5px) !important;
backdrop-filter: blur(5px) !important; -webkit-backdrop-filter: blur(5px) !important;
/* biome-ignore lint: Performance mode style requires priority */
-webkit-backdrop-filter: blur(5px) !important;
} }
/* Feed sidebar items - transparent */ /* Feed sidebar items - transparent */
@@ -160,12 +162,10 @@ main,
[class*="_cellContainer"], [class*="_cellContainer"],
[data-test="feed-interval"], [data-test="feed-interval"],
[data-test="feed-item"] { [data-test="feed-item"] {
/* biome-ignore lint: Match theme transparency */ background-color: transparent !important;
background-color: transparent !important;
} }
/* Remove bottom gradient */ /* Remove bottom gradient */
[class^="_bottomGradient"] { [class^="_bottomGradient"] {
/* biome-ignore lint: Explicitly remove conflicting gradient */ display: none !important;
display: none !important;
} }
@@ -1,9 +0,0 @@
/* Floating Rounded Player Bar from Obsidian <3 */
/* MARKER: Floating Player Bar CSS */
[data-test="footer-player"] {
position: absolute !important;
backdrop-filter: blur(10px);
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin, rgba(255, 255, 255, 0.1)) !important;
}
File diff suppressed because it is too large Load Diff
+50 -428
View File
@@ -1,465 +1,87 @@
/* Font imports for lyrics */ /* Font imports for lyrics */
@font-face { @font-face {
font-family: "AbyssFont"; font-family: "AbyssFont";
font-weight: 400; font-weight: 400;
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") format("woff2");
format("woff2");
} }
@font-face { @font-face {
font-family: "AbyssFont"; font-family: "AbyssFont";
font-weight: 500; font-weight: 500;
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2");
format("woff2");
} }
@font-face { @font-face {
font-family: "AbyssFont"; font-family: "AbyssFont";
font-weight: 600; font-weight: 600;
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2");
format("woff2");
} }
@font-face { @font-face {
font-family: "AbyssFont"; font-family: "AbyssFont";
font-weight: 700; font-weight: 700;
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2");
format("woff2");
} }
/* Enhanced lyrics styling with glow effects */ /* Enhanced lyrics styling with glow effects */
[class*="_lyricsText"] > div > span[data-current="true"] { [class*="_lyricsText"] > div > span[data-current="true"] {
text-shadow: text-shadow: 0 0 2px #fff, 0 0 20px #fff !important;
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #fff), padding-left: 20px;
/* biome-ignore lint: Required to override app glow strength */ transition-duration: 0.7s;
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #fff) !important; font-size: 55px;
padding-left: 20px; color: white !important;
transition-duration: 0.7s; font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: calc(55px * var(--rl-font-scale, 1)); font-weight: 700;
/* 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 { [class*="_lyricsText"] > div > span {
text-shadow: text-shadow: 0 0 0px transparent, 0 0 0px transparent;
0 0 0px transparent, transition-duration: 0.25s;
0 0 0px transparent; color: rgba(128, 128, 128, 0.4);
transition-duration: 0.25s; font-size: 40px;
color: rgba(128, 128, 128, 0.4); font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: calc(40px * var(--rl-font-scale, 1)); font-weight: 700;
font-family:
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 700;
} }
[class*="_lyricsText"] > div > span:hover { [class*="_lyricsText"] > div > span:hover {
text-shadow: text-shadow: 0 0 2px lightgray, 0 0 20px lightgray !important;
0 0 var(--rl-glow-inner, 2px) lightgray, color: lightgray !important;
/* biome-ignore lint: Hover glow should override defaults */ padding-left: 20px;
0 0 var(--rl-glow-outer, 20px) lightgray !important; transition-duration: 0.7s;
/* biome-ignore lint: Hover color override */
color: lightgray !important;
padding-left: 20px;
transition-duration: 0.7s;
} }
/* Track title glow */ /* Track title glow */
[data-test="now-playing-track-title"] { [data-test="now-playing-track-title"] {
/* Title text color/gradient is left to default app styling; only glow is customized. */ text-shadow: 0 0 1px #fff, 0 0 30px #fff !important;
text-shadow:
0 0 var(--rl-glow-inner, 1px) var(--cl-glow1, #fff),
/* biome-ignore lint: Title glow needs priority */
0 0 var(--rl-glow-outer, 30px) #fff !important;
/* biome-ignore lint: Reset vendor background clip */
-webkit-background-clip: initial !important;
/* biome-ignore lint: Reset background clip */
background-clip: initial !important;
/* biome-ignore lint: Reset vendor text fill */
-webkit-text-fill-color: initial !important;
/* biome-ignore lint: Ensure inherited color takes precedence */
color: inherit !important;
}
/* When track title glow setting is disabled, remove glow regardless of Colorama */
.rl-title-glow-disabled[data-test="now-playing-track-title"] {
/* biome-ignore lint: Full reset required */
text-shadow: none !important;
} }
/* Current line transitions */ /* Current line transitions */
[class*="_lyricsText"] > div > span { [class*="_lyricsText"] > div > span {
transition: transition: text-shadow 0.7s ease-in-out, color 0.7s ease-in-out, padding 0.7s ease-in-out !important;
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;
}
[data-rl-injected][role="tabpanel"] {
transform: translateX(calc(var(--rl-glow-outer) * -1)) !important;
} }
/* Lyrics container styling */ /* Lyrics container styling */
[class^="_lyricsContainer"] > div > div > span { [class^="_lyricsContainer"] > div > div > span {
margin-bottom: 2rem; margin-bottom: 2rem;
opacity: 1; opacity: 1;
font-family: font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", font-weight: 700;
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; font-size: 38px !important;
font-weight: 700;
/* biome-ignore lint: Typography override for readability */
font-size: calc(38px * var(--rl-font-scale, 1)) !important;
} }
/* MARKER: WBW lyrics CSS */ /* Reset all lyrics styling when disabled */
/* 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 */
.rl-wbw-word {
text-shadow:
0 0 0px transparent,
0 0 0px transparent;
color: rgba(128, 128, 128, 0.4);
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(128, 128, 128, 0.4), rgba(128, 128, 128, 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(128, 128, 128, 0.4);
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(128, 128, 128, 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 [class*="_lyricsText"] > div > span[data-current="true"], .lyrics-glow-disabled [class*="_lyricsText"] > div > span[data-current="true"],
.lyrics-glow-disabled [class*="_lyricsText"] > div > span:hover { .lyrics-glow-disabled [class*="_lyricsText"] > div > span,
/* biome-ignore lint: Kill glow on active/hover lines */ .lyrics-glow-disabled [class*="_lyricsText"] > div > span:hover,
text-shadow: none !important; .lyrics-glow-disabled [data-test="now-playing-track-title"],
} .lyrics-glow-disabled [class^="_lyricsContainer"] > div > div > span {
text-shadow: none !important;
/* kill glow on active word */ padding-left: 0 !important;
.lyrics-glow-disabled .rl-wbw-word.rl-wbw-active { transition: none !important;
/* biome-ignore lint: Kill glow on active word */ font-size: inherit !important;
text-shadow: none !important; color: inherit !important;
} font-family: inherit !important;
font-weight: inherit !important;
/* kill glow on hovered word */ margin-bottom: inherit !important;
.lyrics-glow-disabled .rl-wbw-line:not(.rl-wbw-line-active) opacity: inherit !important;
> .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;
} }
@@ -1,15 +1,15 @@
/* Hide player bar when setting is disabled, but show on hover - only when UI is hidden */ /* Hide player bar when setting is disabled, but show on hover - only when UI is hidden */
.radiant-lyrics-ui-hidden [data-test="footer-player"] { .radiant-lyrics-ui-hidden [data-test="footer-player"] {
opacity: 0 !important; opacity: 0 !important;
transition: opacity 0.5s ease-in-out !important; transition: opacity 0.5s ease-in-out !important;
} }
.radiant-lyrics-ui-hidden [data-test="footer-player"]:hover { .radiant-lyrics-ui-hidden [data-test="footer-player"]:hover {
opacity: 1 !important; opacity: 1 !important;
} }
/* Also show player bar when hovering over the bottom area - only when UI is hidden */ /* Also show player bar when hovering over the bottom area - only when UI is hidden */
.radiant-lyrics-ui-hidden body.rl-footer-hover [data-test="footer-player"], .radiant-lyrics-ui-hidden:has([data-test="footer-player"]:hover) [data-test="footer-player"],
.radiant-lyrics-ui-hidden [data-test="footer-player"]:hover { .radiant-lyrics-ui-hidden [data-test="footer-player"]:hover {
opacity: 1 !important; opacity: 1 !important;
} }
+208 -335
View File
@@ -1,351 +1,224 @@
/* Sidebar */
[class*="_sidebar_"] {
background-color: transparent !important;
}
/* Section header */
[class*="_sectionHeader_"] {
background-color: transparent !important;
}
/* Rounded corners */
[class*="_thumbnail_"],
[class*="_imageWrapper_"],
[class*="_coverImage_"],
[class*="_overlayIconWrapperAlbum_"],
[class*="_playButton_"] {
border-radius: 5px !important;
}
/* MARKER: HideUI CSS*/
/* Only apply styles when UI is hidden */ /* Only apply styles when UI is hidden */
.radiant-lyrics-ui-hidden [class*="tabItems"] { .radiant-lyrics-ui-hidden [class*="tabItems"] {
opacity: 0 !important; opacity: 0 !important;
transition: opacity 0.4s ease-in-out; transition: opacity 0.4s ease-in-out;
} }
.radiant-lyrics-ui-hidden [class*="tabItems"]:hover { /* Default state - visible */
opacity: 1 !important; [class*="tabItems"] {
transition: opacity 0.4s ease-in-out;
} }
/* Hide header container (search, minimize, fullscreen) when UI is hidden */ /* 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"] {
opacity: 0 !important; .radiant-lyrics-ui-hidden [data-test="header-container"]:not(:has(.hide-ui-button)) {
visibility: hidden !important; opacity: 0 !important;
transition: opacity 0.4s ease-in-out, visibility 0s linear 0.4s; transition: opacity 0.4s ease-in-out;
pointer-events: none !important;
} }
/* Immediate hide class for unhide button */ /* 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;
}
/* Default state for header */
[data-test="header-container"] {
transition: opacity 0.4s ease-in-out;
}
/* 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 with smooth transition (had issues with the fade out.. so I removed it) */
.hide-immediately { .hide-immediately {
opacity: 0 !important; opacity: 0 !important;
visibility: hidden !important; visibility: hidden !important;
pointer-events: none !important; pointer-events: none !important;
} }
/* 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 { .unhide-ui-button.auto-faded {
background-color: transparent !important; background-color: transparent !important;
border-color: transparent !important; border-color: transparent !important;
box-shadow: none !important; box-shadow: none !important;
backdrop-filter: none !important; backdrop-filter: none !important;
-webkit-backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
color: rgba(255, 255, 255, 0.4) !important; color: rgba(255, 255, 255, 0.8) !important;
transition: transition: background-color 0.8s ease-in-out, border-color 0.8s ease-in-out, box-shadow 0.8s ease-in-out, backdrop-filter 0.8s ease-in-out, color 0.8s ease-in-out;
background-color 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;
} }
/* Restore button styling on hover */
.unhide-ui-button.auto-faded:hover { .unhide-ui-button.auto-faded:hover {
background-color: rgba(255, 255, 255, 0.2) !important; background-color: rgba(255, 255, 255, 0.2) !important;
border-color: rgba(255, 255, 255, 0.3) !important; border-color: rgba(255, 255, 255, 0.3) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(10px) !important; backdrop-filter: blur(10px) !important;
-webkit-backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important;
color: white !important; color: white !important;
transition: 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;
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 tab */
[data-test="tabs-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 tab is active — show divider & make icon black*/
[data-test="tabs-lyrics"][aria-selected="true"] .sticky-lyrics-trigger {
color: black;
cursor: pointer;
}
[data-test="tabs-lyrics"][aria-selected="true"] .sticky-lyrics-trigger::before {
background: rgba(0, 0, 0, 0.25);
}
[data-test="tabs-lyrics"][aria-selected="true"] .sticky-lyrics-trigger:hover {
color: rgba(0, 0, 0, 0.6);
}
/* Square the Lyrics button bottom corners when dropdown is open */
[data-test="tabs-lyrics"].sticky-lyrics-open {
border-bottom-left-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
/* Dropdown */
.sticky-lyrics-dropdown {
position: fixed;
background: white;
border-radius: 0 0 16px 16px;
padding: 8px 12px 10px;
box-sizing: border-box;
z-index: 10000;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
clip-path: inset(0 -20px -20px -20px);
animation: stickyLyricsDropdownIn 0.12s ease-out;
}
@keyframes stickyLyricsDropdownIn {
from {
opacity: 0;
clip-path: inset(0 0 100% 0);
}
to {
opacity: 1;
clip-path: inset(0 0 0 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, 1);
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.2);
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.2);
}
.sticky-lyrics-switch input:checked + .sticky-lyrics-slider {
background-color: black;
}
.sticky-lyrics-switch input:checked + .sticky-lyrics-slider::before {
transform: translateX(16px);
}
/* 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.08);
border-radius: 10px;
padding: 2px;
gap: 2px;
width: 100%;
}
.rl-seg-btn {
flex: 1;
border: none;
background: transparent;
color: rgba(0, 0, 0, 0.5);
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.05);
}
.rl-seg-btn.rl-seg-active {
background: white;
color: black;
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 */
/* Fixes the new Sticky Header Tidal added.. in the shittest jankiest way possible */
/* [class*="_stickyHeader"] {
background: transparent !important;
backdrop-filter: blur(50px);
background-color: transparent !important;
width: fit-content !important;
padding-right: 3.5% !important;
-webkit-mask-image:
linear-gradient(to bottom, black 60%, transparent),
linear-gradient(to right, black 85%, transparent) !important;
mask-image:
linear-gradient(to bottom, black 60%, transparent),
linear-gradient(to right, black 85%, transparent) !important;
-webkit-mask-composite: source-in !important;
mask-composite: intersect !important;
padding-bottom: 5px !important;
}
[class*="_playQueueItems"]{
border-radius: 2.5px 0 0 0 !important;
}
[data-test="playqueue-sticky-clear-active-items"] {
visibility: collapse !important;
width: 0px !important;
}
[data-test="playqueue-sticky-clear-source-items"] {
visibility: collapse !important;
width: 0px !important;
} */
/* Remove the background color from the small header */
[class*="_smallHeader"]::before {
background-color: transparent !important;
}
/* fixes Tidals broken mini cover art padding | Cheers Aya <3*/
._imageBorder_110890a {
filter: opacity(0);
}
._container_14bcbd4._playingFrom_79b167e {
transform: scale(1.01) translatex(.1em);
}
._leftColumn_aaf28de {
min-height: 110%;
transform: translatey(-.23em);
}
._imageryContainer_f99fc07.image {
transform: scale(1.03) translatey(.2em) translatex(.1em);
background-color: #00000000;
padding: 0em !important;
}
._image_145331a._cellImage_0ef8dd3 {
border-radius: .7em !important;
}
[data-test="footer-player"] {
._container_14bcbd4._playingFrom_79b167e > ._text_15008b2._medium20_1lyag_192._marketText_1lyag_1 {
transform: translatey(-.2em);
}
[class="image _imageryContainer_f99fc07"] {
transform: translatey(.3em) !important;
}
._image_145331a._cellImage_0ef8dd3 {
border-radius: .25em !important;
}
._toggleButton_809eee8 {
transform: translateY(-.22em);
}
[class="image _imageryContainer_f99fc07"]:hover {
[class="_cellImage_0ef8dd3 _image_145331a"] {
filter: brightness(.3);
}
}
._notFullscreenOverlay_1442d60 {
background: none !important;
transition: 0ms;
}
._notFullscreenOverlay_1442d60 ._nowPlayingButton_c1a86fa {
background-color: rgba(245, 245, 220, 0);
}
} }