83 Commits

Author SHA1 Message Date
meoware.exe 4b2478c301 Fixed UI Freeze on Tidal line fallback 2026-04-02 16:11:06 +11:00
meoware.exe 59562f8264 Fixed Slider 2026-04-01 13:23:41 +11:00
meoware.exe 40853d6e64 Change API 2026-04-01 12:58:24 +11:00
meoware.exe 74e3c97147 Add Audio Viz to Now Playing & Remove Lyrics Scrollbar 2026-03-31 20:53:13 +11:00
meoware.exe b79e15b6c5 Fixed Cascading Redux Errors 2026-03-31 20:24:52 +11:00
meoware.exe 9d1ca88e46 Adjust UI thingos 2026-03-28 21:39:34 +11:00
meoware.exe bff87b96a1 Cleanup Legacy Code 2026-03-28 21:12:06 +11:00
meoware.exe 9d6afcaaf5 Lyrics Dropdown 2026-03-28 20:55:33 +11:00
meoware.exe 8f995d8474 Made Compatible (Not Rewritten) 2026-03-27 23:47:56 +11:00
meoware.exe 5189d2bbea Prevent New UI 2026-03-25 15:07:02 +11:00
meoware.exe 1ab2eda25c Platform Param (DX Logs) 2026-03-10 20:57:20 +11:00
meoware.exe 5e00accc7f Fix Race Condition 2026-03-02 14:32:32 +11:00
meoware.exe 765c8baf96 Better Glow Cutoff | Thx Aya <3 2026-02-28 22:33:05 +11:00
meoware.exe d6c2d3ac88 Fixed Glow Cutoff 2026-02-28 22:29:24 +11:00
meoware.exe 7748f2fe08 Fixed Player Bar Border 2026-02-28 21:43:36 +11:00
meoware.exe 7ad4bbb332 Hotfix #7000... 2026-02-28 21:33:44 +11:00
meoware.exe b493624bda Patched Context Menus.. Again.. 2026-02-28 21:09:44 +11:00
meoware.exe 651e5cbc14 Quick Hotfix 2026-02-28 20:58:13 +11:00
meoware.exe 3e51ac45f8 Fixed Context Menus & other things 2026-02-28 19:51:34 +11:00
meoware.exe 76b1e264f8 Inject Lyrics to Tracks without them in tidal <3 2026-02-28 19:02:24 +11:00
meoware.exe c88ddef2f9 Forgot a Timeout <3 2026-02-28 16:45:21 +11:00
meoware.exe 38cdc156d6 Romanize ALL Tracks <3 2026-02-28 16:30:16 +11:00
meoware.exe 055fff6d47 Apply Effects to ALL Tracks! 2026-02-28 15:57:21 +11:00
meoware.exe 00eaf37dfa Added Romanized Lyrics 2026-02-27 19:19:54 +11:00
meoware.exe c6e916e6f6 Fixed Mini Cover Art Padding 2026-02-25 21:37:27 +11:00
meoware.exe 64dfe47592 Added Lyric Font Size 2026-02-25 21:02:09 +11:00
meoware.exe ef4c73037f Fixed Disabling Lyrics Glow 2026-02-25 20:54:51 +11:00
meoware.exe ec25abf6f5 Fixed Colorama lyrics 2026-02-25 20:38:30 +11:00
meowarex 7d2f3d3c1a Reduced 404 Spams 2026-02-25 17:29:35 +11:00
meoware.exe e766bac0fa Apply Context Aware & Bubbled lyrics to Line 2026-02-24 23:38:16 +11:00
meoware.exe d07444e102 Update Gitignore <3 2026-02-24 23:28:02 +11:00
meoware.exe 56c73abc05 CodeReview 2026-02-24 23:25:21 +11:00
meoware.exe 20adbd26dc Context Aware Lyrics & Aniamtions (Line) 2026-02-24 23:08:29 +11:00
meoware.exe ff417f5472 ISRC Support 2026-02-24 15:05:46 +11:00
meoware.exe 0a694a5bc0 WIP Animations 2026-02-21 03:54:45 +11:00
meoware.exe 84af1a40f6 REMIX Detection 2026-02-21 01:06:54 +11:00
meoware.exe adcbadcf49 Cleanup <3 2026-02-20 23:53:17 +11:00
meoware.exe af4cd80c7c Fixed Random Srcub Fire 2026-02-20 23:05:51 +11:00
meoware.exe 256dd3d724 Syllable Lyrics <3 2026-02-20 22:55:10 +11:00
meoware.exe d6a3b26b41 Rewrite Timeouts + Bug Fixes 2026-02-20 15:21:58 +11:00
meoware.exe df80ef748e Fix Race Condition 2026-02-19 23:53:48 +11:00
meoware.exe 68fc92b2db WBW + Observer Refactor + Prep for Syllables 2026-02-19 23:44:03 +11:00
meoware.exe 1aa12e9fd3 Improved Logic 2026-02-13 14:47:29 +11:00
meoware.exe 8196ed6778 Added Quality Matched Seeker Color 2026-02-13 14:28:39 +11:00
meoware.exe 6af3b93272 Deprecated Obsidian | Merged into RL <3 2026-02-11 21:01:05 +11:00
meoware.exe 422d03a54e Biome <3 2026-02-11 20:54:57 +11:00
meoware.exe b27f0ca165 CodeReview Changes 2026-02-11 20:46:07 +11:00
meoware.exe cd35fee3f0 Merged Obsidian into RL + Added Conditional Settings Visability 2026-02-11 20:26:05 +11:00
meoware.exe bce5ddba54 Updated Stikcy Lyrics Default 2026-02-09 22:58:51 +11:00
meoware.exe 9c537fa877 Added Sticky Lyrics 2026-02-09 22:49:29 +11:00
meoware.exe 6981cc8315 Removed Small Header BG 2026-02-09 19:36:58 +11:00
meoware.exe 56b7476e92 Fix Tidals New Sticky Header 2026-02-08 00:27:54 +11:00
meoware.exe 09857b6b54 Fixed Media Table Border + Search bar Width 2026-01-15 21:25:35 +11:00
Meow Meow 5e700692e7 Fix typos and improve README formatting
Updated installation instructions and credits section.
2026-01-11 15:47:52 +11:00
meoware.exe dc82194a90 Reduces New Corner Radius 2025-12-30 14:49:06 +11:00
meoware.exe 36257a954e HideUI Now hides header bar (Minimize, Fullscreen & Search) 2025-12-30 14:37:30 +11:00
meoware.exe e62944a0df Fixed HideUI & Removed Animations :( 2025-12-30 14:11:49 +11:00
meoware.exe 6d9184e5eb Re added CSS 2025-12-30 13:40:21 +11:00
meoware.exe 081b4cbdd8 Removed Duplicate Code 2025-12-30 13:17:25 +11:00
meoware.exe 4ca99ebd72 Updated for Sidebar 3.0 + Fixed Image Radius + Fixed Header Clipping 2025-12-30 13:10:18 +11:00
meoware.exe 047d4de2f4 Update BIOME Preferences 2025-10-22 11:08:57 +11:00
meoware.exe d83a786de3 Updated README <3 2025-09-09 21:36:24 +10:00
meoware.exe 0356ea6b76 Renamed oled-theme to obsidian 2025-09-09 21:30:19 +10:00
meoware.exe b9a9588f9d Adjusted Default Settings 2025-09-09 21:09:08 +10:00
meoware.exe fa0a7b7f56 Adjusted Cover Scale Settings 2025-09-09 20:58:28 +10:00
meoware.exe f2c31bb33a Background Cover Scale 2025-09-09 20:44:15 +10:00
meoware.exe 78d960588c Background Radius 2025-09-09 20:23:42 +10:00
meoware.exe 2ea44bd3cc Background Cover Scale 2025-09-09 20:09:24 +10:00
meoware.exe 9c9b47c930 Fixed Green HideUI Button 2025-09-09 19:33:29 +10:00
meoware.exe d53fd08ee8 Code Review 2025-09-09 19:20:30 +10:00
meoware.exe 11d08b6403 Flow Refactor 2025-09-09 19:01:12 +10:00
meoware.exe 0d9b378e43 BIOME Refactor 2025-09-09 18:31:35 +10:00
meoware.exe 99661096d5 BIOME Formating 2025-09-09 17:59:47 +10:00
meoware.exe 8178699d81 Fixed Title Glow Persistance 2025-08-14 11:39:08 +10:00
meoware.exe 82dfb39ff5 Improved Settings + Labeling 2025-08-13 21:32:06 +10:00
meoware.exe 0b9c27eaaf Cleanup 2025-08-13 21:14:32 +10:00
meoware.exe 40ed89dd34 Updated Settings 2025-08-13 21:08:30 +10:00
meoware.exe c0255acb4c Improved Settings 2025-08-13 20:25:20 +10:00
meoware.exe 1fda054d2a Added Colorama-Lyrics 2025-08-12 23:43:51 +10:00
meoware.exe cf9bbb62e6 Updated Defaults 2025-08-12 22:25:57 +10:00
meoware.exe 7de6a98d8e Adjustable Glow 2025-08-12 22:13:28 +10:00
meoware.exe 2e7e51b7eb Removed Lib 2025-08-12 21:52:05 +10:00
meoware.exe fe3f0011eb Performance Overhaul 2025-08-12 21:41:52 +10:00
34 changed files with 10055 additions and 3539 deletions
+2 -4
View File
@@ -1,6 +1,4 @@
node_modules/
dist/
dist/itzzexcel.oled-theme.json
dist/itzzexcel.oled-theme.mjs
dist/itzzexcel.oled-theme.mjs.map
dist/store.json
Notes.md
/Reference/
+4
View File
@@ -0,0 +1,4 @@
{
"snyk.advanced.organization": "5e35d31d-0df9-4f3c-b4b3-168a24114801",
"snyk.advanced.autoSelectOrganization": true
}
+32 -13
View File
@@ -4,14 +4,13 @@ A collection of Luna plugins for Tidal, ported from Neptune framework.
## Plugins
### 🎨 OLED Theme
**Location:** `plugins/oled-theme-luna/`
### 🎨 Obsidian
**Location:** `plugins/obsidian-theme-luna/`
A dark OLED-friendly theme plugin that transforms Tidal Luna's appearance.
A dark OLED-friendly theme that transforms Tidal Luna's appearance.
**Features:**
- Applies a dark, OLED-optimized theme
- Fetches the latest theme CSS from the GitHub repository
- Reduces battery consumption on OLED displays.. i guess <3
- Modern, sleek dark interface
@@ -34,6 +33,16 @@ Allows users to copy song lyrics by selecting them directly in the interface.
- Automatic clipboard copying of selected lyrics
- Smart lyric span detection
### 🧽 Element Hider
**Location:** `plugins/element-hider-luna/`
Allows users to hide/remove UI elements by right clicking on them.
**Features:**
- Remove/Hide ANY UI element
- Automagically saves hidden elements
- Allows for elements to be restored
### 🎶 Audio Visualizer
**Location:** `plugins/audio-visualizer-luna/`
@@ -49,8 +58,21 @@ Allows users to copy song lyrics by selecting them directly in the interface.
## 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
1. Open TidalLuna after Building & Serving
### (They are in the store by default now)
1. Open TidaLuna after Building & Serving
2. Navigate to Luna Settings (Top right of Tidal)
3. Click "Plugin Store" Tab
4. Paste in the "Install from URL" Bar `https://github.com/meowarex/tidalluna-plugins/releases/download/latest/store.json`
@@ -63,7 +85,7 @@ Allows users to copy song lyrics by selecting them directly in the interface.
git clone https://github.com/meowarex/tidalluna-plugins
# Change Folder to the Repo
cd neptune-projects-fork
cd tidalluna-plugins
# Install dependencies
pnpm install
@@ -73,7 +95,7 @@ pnpm run watch
```
### Installing Plugins in TidalLuna
1. Open TidalLuna after Building & Serving
1. Open TidaLuna after Building & Serving
2. Navigate to Luna Settings (Top right of Tidal)
3. Click "Plugin Store" Tab
4. Click Install on the Plugins at the top Labeled with "[Dev]"
@@ -82,7 +104,7 @@ pnpm run watch
## Development
This project is made for:
- **TidalLuna** - Modern plugin framework for Tidal | Inrixia
- **[TidaLuna](https://github.com/Inrixia/TidaLuna)** - Modern plugin framework for Tidal | Inrixia
## GitHub Actions
@@ -90,10 +112,7 @@ This project is made for:
- **Release automation** for distributing plugins
- **Artifact uploads** for easy plugin distribution
## Based On <3
- **itzzexcel** - [GitHub](https://github.com/ItzzExcel)
## Credits
Original Neptune versions by itzzexcel. Ported to Luna framework following the Luna plugin template structure by meowarex with help from Inrixia <3
Inrixia | [TidalLuna](https://github.com/Inrixia/TidaLuna) Plugin Framework (The successor Neptune)
ItzzExcel | The Original Neptune version of "Radiant Lyrics" (Which was ported to Luna and Rewritten by me!)
+9
View File
@@ -0,0 +1,9 @@
{
"linter": {
"rules": {
"complexity": {
"useArrowFunction": "off"
}
}
}
}
+2356
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -18,5 +18,8 @@
"rimraf": "^6.0.1",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
},
"dependencies": {
"pnpm": "^10.14.0"
}
}
+410 -329
View File
@@ -1,355 +1,436 @@
import { ReactiveStore } from "@luna/core";
import { LunaSettings, LunaNumberSetting, LunaSwitchSetting, LunaTextSetting } from "@luna/ui";
import {
LunaSettings,
LunaNumberSetting,
LunaSwitchSetting,
LunaTextSetting,
} from "@luna/ui";
import React from "react";
export const settings = await ReactiveStore.getPluginStorage("AudioVisualizer", {
barCount: 32,
barColor: "#ffffff",
barRounding: true,
customColors: [] as string[]
});
export const settings = await ReactiveStore.getPluginStorage(
"AudioVisualizer",
{
barCount: 32,
barColor: "#ffffff",
barRounding: true,
customColors: [] as string[],
},
);
export const Settings = () => {
const [barCount, setBarCount] = React.useState(settings.barCount);
const [barColor, setBarColor] = React.useState(settings.barColor);
const [barRounding, setBarRounding] = React.useState(settings.barRounding);
const [showColorPicker, setShowColorPicker] = React.useState(false);
const [isAnimatingIn, setIsAnimatingIn] = React.useState(false);
const [shouldRender, setShouldRender] = React.useState(false);
const [customInput, setCustomInput] = React.useState(settings.barColor);
const [customColors, setCustomColors] = React.useState(settings.customColors);
const [hoveredColorIndex, setHoveredColorIndex] = React.useState<number | null>(null);
const [barCount, setBarCount] = React.useState(settings.barCount);
const [barColor, setBarColor] = React.useState(settings.barColor);
const [barRounding, setBarRounding] = React.useState(settings.barRounding);
const [showColorPicker, setShowColorPicker] = React.useState(false);
const [isAnimatingIn, setIsAnimatingIn] = React.useState(false);
const [shouldRender, setShouldRender] = React.useState(false);
const [customInput, setCustomInput] = React.useState(settings.barColor);
const [customColors, setCustomColors] = React.useState(settings.customColors);
const [hoveredColorIndex, setHoveredColorIndex] = React.useState<
number | null
>(null);
const closeColorPicker = () => {
setIsAnimatingIn(false);
setTimeout(() => {
setShowColorPicker(false);
setShouldRender(false);
}, 200); // Wait for animation to complete because i need to
};
const closeColorPicker = () => {
setIsAnimatingIn(false);
setTimeout(() => {
setShowColorPicker(false);
setShouldRender(false);
}, 200); // Wait for animation to complete because i need to
};
const openColorPicker = () => {
setShowColorPicker(true);
setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10);
};
const openColorPicker = () => {
setShowColorPicker(true);
setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10);
};
React.useEffect(() => {
if (showColorPicker) {
setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10);
}
}, [showColorPicker]);
React.useEffect(() => {
if (showColorPicker) {
setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10);
}
}, [showColorPicker]);
// Common color presets for cool points :D
const colorPresets = [
"#ffffff", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff",
"#ff8800", "#8800ff", "#0088ff", "#88ff00", "#ff0088", "#00ff88",
"#444444", "#888888", "#cccccc", "#1db954", "#e22134", "#1976d2"
];
// Common color presets for cool points :D
const colorPresets = [
"#ffffff",
"#ff0000",
"#00ff00",
"#0000ff",
"#ffff00",
"#ff00ff",
"#00ffff",
"#ff8800",
"#8800ff",
"#0088ff",
"#88ff00",
"#ff0088",
"#00ff88",
"#444444",
"#888888",
"#cccccc",
"#1db954",
"#e22134",
"#1976d2",
];
const updateColor = (color: string) => {
setBarColor(color);
setCustomInput(color);
settings.barColor = color;
(window as any).updateAudioVisualizer?.();
};
const updateColor = (color: string) => {
setBarColor(color);
setCustomInput(color);
settings.barColor = color;
(window as any).updateAudioVisualizer?.();
};
const addCustomColor = () => {
if (customInput) {
// Trim whitespace and convert to lowercase
const trimmedInput = customInput.trim().toLowerCase();
const addCustomColor = () => {
if (customInput) {
// Trim whitespace and convert to lowercase
const trimmedInput = customInput.trim().toLowerCase();
// Validate hex color format
const hexColorRegex = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
// Validate hex color format
const hexColorRegex = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
if (hexColorRegex.test(trimmedInput) &&
!colorPresets.includes(trimmedInput) &&
!customColors.includes(trimmedInput)) {
const newCustomColors = [...customColors, trimmedInput];
setCustomColors(newCustomColors);
settings.customColors = newCustomColors;
}
}
};
if (
hexColorRegex.test(trimmedInput) &&
!colorPresets.includes(trimmedInput) &&
!customColors.includes(trimmedInput)
) {
const newCustomColors = [...customColors, trimmedInput];
setCustomColors(newCustomColors);
settings.customColors = newCustomColors;
}
}
};
const removeCustomColor = (colorToRemove: string) => {
const newCustomColors = customColors.filter(color => color !== colorToRemove);
setCustomColors(newCustomColors);
settings.customColors = newCustomColors;
const removeCustomColor = (colorToRemove: string) => {
const newCustomColors = customColors.filter(
(color) => color !== colorToRemove,
);
setCustomColors(newCustomColors);
settings.customColors = newCustomColors;
// If the removed color was the selected color (reset to white)
if (barColor === colorToRemove) {
updateColor("#ffffff");
}
};
// If the removed color was the selected color (reset to white)
if (barColor === colorToRemove) {
updateColor("#ffffff");
}
};
const allColors = [...colorPresets, ...customColors];
const allColors = [...colorPresets, ...customColors];
return (
<LunaSettings>
<LunaSwitchSetting
title="Bar Roundness"
desc="Enable rounded corners on visualizer bars"
checked={barRounding}
onChange={(_, checked) => {
setBarRounding(checked);
settings.barRounding = checked;
(window as any).updateAudioVisualizer?.();
}}
/>
return (
<LunaSettings>
<LunaSwitchSetting
title="Bar Roundness"
desc="Enable rounded corners on visualizer bars"
checked={barRounding}
onChange={(_, checked) => {
setBarRounding(checked);
settings.barRounding = checked;
(window as any).updateAudioVisualizer?.();
}}
/>
<LunaNumberSetting
title="Bar Count"
desc="Number of frequency bars to display"
min={8}
max={64}
step={1}
value={barCount}
onNumber={(value: number) => {
setBarCount(value);
settings.barCount = value;
(window as any).updateAudioVisualizer?.();
}}
/>
<LunaNumberSetting
title="Bar Count"
desc="Number of frequency bars to display"
min={8}
max={64}
step={1}
value={barCount}
onNumber={(value: number) => {
setBarCount(value);
settings.barCount = value;
(window as any).updateAudioVisualizer?.();
}}
/>
{/* YUP YOUR EYES WORK... we do be using React code in the settings..*/}
{/* I'm not sure if this is a good idea, but it works & looks amazing */}
{/* Sorry @Inrixia <3 */}
{/* YUP YOUR EYES WORK... we do be using React code in the settings..*/}
{/* I'm not sure if this is a good idea, but it works & looks amazing */}
{/* Sorry @Inrixia <3 */}
<div style={{
padding: "16px 0",
display: "flex",
justifyContent: "space-between",
alignItems: "center"
}}>
<div>
<div style={{ fontWeight: "normal", fontSize: "1.075rem", marginBottom: "4px" }}>Bar Color</div>
<div style={{ opacity: 0.7, fontSize: "14px" }}>Color of the visualizer bars</div>
</div>
<div style={{ display: "flex", gap: "8px", alignItems: "center", position: "relative" }}>
<button
onClick={() => showColorPicker ? closeColorPicker() : openColorPicker()}
style={{
width: "32px",
height: "32px",
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: "6px",
cursor: "pointer",
background: barColor,
backdropFilter: "blur(10px)",
WebkitBackdropFilter: "blur(10px)",
position: "relative",
overflow: "hidden"
}}
>
<div style={{
position: "absolute",
inset: 0,
background: "rgba(0,0,0,0.1)",
backdropFilter: "blur(2px)"
}} />
</button>
<div
style={{
padding: "16px 0",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div>
<div
style={{
fontWeight: "normal",
fontSize: "1.075rem",
marginBottom: "4px",
}}
>
Bar Color
</div>
<div style={{ opacity: 0.7, fontSize: "14px" }}>
Color of the visualizer bars
</div>
</div>
<div
style={{
display: "flex",
gap: "8px",
alignItems: "center",
position: "relative",
}}
>
<button
onClick={() =>
showColorPicker ? closeColorPicker() : openColorPicker()
}
style={{
width: "32px",
height: "32px",
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: "6px",
cursor: "pointer",
background: barColor,
backdropFilter: "blur(10px)",
WebkitBackdropFilter: "blur(10px)",
position: "relative",
overflow: "hidden",
}}
>
<div
style={{
position: "absolute",
inset: 0,
background: "rgba(0,0,0,0.1)",
backdropFilter: "blur(2px)",
}}
/>
</button>
{/* Custom Color Picker Modal */}
{shouldRender && (
<>
{/* Backdrop */}
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0,0,0,0.6)",
zIndex: 1000,
opacity: isAnimatingIn ? 1 : 0,
transition: "opacity 0.2s ease"
}}
onClick={closeColorPicker}
/>
{/* Custom Color Picker Modal */}
{shouldRender && (
<>
{/* Backdrop */}
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0,0,0,0.6)",
zIndex: 1000,
opacity: isAnimatingIn ? 1 : 0,
transition: "opacity 0.2s ease",
}}
onClick={closeColorPicker}
/>
{/* Color Picker Panel */}
<div style={{
position: "fixed",
top: "50%",
left: "50%",
background: "rgba(20,20,20,0.98)",
backdropFilter: "blur(20px)",
WebkitBackdropFilter: "blur(20px)",
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: "16px",
padding: "20px",
minWidth: "320px",
maxWidth: "90vw",
maxHeight: "90vh",
zIndex: 1001,
boxShadow: "0 20px 40px rgba(0,0,0,0.7)",
opacity: isAnimatingIn ? 1 : 0,
transform: isAnimatingIn ? "translate(-50%, -50%) scale(1)" : "translate(-50%, -50%) scale(0.9)",
transition: "all 0.2s ease"
}}>
<div style={{ marginBottom: "12px", color: "#fff", fontWeight: "bold", fontSize: "14px" }}>
Choose Color
</div>
{/* Color Picker Panel */}
<div
style={{
position: "fixed",
top: "50%",
left: "50%",
background: "rgba(20,20,20,0.98)",
backdropFilter: "blur(20px)",
WebkitBackdropFilter: "blur(20px)",
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: "16px",
padding: "20px",
minWidth: "320px",
maxWidth: "90vw",
maxHeight: "90vh",
zIndex: 1001,
boxShadow: "0 20px 40px rgba(0,0,0,0.7)",
opacity: isAnimatingIn ? 1 : 0,
transform: isAnimatingIn
? "translate(-50%, -50%) scale(1)"
: "translate(-50%, -50%) scale(0.9)",
transition: "all 0.2s ease",
}}
>
<div
style={{
marginBottom: "12px",
color: "#fff",
fontWeight: "bold",
fontSize: "14px",
}}
>
Choose Color
</div>
{/* Color Grid */}
<div style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: "8px",
marginBottom: "16px"
}}>
{allColors.map((color, index) => {
const isCustomColor = customColors.includes(color);
const isHovered = hoveredColorIndex === index;
return (
<div
key={index}
style={{
position: "relative",
width: "32px",
height: "32px",
cursor: "pointer"
}}
className="color-item"
onMouseEnter={() => setHoveredColorIndex(index)}
onMouseLeave={() => setHoveredColorIndex(null)}
>
<button
onClick={() => {
updateColor(color);
closeColorPicker();
}}
style={{
width: "100%",
height: "100%",
borderRadius: "6px",
border: barColor === color ? "2px solid #fff" : "1px solid rgba(255,255,255,0.2)",
background: color,
cursor: "pointer",
transition: "all 0.2s ease"
}}
/>
{isCustomColor && (
<button
onClick={(e) => {
e.stopPropagation();
removeCustomColor(color);
}}
style={{
position: "absolute",
top: "-4px",
right: "-4px",
width: "16px",
height: "16px",
borderRadius: "50%",
border: "1px solid rgba(255,255,255,0.8)",
background: "rgba(0,0,0,0.8)",
color: "#fff",
cursor: "pointer",
fontSize: "10px",
display: "flex",
alignItems: "center",
justifyContent: "center",
opacity: isHovered ? 1 : 0,
transition: "opacity 0.2s ease",
zIndex: 10
}}
className="remove-button"
>
×
</button>
)}
</div>
);
})}
</div>
{/* Color Grid */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: "8px",
marginBottom: "16px",
}}
>
{allColors.map((color, index) => {
const isCustomColor = customColors.includes(color);
const isHovered = hoveredColorIndex === index;
return (
<div
key={index}
style={{
position: "relative",
width: "32px",
height: "32px",
cursor: "pointer",
}}
className="color-item"
onMouseEnter={() => setHoveredColorIndex(index)}
onMouseLeave={() => setHoveredColorIndex(null)}
>
<button
onClick={() => {
updateColor(color);
closeColorPicker();
}}
style={{
width: "100%",
height: "100%",
borderRadius: "6px",
border:
barColor === color
? "2px solid #fff"
: "1px solid rgba(255,255,255,0.2)",
background: color,
cursor: "pointer",
transition: "all 0.2s ease",
}}
/>
{isCustomColor && (
<button
onClick={(e) => {
e.stopPropagation();
removeCustomColor(color);
}}
style={{
position: "absolute",
top: "-4px",
right: "-4px",
width: "16px",
height: "16px",
borderRadius: "50%",
border: "1px solid rgba(255,255,255,0.8)",
background: "rgba(0,0,0,0.8)",
color: "#fff",
cursor: "pointer",
fontSize: "10px",
display: "flex",
alignItems: "center",
justifyContent: "center",
opacity: isHovered ? 1 : 0,
transition: "opacity 0.2s ease",
zIndex: 10,
}}
className="remove-button"
>
×
</button>
)}
</div>
);
})}
</div>
{/* Custom Hex Input */}
<div style={{ marginBottom: "12px" }}>
<div style={{ color: "rgba(255,255,255,0.7)", fontSize: "12px", marginBottom: "6px" }}>
Add Custom Color
</div>
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
<input
type="text"
value={customInput}
onChange={(e) => setCustomInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
updateColor(customInput);
addCustomColor();
}
}}
placeholder="#ffffff"
style={{
flex: 1,
padding: "8px 12px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.1)",
color: "#fff",
fontSize: "14px",
fontFamily: "monospace",
boxSizing: "border-box"
}}
/>
<button
onClick={() => {
updateColor(customInput);
addCustomColor();
}}
style={{
width: "32px",
height: "32px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.3)",
background: "rgba(255,255,255,0.15)",
color: "#fff",
cursor: "pointer",
fontSize: "16px",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.2s ease"
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.25)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.15)";
}}
>
+
</button>
</div>
</div>
{/* Custom Hex Input */}
<div style={{ marginBottom: "12px" }}>
<div
style={{
color: "rgba(255,255,255,0.7)",
fontSize: "12px",
marginBottom: "6px",
}}
>
Add Custom Color
</div>
<div
style={{
display: "flex",
gap: "8px",
alignItems: "center",
}}
>
<input
type="text"
value={customInput}
onChange={(e) => setCustomInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
updateColor(customInput);
addCustomColor();
}
}}
placeholder="#ffffff"
style={{
flex: 1,
padding: "8px 12px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.1)",
color: "#fff",
fontSize: "14px",
fontFamily: "monospace",
boxSizing: "border-box",
}}
/>
<button
onClick={() => {
updateColor(customInput);
addCustomColor();
}}
style={{
width: "32px",
height: "32px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.3)",
background: "rgba(255,255,255,0.15)",
color: "#fff",
cursor: "pointer",
fontSize: "16px",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.2s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background =
"rgba(255,255,255,0.25)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background =
"rgba(255,255,255,0.15)";
}}
>
+
</button>
</div>
</div>
{/* Close Button (Done) - Also runs when color chosen*/}
<button
onClick={closeColorPicker}
style={{
width: "100%",
padding: "8px",
borderRadius: "6px",
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.1)",
color: "#fff",
cursor: "pointer",
fontSize: "12px"
}}
>
Done
</button>
</div>
</>
)}
</div>
</div>
</LunaSettings>
);
{/* 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>
);
};
+351 -344
View File
@@ -7,24 +7,24 @@ import visualizerStyles from "file://styles.css?minify";
export const { trace } = Tracer("[Audio Visualizer]");
// Helper function for consistent logging
const log = (message: string) => console.log(`[Audio Visualizer] ${message}`);
const warn = (message: string) => console.warn(`[Audio Visualizer] ${message}`);
const error = (message: string) => console.error(`[Audio Visualizer] ${message}`);
export { Settings };
// Basic config with settings
const config = {
enabled: true,
position: 'left' as 'left' | 'right',
width: 200,
height: 40,
get barCount() { return settings.barCount; },
get color() { return settings.barColor; },
get barRounding() { return settings.barRounding; },
sensitivity: 1.5,
smoothing: 0.8,
visualizerType: 'bars' as 'bars' | 'waveform' | 'circular'
enabled: true,
width: 200,
height: 40,
get barCount() {
return settings.barCount;
},
get color() {
return settings.barColor;
},
get barRounding() {
return settings.barRounding;
},
sensitivity: 1.5,
smoothing: 0.8,
};
// Clean up resources
@@ -42,290 +42,296 @@ let animationId: number | null = null;
let currentAudioElement: HTMLAudioElement | null = null;
let isSourceConnected: boolean = false;
// Canvas and container elements
let visualizerContainer: HTMLDivElement | null = null;
let canvas: HTMLCanvasElement | null = null;
let canvasContext: CanvasRenderingContext2D | null = null;
// Each placement gets its own container/canvas/context
interface VisualizerSlot {
container: HTMLDivElement | null;
canvas: HTMLCanvasElement | null;
ctx: CanvasRenderingContext2D | null;
}
const navSlot: VisualizerSlot = { container: null, canvas: null, ctx: null };
const npSlot: VisualizerSlot = { container: null, canvas: null, ctx: null };
// Find the audio element - this is a bit of a hack but it works
const findAudioElement = (): HTMLAudioElement | null => {
// Try main selectors first
const selectors = [
'audio',
'video',
'audio[data-test]',
'[data-test="audio-player"] audio'
];
// Try main selectors first
const selectors = [
"audio",
"video",
"audio[data-test]",
'[data-test="audio-player"] audio',
];
for (const selector of selectors) {
const element = document.querySelector(selector) as HTMLAudioElement;
if (element && (element.tagName === 'AUDIO' || element.tagName === 'VIDEO')) {
return element;
}
}
for (const selector of selectors) {
const element = document.querySelector(selector) as HTMLAudioElement;
if (
element &&
(element.tagName === "AUDIO" || element.tagName === "VIDEO")
) {
return element;
}
}
// Quick scan for any audio elements
const audioElements = document.querySelectorAll('audio, video');
for (const element of audioElements) {
const audioEl = element as HTMLAudioElement;
if (audioEl.src || audioEl.currentSrc) {
return audioEl;
}
}
// Quick scan for any audio elements
const audioElements = document.querySelectorAll("audio, video");
for (const element of audioElements) {
const audioEl = element as HTMLAudioElement;
if (audioEl.src || audioEl.currentSrc) {
return audioEl;
}
}
return null;
return null;
};
// Initialize audio visualization
const initializeAudioVisualizer = async (): Promise<void> => {
try {
// Find the audio element
const audioElement = findAudioElement();
if (!audioElement) {
return;
}
try {
// Find the audio element
const audioElement = findAudioElement();
if (!audioElement) {
return;
}
// create audio context
if (!audioContext) {
audioContext = new AudioContext();
log("Created AudioContext");
}
// create audio context
if (!audioContext) {
audioContext = new AudioContext();
log("Created AudioContext");
}
// create analyser
if (!analyser) {
analyser = audioContext.createAnalyser();
analyser.fftSize = 512; // Fixed power of 2 that provides enough frequency bins
analyser.smoothingTimeConstant = config.smoothing;
dataArray = new Uint8Array(analyser.frequencyBinCount);
log("Created AnalyserNode");
}
// create analyser
if (!analyser) {
analyser = audioContext.createAnalyser();
analyser.fftSize = 512; // Fixed power of 2 that provides enough frequency bins
analyser.smoothingTimeConstant = config.smoothing;
dataArray = new Uint8Array(analyser.frequencyBinCount);
log("Created AnalyserNode");
}
// attempt audio connection if not already connected
if (!isSourceConnected && audioElement !== currentAudioElement) {
try {
// Create audio source - this might fail if already connected elsewhere
audioSource = audioContext.createMediaElementSource(audioElement);
audioSource.connect(analyser);
// CRITICAL: connect back to destination for audio output (otherwise no sound)
analyser.connect(audioContext.destination);
// attempt audio connection if not already connected
if (!isSourceConnected && audioElement !== currentAudioElement) {
try {
// Create audio source - this might fail if already connected elsewhere
audioSource = audioContext.createMediaElementSource(audioElement);
audioSource.connect(analyser);
// CRITICAL: connect back to destination for audio output (otherwise no sound)
analyser.connect(audioContext.destination);
currentAudioElement = audioElement;
isSourceConnected = true;
log("Connected to audio stream with output");
} catch (error) {
// Audio is connected elsewhere - that's fine, we just can't visualize
if (error instanceof Error && error.message.includes('already connected')) {
log("Audio already connected elsewhere - skipping visualization");
}
return;
}
}
currentAudioElement = audioElement;
isSourceConnected = true;
log("Connected to audio stream with output");
} catch (error) {
// Audio is connected elsewhere - that's fine, we just can't visualize
if (
error instanceof Error &&
error.message.includes("already connected")
) {
log("Audio already connected elsewhere - skipping visualization");
}
return;
}
}
// Resume context only if needed and don't wait for it
// (otherwise it will wait for the audio to start playing)
if (audioContext.state === 'suspended') {
audioContext.resume().catch(() => {}); // Fire and forget
}
// Resume context only if needed and don't wait for it
// (otherwise it will wait for the audio to start playing)
if (audioContext.state === "suspended") {
audioContext.resume().catch(() => {}); // Fire and forget
}
// Create UI only if it doesn't exist
if (!visualizerContainer) {
createVisualizerUI();
}
createVisualizerUI();
// Start animation only if not already running
if (!animationId) {
animate();
}
} catch (err) {
// log errors
console.error(err);
}
// Start animation only if not already running
if (!animationId) {
animate();
}
} catch (err) {
// log errors
console.error(err);
}
};
const makeSlotElements = (): { container: HTMLDivElement; canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } | null => {
const container = document.createElement("div");
container.className = "audio-visualizer-container";
container.style.cssText = `
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 4px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
`;
const cvs = document.createElement("canvas");
cvs.width = config.width;
cvs.height = config.height;
cvs.style.cssText = `
width: ${config.width}px;
height: ${config.height}px;
border-radius: 4px;
`;
container.appendChild(cvs);
const ctx = cvs.getContext("2d");
if (!ctx) return null;
return { container, canvas: cvs, ctx };
};
const clearSlot = (slot: VisualizerSlot): void => {
slot.container?.remove();
slot.container = null;
slot.canvas = null;
slot.ctx = null;
};
const ensureNavSlot = (): void => {
if (navSlot.container?.isConnected) return;
clearSlot(navSlot);
const searchField = document.querySelector('input[class*="_searchField"]') as HTMLInputElement;
if (!searchField) return;
const searchContainer = searchField.parentElement;
if (!searchContainer?.parentElement) return;
const els = makeSlotElements();
if (!els) return;
els.container.style.marginRight = "12px";
Object.assign(navSlot, els);
searchContainer.parentElement.insertBefore(els.container, searchContainer);
};
const ensureNpSlot = (): void => {
if (npSlot.container?.isConnected) return;
clearSlot(npSlot);
const artistInfo = document.querySelector('[data-test="artist-info"]');
if (!artistInfo) return;
const leftContent = artistInfo.parentElement;
if (!leftContent) return;
const els = makeSlotElements();
if (!els) return;
els.container.style.marginLeft = "12px";
Object.assign(npSlot, els);
leftContent.insertBefore(els.container, artistInfo.nextSibling);
};
// Create the visualizer UI container and canvas
const createVisualizerUI = (): void => {
// Remove existing visualizer if it exists
removeVisualizerUI();
if (!config.enabled) return;
// Find the search bar
const searchField = document.querySelector('input[class*="_searchField"]') as HTMLInputElement;
if (!searchField) {
warn("Search field not found");
return;
}
const searchContainer = searchField.parentElement;
if (!searchContainer) {
warn("Search container not found");
return;
}
// Create visualizer container
visualizerContainer = document.createElement('div');
visualizerContainer.id = 'audio-visualizer-container';
visualizerContainer.style.cssText = `
display: flex;
align-items: center;
justify-content: center;
margin-${config.position === 'left' ? 'right' : 'left'}: 12px;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 4px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
`;
// Create canvas
canvas = document.createElement('canvas');
canvas.width = config.width;
canvas.height = config.height;
canvas.style.cssText = `
width: ${config.width}px;
height: ${config.height}px;
border-radius: 4px;
`;
visualizerContainer.appendChild(canvas);
canvasContext = canvas.getContext('2d');
// Insert visualizer next to search bar
if (config.position === 'left') {
searchContainer.parentElement?.insertBefore(visualizerContainer, searchContainer);
} else {
searchContainer.parentElement?.insertBefore(visualizerContainer, searchContainer.nextSibling);
}
if (!config.enabled) return;
ensureNavSlot();
ensureNpSlot();
};
// Remove visualizer UI
const removeVisualizerUI = (): void => {
if (visualizerContainer) {
visualizerContainer.remove();
visualizerContainer = null;
canvas = null;
canvasContext = null;
}
clearSlot(navSlot);
clearSlot(npSlot);
};
// Animation loop for rendering visualizer
const animate = (): void => {
if (!canvasContext || !canvas) {
animationId = null;
return;
}
// Re-attach slots that got disconnected from the DOM
createVisualizerUI();
// Update canvas color in case it changed
canvasContext.fillStyle = config.color;
canvasContext.strokeStyle = config.color;
const slots = [navSlot, npSlot].filter(s => s.ctx && s.canvas);
if (slots.length === 0) {
animationId = requestAnimationFrame(animate);
return;
}
// Check if we have real audio data - this might not be needed but its a good idea
let hasRealAudio = false;
if (analyser && dataArray) {
analyser.getByteFrequencyData(dataArray);
// Check if there's actual audio signal (not just silence)
const avgVolume = dataArray.reduce((sum, val) => sum + val, 0) / dataArray.length;
hasRealAudio = avgVolume > 5; // Threshold for detecting actual audio
}
let hasRealAudio = false;
if (analyser && dataArray) {
analyser.getByteFrequencyData(dataArray);
const avgVolume =
dataArray.reduce((sum, val) => sum + val, 0) / dataArray.length;
hasRealAudio = avgVolume > 5;
}
// Clear canvas
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
for (const slot of slots) {
const ctx = slot.ctx!;
const cvs = slot.canvas!;
ctx.fillStyle = config.color;
ctx.strokeStyle = config.color;
ctx.clearRect(0, 0, cvs.width, cvs.height);
if (hasRealAudio && analyser && dataArray) {
// Draw real audio visualization
switch (config.visualizerType) {
case 'bars': // Is implemented YAYYY (default)
drawBars();
break;
case 'waveform': // Not implemented yet
drawWaveform();
break;
case 'circular': // Not implemented yet
drawCircular();
break;
}
} else {
// Draw cool scrolling wave effect when no audio
drawScrollingWave();
}
if (hasRealAudio && analyser && dataArray) {
drawBars(ctx, cvs);
} else {
drawScrollingWave(ctx, cvs);
}
}
animationId = requestAnimationFrame(animate);
animationId = requestAnimationFrame(animate);
};
// Global wave animation state
let waveTime = 0;
// Helper function to draw rounded rectangles
const drawRoundedRect = (ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number): void => {
ctx.beginPath();
ctx.roundRect(x, y, width, height, radius);
ctx.fill();
const drawRoundedRect = (
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
radius: number,
): void => {
ctx.beginPath();
ctx.roundRect(x, y, width, height, radius);
ctx.fill();
};
// Draw scrolling wave effect when no audio is detected
const drawScrollingWave = (): void => {
if (!canvasContext || !canvas) return;
const drawScrollingWave = (ctx: CanvasRenderingContext2D, cvs: HTMLCanvasElement): void => {
waveTime += 0.05 / [navSlot, npSlot].filter(s => s.ctx).length;
waveTime += 0.05; // Speed of wave animation
const barCount = config.barCount;
const barWidth = cvs.width / barCount;
const maxHeight = cvs.height * 0.6;
const barCount = config.barCount;
const barWidth = canvas.width / barCount;
const maxHeight = canvas.height * 0.6;
ctx.fillStyle = config.color;
canvasContext.fillStyle = config.color;
for (let i = 0; i < barCount; i++) {
const x = i / barCount;
const wave1 = Math.sin(x * Math.PI * 2 + waveTime) * 0.3;
const wave2 = Math.sin(x * Math.PI * 4 + waveTime * 1.3) * 0.2;
const wave3 = Math.sin(x * Math.PI * 6 + waveTime * 0.7) * 0.1;
const combinedWave = (wave1 + wave2 + wave3 + 1) / 2;
const travelWave = Math.sin(x * Math.PI * 3 - waveTime * 2) * 0.5 + 0.5;
const barHeight = maxHeight * combinedWave * travelWave * 0.8 + 2;
for (let i = 0; i < barCount; i++) {
// Create a sine wave that scrolls back and forth
const x = i / barCount;
const wave1 = Math.sin(x * Math.PI * 2 + waveTime) * 0.3;
const wave2 = Math.sin(x * Math.PI * 4 + waveTime * 1.3) * 0.2;
const wave3 = Math.sin(x * Math.PI * 6 + waveTime * 0.7) * 0.1;
const xPos = i * barWidth;
const yPos = (cvs.height - barHeight) / 2;
// Combine waves for complex pattern
const combinedWave = (wave1 + wave2 + wave3 + 1) / 2; // Normalize to 0-1
// Add a traveling wave effect
const travelWave = Math.sin(x * Math.PI * 3 - waveTime * 2) * 0.5 + 0.5;
// Final height calculation
const barHeight = maxHeight * combinedWave * travelWave * 0.8 + 2; // Minimum height of 2px
const xPos = i * barWidth;
const yPos = (canvas.height - barHeight) / 2;
// Draw rounded or square bars based on setting
if (config.barRounding) {
drawRoundedRect(canvasContext, xPos, yPos, barWidth - 1, barHeight, 2);
} else {
canvasContext.fillRect(xPos, yPos, barWidth - 1, barHeight);
}
}
if (config.barRounding) {
drawRoundedRect(ctx, xPos, yPos, barWidth - 1, barHeight, 2);
} else {
ctx.fillRect(xPos, yPos, barWidth - 1, barHeight);
}
}
};
// Draw frequency bars - default
const drawBars = (): void => {
if (!canvasContext || !dataArray || !canvas) return;
const drawBars = (ctx: CanvasRenderingContext2D, cvs: HTMLCanvasElement): void => {
if (!dataArray) return;
const barWidth = canvas.width / config.barCount;
const heightScale = canvas.height / 255;
const barWidth = cvs.width / config.barCount;
const heightScale = cvs.height / 255;
canvasContext.fillStyle = config.color;
ctx.fillStyle = config.color;
for (let i = 0; i < config.barCount; i++) {
const dataIndex = Math.floor(i * (dataArray.length / config.barCount));
const barHeight = (dataArray[dataIndex] * config.sensitivity * heightScale);
for (let i = 0; i < config.barCount; i++) {
const dataIndex = Math.floor(i * (dataArray.length / config.barCount));
const barHeight = dataArray[dataIndex] * config.sensitivity * heightScale;
const x = i * barWidth;
const y = canvas.height - barHeight;
const x = i * barWidth;
const y = cvs.height - barHeight;
// Draw rounded or square bars based on setting
if (config.barRounding) {
drawRoundedRect(canvasContext, x, y, barWidth - 1, barHeight, 2);
} else {
canvasContext.fillRect(x, y, barWidth - 1, barHeight);
}
}
if (config.barRounding) {
drawRoundedRect(ctx, x, y, barWidth - 1, barHeight, 2);
} else {
ctx.fillRect(x, y, barWidth - 1, barHeight);
}
}
};
// Draw waveform visualization - NOT IMPLEMENTED YET
@@ -384,24 +390,24 @@ const drawBars = (): void => {
// }
// };
// Update visualizer settings
const updateAudioVisualizer = (): void => {
if (analyser) {
// use a fixed size that provides enough frequency bins
analyser.fftSize = 512; // Fixed power of 2 - important
analyser.smoothingTimeConstant = config.smoothing;
dataArray = new Uint8Array(analyser.frequencyBinCount);
}
if (analyser) {
analyser.fftSize = 512;
analyser.smoothingTimeConstant = config.smoothing;
dataArray = new Uint8Array(analyser.frequencyBinCount);
}
if (canvas) {
canvas.width = config.width;
canvas.height = config.height;
canvas.style.width = `${config.width}px`;
canvas.style.height = `${config.height}px`;
}
for (const slot of [navSlot, npSlot]) {
if (slot.canvas) {
slot.canvas.width = config.width;
slot.canvas.height = config.height;
slot.canvas.style.width = `${config.width}px`;
slot.canvas.style.height = `${config.height}px`;
}
}
// Recreate UI if position changed
createVisualizerUI();
removeVisualizerUI();
createVisualizerUI();
};
// Make updateAudioVisualizer available globally for settings
@@ -409,116 +415,117 @@ const updateAudioVisualizer = (): void => {
// Clean up function
const cleanupAudioVisualizer = (): void => {
// stop animation and hide UI - don't touch audio connections (otherwise it will reconnect)
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
// stop animation and hide UI - don't touch audio connections (otherwise it will reconnect)
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
removeVisualizerUI();
removeVisualizerUI();
// i was killing audio connections - But it was reconnecting and being a pain
// so i just left it alone - it works fine
// i was killing audio connections - But it was reconnecting and being a pain
// so i just left it alone - it works fine
};
// Initialize when DOM is ready and track is playing
const observePlayState = (): void => {
let hasTriedInitialization = false;
let checkCount = 0;
let hasTriedInitialization = false;
let checkCount = 0;
const checkAndInitialize = () => {
checkCount++;
const checkAndInitialize = () => {
checkCount++;
// Only try to initialize once when music starts playing
if (PlayState.playing && !hasTriedInitialization) {
hasTriedInitialization = true;
log("Initializing audio visualizer...");
// Only try to initialize once when music starts playing
if (PlayState.playing && !hasTriedInitialization) {
hasTriedInitialization = true;
log("Initializing audio visualizer...");
// Initialize immediately - no delay (after audio starts playing ofc)
initializeAudioVisualizer().then(() => {
if (audioContext && analyser) {
log("Audio visualizer ready!");
} else {
hasTriedInitialization = false; // Allow retry if failed
}
});
} else if (!PlayState.playing && hasTriedInitialization) {
// Reset try flag when music stops so it can try again next time (otherwise it explode)
hasTriedInitialization = false;
}
// Initialize immediately - no delay (after audio starts playing ofc)
initializeAudioVisualizer().then(() => {
if (audioContext && analyser) {
log("Audio visualizer ready!");
} else {
hasTriedInitialization = false; // Allow retry if failed
}
});
} else if (!PlayState.playing && hasTriedInitialization) {
// Reset try flag when music stops so it can try again next time (otherwise it explode)
hasTriedInitialization = false;
}
// Keep animation running regardless of play state
if (!animationId) {
animate();
}
};
// Keep animation running regardless of play state
if (!animationId) {
animate();
}
};
// Start with fast checking, then slow down
const fastInterval = setInterval(() => {
checkAndInitialize();
if (checkCount > 10) { // After 10 quick checks, switch to slower
clearInterval(fastInterval);
const slowInterval = setInterval(checkAndInitialize, 2000);
unloads.add(() => clearInterval(slowInterval));
}
}, 200); // Check every 200ms initially
// Start with fast checking, then slow down
const fastInterval = setInterval(() => {
checkAndInitialize();
if (checkCount > 10) {
// After 10 quick checks, switch to slower
clearInterval(fastInterval);
const slowInterval = setInterval(checkAndInitialize, 2000);
unloads.add(() => clearInterval(slowInterval));
}
}, 200); // Check every 200ms initially
unloads.add(() => clearInterval(fastInterval));
unloads.add(() => clearInterval(fastInterval));
// Immediate first check
checkAndInitialize();
// Immediate first check
checkAndInitialize();
};
// Initialize the plugin
const initialize = (): void => {
log("Audio Visualizer plugin initializing...");
log("Audio Visualizer plugin initializing...");
// Start immediately - DOM should be ready by plugin load
setTimeout(() => {
log("Starting visualizer...");
// Create UI immediately so wave effect shows
createVisualizerUI();
// Start animation loop immediately
animate();
// Also observe play state for audio detection
observePlayState();
}, 100); // Minimal delay to ensure DOM is ready
// Start immediately - DOM should be ready by plugin load
setTimeout(() => {
log("Starting visualizer...");
// Create UI immediately so wave effect shows
createVisualizerUI();
// Start animation loop immediately
animate();
// Also observe play state for audio detection
observePlayState();
}, 100); // Minimal delay to ensure DOM is ready
};
// Complete cleanup function for plugin unload
const completeCleanup = (): void => {
log("Complete cleanup - plugin unloading");
log("Complete cleanup - plugin unloading");
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
removeVisualizerUI();
removeVisualizerUI();
// Fully disconnect and reset everything
if (audioSource) {
try {
audioSource.disconnect();
log("Disconnected audio source completely");
} catch (e) {
log("Audio source already disconnected");
}
}
// Fully disconnect and reset everything
if (audioSource) {
try {
audioSource.disconnect();
log("Disconnected audio source completely");
} catch (e) {
log("Audio source already disconnected");
}
}
// Close audio context completely on plugin unload
if (audioContext && audioContext.state !== 'closed') {
audioContext.close();
log("Closed AudioContext");
}
// Close audio context completely on plugin unload
if (audioContext && audioContext.state !== "closed") {
audioContext.close();
log("Closed AudioContext");
}
// Reset all references
audioContext = null;
analyser = null;
audioSource = null;
dataArray = null;
currentAudioElement = null;
isSourceConnected = false;
// Reset all references
audioContext = null;
analyser = null;
audioSource = null;
dataArray = null;
currentAudioElement = null;
isSourceConnected = false;
};
// Register cleanup
+33 -39
View File
@@ -1,56 +1,50 @@
/* Audio Visualizer CSS - Only applies to the Visualizer */
/* Audio Visualizer CSS */
#audio-visualizer-container {
transition: all 0.3s ease-in-out;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
.audio-visualizer-container {
transition: all 0.3s ease-in-out;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
animation: av-fadeIn 0.5s ease-out;
}
#audio-visualizer-container:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
.audio-visualizer-container:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
#audio-visualizer-container canvas {
display: block;
transition: all 0.3s ease-in-out;
.audio-visualizer-container canvas {
display: block;
transition: all 0.3s ease-in-out;
}
/* Responsive adjustments */
@media (max-width: 768px) {
#audio-visualizer-container {
margin: 4px;
padding: 2px;
}
.audio-visualizer-container {
margin: 4px;
padding: 2px;
}
#audio-visualizer-container canvas {
max-width: 150px;
max-height: 30px;
}
.audio-visualizer-container canvas {
max-width: 150px;
max-height: 30px;
}
}
/* Where to put the thingy */
[class*="_searchField"] {
transition: all 0.3s ease-in-out;
.audio-visualizer-container.active {
box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
}
/* Shadow when active - doesnt seem to only apply when active but thats better */
#audio-visualizer-container.active {
box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
@keyframes av-fadeIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Fade in animation */
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
#audio-visualizer-container {
animation: fadeIn 0.5s ease-out;
[data-type="search-field"] {
min-width: 220px !important;
}
@@ -1,6 +1,6 @@
{
"name": "@meowarex/oled-theme",
"description": "A dark OLED-friendly theme plugin that transforms Tidal Luna's appearance.",
"name": "@meowarex/colorama-lyrics",
"description": "Customize lyrics colors: single, gradient & auto from cover art",
"author": {
"name": "meowarex",
"url": "https://github.com/meowarex",
@@ -0,0 +1,378 @@
import { ReactiveStore } from "@luna/core";
import { LunaSettings, LunaSwitchSetting } from "@luna/ui";
import React from "react";
declare global {
interface Window {
applyColoramaLyrics?: () => void;
}
}
type SwitchChangeHandler = (
event: React.ChangeEvent<HTMLInputElement> | null,
checked: boolean,
) => void;
export const settings = await ReactiveStore.getPluginStorage("ColoramaLyrics", {
enabled: true,
singleColor: "#FFFFFF",
singleAlpha: 100,
customColors: [] as string[],
excludeInactive: false,
});
export const Settings = () => {
const [singleColor, setSingleColor] = React.useState(settings.singleColor);
const [singleAlpha, setSingleAlpha] = React.useState<number>(
settings.singleAlpha ?? 100,
);
const [customInput, setCustomInput] = React.useState(settings.singleColor);
const [customColors, setCustomColors] = React.useState(settings.customColors);
const [showPicker, setShowPicker] = React.useState(false);
const [isAnimatingIn, setIsAnimatingIn] = React.useState(false);
const [shouldRender, setShouldRender] = React.useState(false);
const [excludeInactive, setExcludeInactive] = React.useState(
settings.excludeInactive,
);
const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType<{
title: string;
desc?: string;
checked: boolean;
onChange: SwitchChangeHandler;
}>;
const normalizeToRGB = (
hex: string,
fallback: string = "#FFFFFF",
): string => {
let v = hex.trim().toLowerCase();
if (!v.startsWith("#")) v = `#${v}`;
if (/^#([0-9a-f]{3,4})$/.test(v)) {
const m = v.slice(1);
const r = m[0];
const g = m[1];
const b = m[2];
return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
}
if (/^#([0-9a-f]{8})$/.test(v)) {
const rrggbb = v.slice(3);
return `#${rrggbb}`.toUpperCase();
}
if (/^#([0-9a-f]{6})$/.test(v)) return v.toUpperCase();
return fallback;
};
const colorPresets = [
"#FFFFFF",
"#FF0000",
"#00FF00",
"#0000FF",
"#FFFF00",
"#FF00FF",
"#00FFFF",
"#FF8800",
"#8800FF",
"#0088FF",
"#88FF00",
"#FF0088",
"#00FF88",
"#444444",
"#888888",
"#CCCCCC",
"#1DB954",
"#E22134",
"#1976D2",
];
const openPicker = () => {
setShowPicker(true);
setShouldRender(true);
setTimeout(() => setIsAnimatingIn(true), 10);
};
const closePicker = () => {
setIsAnimatingIn(false);
setTimeout(() => {
setShowPicker(false);
setShouldRender(false);
}, 200);
};
const hexColorRegex = /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3,4})$/i;
const applyCustomInputColor = (raw: string, updateInput: boolean): void => {
const trimmed = raw.trim();
if (!hexColorRegex.test(trimmed)) return;
const next = normalizeToRGB(trimmed);
settings.singleColor = next;
setSingleColor(next);
if (updateInput) setCustomInput(next);
requestApply();
};
const addCustomColor = () => {
const trimmed = customInput.trim();
if (
hexColorRegex.test(trimmed) &&
!colorPresets.includes(trimmed) &&
!customColors.includes(normalizeToRGB(trimmed))
) {
const updated = [...customColors, normalizeToRGB(trimmed)];
setCustomColors(updated);
settings.customColors = updated;
}
};
const allColors = [...colorPresets, ...customColors];
const requestApply = () => {
window.applyColoramaLyrics?.();
};
return (
<LunaSettings>
{/* Single color picker button */}
<div
style={{
padding: "8px 0",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div>
<div
style={{
fontWeight: "normal",
fontSize: "1.075rem",
marginBottom: 4,
}}
>
Lyrics Color
</div>
<div style={{ opacity: 0.7, fontSize: 14 }}>Set lyrics color</div>
</div>
<div
style={{
display: "flex",
gap: 8,
alignItems: "center",
position: "relative",
}}
>
<button
type="button"
onClick={() => (showPicker ? closePicker() : openPicker())}
style={{
width: 32,
height: 32,
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: 6,
cursor: "pointer",
background: normalizeToRGB(singleColor),
}}
/>
</div>
</div>
{/* Color picker modal */}
{shouldRender && (
<>
<button
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0,0,0,0.6)",
zIndex: 1000,
opacity: isAnimatingIn ? 1 : 0,
transition: "opacity 0.2s ease",
}}
type="button"
aria-label="Close color picker"
onClick={closePicker}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === "Escape") closePicker();
}}
/>
<div
style={{
position: "fixed",
top: "50%",
left: "50%",
background: "rgba(20,20,20,0.98)",
backdropFilter: "blur(20px)",
WebkitBackdropFilter: "blur(20px)",
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: 16,
padding: 20,
minWidth: 320,
maxWidth: "90vw",
maxHeight: "90vh",
zIndex: 1001,
boxShadow: "0 20px 40px rgba(0,0,0,0.7)",
opacity: isAnimatingIn ? 1 : 0,
transform: isAnimatingIn
? "translate(-50%, -50%) scale(1)"
: "translate(-50%, -50%) scale(0.9)",
transition: "all 0.2s ease",
}}
>
<div
style={{
marginBottom: 12,
color: "#fff",
fontWeight: "bold",
fontSize: 14,
}}
>
Lyrics Color
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: 8,
marginBottom: 16,
}}
>
{allColors.map((color) => (
<button
key={color}
type="button"
onClick={() => {
const next = normalizeToRGB(color);
settings.singleColor = next;
setSingleColor(next);
setCustomInput(next);
requestApply();
}}
style={{
width: 32,
height: 32,
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.2)",
background: normalizeToRGB(color),
cursor: "pointer",
}}
/>
))}
</div>
<div style={{ marginBottom: 12 }}>
<div
style={{
color: "rgba(255,255,255,0.7)",
fontSize: 12,
marginBottom: 6,
}}
>
Custom Hex (#RRGGBB)
</div>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<input
type="text"
value={customInput}
onChange={(e) => setCustomInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
applyCustomInputColor(customInput, true);
addCustomColor();
}
}}
placeholder="#RRGGBB"
style={{
flex: 1,
padding: "8px 12px",
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.1)",
color: "#fff",
fontSize: 14,
fontFamily: "monospace",
boxSizing: "border-box",
}}
/>
<button
onClick={() => {
applyCustomInputColor(customInput, false);
addCustomColor();
}}
style={{
width: 32,
height: 32,
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.3)",
background: "rgba(255,255,255,0.15)",
color: "#fff",
cursor: "pointer",
fontSize: 16,
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.2s ease",
}}
type="button"
>
+
</button>
</div>
</div>
<div style={{ marginBottom: 16 }}>
<div
style={{
color: "rgba(255,255,255,0.8)",
fontSize: 12,
marginBottom: 6,
}}
>
Alpha
</div>
<input
type="range"
min={5}
max={100}
step={1}
value={singleAlpha}
onChange={(e) => {
const value = Number(e.target.value);
settings.singleAlpha = value;
setSingleAlpha(value);
requestApply();
}}
style={{ width: "100%" }}
/>
</div>
<button
onClick={closePicker}
style={{
width: "100%",
padding: 8,
borderRadius: 6,
border: "1px solid rgba(255,255,255,0.2)",
background: "rgba(255,255,255,0.1)",
color: "#fff",
cursor: "pointer",
fontSize: 12,
}}
type="button"
>
Done
</button>
</div>
</>
)}
<AnySwitch
title="Exclude Inactive"
desc="Apply color only to the currently active lyric line"
checked={excludeInactive}
onChange={(_event: React.ChangeEvent<HTMLInputElement> | null, checked: boolean) => {
settings.excludeInactive = checked;
setExcludeInactive(checked);
requestApply();
}}
/>
</LunaSettings>
);
};
+99
View File
@@ -0,0 +1,99 @@
import { LunaUnload, Tracer } from "@luna/core";
import { StyleTag } from "@luna/lib";
import { settings, Settings } from "./Settings";
import styles from "file://styles.css?minify";
export const { trace } = Tracer("[Colorama Lyrics]");
export { Settings };
export const unloads = new Set<LunaUnload>();
new StyleTag("ColoramaLyrics", unloads, styles);
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
let v = hex.trim();
if (!v.startsWith("#")) v = `#${v}`;
if (/^#([0-9a-fA-F]{3})$/.test(v)) {
const r = parseInt(v[1] + v[1], 16);
const g = parseInt(v[2] + v[2], 16);
const b = parseInt(v[3] + v[3], 16);
return { r, g, b };
}
if (/^#([0-9a-fA-F]{6})$/.test(v)) {
const r = parseInt(v.slice(1, 3), 16);
const g = parseInt(v.slice(3, 5), 16);
const b = parseInt(v.slice(5, 7), 16);
return { r, g, b };
}
if (/^#([0-9a-fA-F]{8})$/.test(v)) {
const r = parseInt(v.slice(3, 5), 16);
const g = parseInt(v.slice(5, 7), 16);
const b = parseInt(v.slice(7, 9), 16);
return { r, g, b };
}
return null;
}
function rgbaFromHexAndAlpha(
hex: string,
alphaPercent: number | undefined,
): string {
const rgb = hexToRgb(hex);
const a = Math.max(0.05, Math.min(100, alphaPercent ?? 100)) / 100;
if (!rgb) return `rgba(255,255,255,${a})`;
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`;
}
function applySingleColor(color: string) {
const alpha = (settings as any).singleAlpha ?? 100;
const rgba = rgbaFromHexAndAlpha(color, alpha);
document.documentElement.style.setProperty("--cl-lyrics-color", rgba);
document.documentElement.style.setProperty("--cl-glow1", rgba);
document.documentElement.style.setProperty("--cl-glow2", rgba);
document.body.classList.add("colorama-single");
}
function applyColoramaLyrics(): void {
if (!settings.enabled) {
document.body.classList.remove("colorama-single");
return;
}
if (settings.excludeInactive) {
document.body.classList.add("colorama-only-active");
} else {
document.body.classList.remove("colorama-only-active");
}
applySingleColor(settings.singleColor);
}
(window as any).applyColoramaLyrics = applyColoramaLyrics;
setTimeout(() => applyColoramaLyrics(), 200);
function hookRadiantUpdates(): void {
const w = window as any;
const wrap = (name: string) => {
const fn = w[name];
if (typeof fn === "function" && !fn.__coloramaPatched) {
const orig = fn.bind(w);
const patched = (...args: unknown[]) => {
const result = orig(...args);
try {
applyColoramaLyrics();
} catch {}
return result;
};
(patched as any).__coloramaPatched = true;
w[name] = patched;
}
};
wrap("updateRadiantLyricsStyles");
wrap("updateRadiantLyricsNowPlayingBackground");
wrap("updateRadiantLyricsGlobalBackground");
wrap("updateRadiantLyricsTextGlow");
}
setTimeout(() => hookRadiantUpdates(), 0);
+117
View File
@@ -0,0 +1,117 @@
/* Variables used by Colorama Lyrics */
:root {
--cl-lyrics-color: #ffffff;
--cl-glow1: #ffffff;
--cl-glow2: #ffffff;
}
/* Apply solid color to lyrics text */
.colorama-single [class*="_lyricsText"] > div > span,
.colorama-single [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-single [class^="_lyricsContainer"] > div > div > span,
.colorama-single
[class^="_lyricsContainer"]
> div
> div
> span[data-current="true"] {
color: var(--cl-lyrics-color) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
}
/* Color Radiant glow shadows using Colorama colors (respect RL sizes) */
.colorama-single [class*="_lyricsText"] > div > span[data-current="true"],
.colorama-single
[class^="_lyricsContainer"]
> div
> div
> span[data-current="true"] {
text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
}
/* Hover: force glow color to match Colorama settings for inactive lines */
.colorama-single [class*="_lyricsText"] > div > span:hover,
.colorama-single [class^="_lyricsContainer"] > div > div > span:hover {
color: var(--cl-lyrics-color) !important;
text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
}
/* MARKER: Radiant WBW Lyrics Support */
/* Single color: active wbw words & syllable finished */
.colorama-single .rl-wbw-word.rl-wbw-active,
.colorama-single .rl-wbw-word.rl-syl-finished {
color: var(--cl-lyrics-color) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
}
/* Single color: glow on active wbw words */
.colorama-single .rl-wbw-word.rl-wbw-active {
text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
}
/* Hover: wbw words pick up Colorama colors */
.colorama-single .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover,
.colorama-single .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word.rl-wbw-word-hover {
color: var(--cl-lyrics-color) !important;
text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
}
/* Only-active: wbw words on inactive lines stay default */
body.colorama-only-active.colorama-single
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word {
color: rgba(128, 128, 128, 0.4) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
}
/* Only-active: hover on inactive wbw lines keeps default */
body.colorama-only-active.colorama-single
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover {
color: lightgray !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
}
/* Only color active line mode */
body.colorama-only-active.colorama-single [class*="_lyricsText"]
> div
> span:not([data-current="true"]) {
color: rgba(128, 128, 128, 0.4) !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
}
/* In only-active mode, keep TIDAL defaults even on hover for inactive lines */
body.colorama-only-active.colorama-single [class*="_lyricsText"]
> div
> span:not([data-current="true"]):hover {
color: lightgray !important;
background: none !important;
-webkit-background-clip: initial !important;
background-clip: initial !important;
-webkit-text-fill-color: initial !important;
text-shadow: initial !important;
}
+107 -82
View File
@@ -1,4 +1,4 @@
import { LunaUnload, Tracer } from "@luna/core";
import { type LunaUnload, Tracer } from "@luna/core";
import { StyleTag } from "@luna/lib";
// Import CSS directly using Luna's file:// syntax - Took me a while to figure out <3
@@ -9,110 +9,135 @@ export const { trace } = Tracer("[Copy Lyrics]");
// clean up resources
export const unloads = new Set<LunaUnload>();
// StyleTag for lyrics selection styling
const lyricsStyleTag = new StyleTag("Copy-Lyrics", unloads, unlockSelection);
// Style injection via side effect
new StyleTag("Copy-Lyrics", unloads, unlockSelection);
function SetClipboard(text: string): void {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed"; // Avoid scrolling to bottom
document.body.appendChild(textarea);
textarea.select();
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed"; // Avoid scrolling to bottom
document.body.appendChild(textarea);
textarea.select();
try {
const success = document.execCommand("copy");
if (!success) throw new Error("Failed to copy text.");
} catch (err) {
trace.msg.err(err instanceof Error ? err.message : String(err));
} finally {
document.body.removeChild(textarea);
}
try {
const success = document.execCommand("copy");
if (!success) throw new Error("Failed to copy text.");
} catch (err) {
trace.msg.err(err instanceof Error ? err.message : String(err));
} finally {
document.body.removeChild(textarea);
}
}
let isSelecting = false;
const onMouseDown = function (): void {
isSelecting = true;
const onMouseDown = (): void => {
isSelecting = true;
};
const onMouseUp = function (event: MouseEvent): void {
if (isSelecting) {
const selection = window.getSelection();
if (selection && selection.toString().length > 0) {
const selectedSpans: HTMLSpanElement[] = [];
const range = selection.getRangeAt(0);
let container = range.commonAncestorContainer;
const onMouseUp = (): void => {
if (isSelecting) {
const selection = window.getSelection();
if (selection?.toString().length > 0) {
const selectedSpans: HTMLSpanElement[] = [];
const range = selection.getRangeAt(0);
let container: Node | null = range.commonAncestorContainer;
// If the container is NOT an element and a document, adjust it.
if (
container.nodeType !== Node.ELEMENT_NODE &&
container.nodeType !== Node.DOCUMENT_NODE
) {
// Get the parent element if it's a text node
const parentElement = container.parentElement;
if (parentElement && parentElement.hasAttribute("data-current")) {
let text_ = selection.toString().trim();
SetClipboard(text_);
trace.msg.log("Copied to clipboard!");
return;
}
}
// Normalize container: if it's a text node, use its parent element/node
if (container && container.nodeType === Node.TEXT_NODE) {
container = (container.parentElement ?? container.parentNode) as Node | null;
}
// Get all the spans inside the container.
const spans = (container as Element).getElementsByTagName("span");
for (let span of spans) {
if (selection.containsNode(span, true)) {
selectedSpans.push(span as HTMLSpanElement);
}
}
// If parent has data-current, treat as single-line copy case
if (
container &&
container.nodeType === Node.ELEMENT_NODE &&
(container as Element).hasAttribute("data-current")
) {
const text_ = selection.toString().trim();
SetClipboard(text_);
trace.msg.log("Copied to clipboard!");
return;
}
// Concat the text of the selected spans.
let hasCorrectAttribute = false;
let text = "";
selectedSpans.forEach((span) => {
if (span.hasAttribute("data-current")) {
hasCorrectAttribute = true;
text += span.textContent + "\n";
if ([...span.classList].some((className) => className.startsWith("endOfStanza--"))) {
text += "\n";
}
}
});
// Ensure we have an Element or Document before querying
if (
!container ||
(container.nodeType !== Node.ELEMENT_NODE &&
container.nodeType !== Node.DOCUMENT_NODE)
) {
isSelecting = false;
return;
}
text = text.trim();
// Get all the spans inside the container.
const spans = (container as Element | Document).getElementsByTagName(
"span",
);
for (const span of spans) {
if (selection.containsNode(span, true)) {
selectedSpans.push(span as HTMLSpanElement);
}
}
if (hasCorrectAttribute) {
SetClipboard(text);
trace.msg.log("Copied to clipboard!");
selection.removeAllRanges();
}
}
isSelecting = false;
}
// Concat the text of the selected spans.
let hasCorrectAttribute = false;
let text = "";
selectedSpans.forEach((span) => {
if (span.hasAttribute("data-current")) {
hasCorrectAttribute = true;
text += span.textContent + "\n";
if (
[...span.classList].some((className) =>
className.startsWith("endOfStanza--"),
)
) {
text += "\n";
}
}
});
text = text.trim();
if (hasCorrectAttribute) {
SetClipboard(text);
trace.msg.log("Copied to clipboard!");
selection.removeAllRanges();
}
}
isSelecting = false;
}
};
const onClickHooked = function (event: MouseEvent): boolean | void {
if (!isSelecting) return;
const onClickHooked = (event: MouseEvent): boolean | undefined => {
if (!isSelecting) return;
const target = event.target as HTMLElement;
if (target.tagName.toLowerCase() === "span" && target.hasAttribute("data-current")) {
// Prevent default behavior and stop event propagation
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
return false;
}
const target = event.target as HTMLElement;
if (
target.tagName.toLowerCase() === "span" &&
target.hasAttribute("data-current")
) {
// Prevent default behavior and stop event propagation
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
return false;
}
return undefined;
};
// Add event listener with capture phase to intercept events before they reach other handlers
document.addEventListener("click", onClickHooked, true);
document.addEventListener("mousedown", onMouseDown);
document.addEventListener("mouseup", onMouseUp);
// Add cleanup to unloads
unloads.add(() => {
// Remove event listeners
document.removeEventListener("click", onClickHooked, true);
document.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mouseup", onMouseUp);
unloads.add((): void => {
// Remove event listeners
document.removeEventListener("click", onClickHooked, true);
document.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mouseup", onMouseUp);
});
+5 -5
View File
@@ -1,9 +1,9 @@
[class^="_lyricsText"]>div>span {
user-select: text;
cursor: text;
[class^="_lyricsText"] > div > span {
user-select: text;
cursor: text;
}
::selection {
background: rgb(72, 0, 60);
color: rgb(255, 255, 255);
background: rgb(72, 0, 60);
color: rgb(255, 255, 255);
}
+1 -1
View File
@@ -9,7 +9,7 @@ export const settings = await ReactiveStore.getPluginStorage("ElementHider", {
className: string;
textContent: string;
timestamp: number;
}>
}>,
});
export const Settings = () => {
+193 -126
View File
@@ -1,5 +1,5 @@
import { LunaUnload, Tracer } from "@luna/core";
import { StyleTag, ContextMenu } from "@luna/lib";
import { type LunaUnload, Tracer } from "@luna/core";
import { StyleTag } from "@luna/lib";
import { settings, Settings } from "./Settings";
// Import CSS directly using Luna's file:// syntax
@@ -13,8 +13,8 @@ export { Settings };
// Clean up resources
export const unloads = new Set<LunaUnload>();
// StyleTag for element hider
const styleTag = new StyleTag("Element-Hider", unloads, styles);
// StyleTag for element hider (side-effect)
new StyleTag("Element-Hider", unloads, styles);
// State management
let targetElement: HTMLElement | null = null;
@@ -32,7 +32,7 @@ function generateElementSelector(element: HTMLElement): string {
}
// Priority 2: data-test attribute (very specific for Tidal <3)
const dataTest = element.getAttribute('data-test');
const dataTest = element.getAttribute("data-test");
if (dataTest) {
return `[data-test="${dataTest}"]`;
}
@@ -41,28 +41,43 @@ function generateElementSelector(element: HTMLElement): string {
let selector = element.tagName.toLowerCase();
// Get filtered classes (exclude our temporary classes)
const classes = element.className ? element.className.trim().split(/\s+/).filter(cls => {
return cls.length > 0 &&
!cls.startsWith('element-hider-') &&
cls !== 'element-hider-target' &&
cls !== 'element-hider-hiding' &&
cls !== 'element-hider-hidden';
}) : [];
const classes = element.className
? element.className
.trim()
.split(/\s+/)
.filter((cls) => {
return (
cls.length > 0 &&
!cls.startsWith("element-hider-") &&
cls !== "element-hider-target" &&
cls !== "element-hider-hiding" &&
cls !== "element-hider-hidden"
);
})
: [];
// Only use classes if we have them and they're not generic and dumb
if (classes.length > 0) {
// Use ALL classes to be very specific
selector += '.' + classes.join('.');
selector += "." + classes.join(".");
// Add parent context for extra specificity (for when the element is inside another element)
const parent = element.parentElement;
if (parent && parent.tagName !== 'BODY' && parent.tagName !== 'HTML') {
const parentClasses = parent.className ? parent.className.trim().split(/\s+/).filter(cls => {
return cls.length > 0 && !cls.startsWith('element-hider-');
}) : [];
if (parent && parent.tagName !== "BODY" && parent.tagName !== "HTML") {
const parentClasses = parent.className
? parent.className
.trim()
.split(/\s+/)
.filter((cls) => {
return cls.length > 0 && !cls.startsWith("element-hider-");
})
: [];
if (parentClasses.length > 0) {
const parentSelector = parent.tagName.toLowerCase() + '.' + parentClasses.slice(0, 2).join('.');
const parentSelector =
parent.tagName.toLowerCase() +
"." +
parentClasses.slice(0, 2).join(".");
selector = `${parentSelector} > ${selector}`;
}
}
@@ -70,19 +85,29 @@ function generateElementSelector(element: HTMLElement): string {
// If no useful classes, use position-based selector with parent context
const parent = element.parentElement;
if (parent) {
const siblings = Array.from(parent.children).filter(child => child.tagName === element.tagName);
const siblings = Array.from(parent.children).filter(
(child) => child.tagName === element.tagName,
);
const index = siblings.indexOf(element);
if (index >= 0) {
selector += `:nth-of-type(${index + 1})`;
// Add parent context
if (parent.tagName !== 'BODY' && parent.tagName !== 'HTML') {
const parentClasses = parent.className ? parent.className.trim().split(/\s+/).filter(cls => {
return cls.length > 0 && !cls.startsWith('element-hider-');
}) : [];
if (parent.tagName !== "BODY" && parent.tagName !== "HTML") {
const parentClasses = parent.className
? parent.className
.trim()
.split(/\s+/)
.filter((cls) => {
return cls.length > 0 && !cls.startsWith("element-hider-");
})
: [];
if (parentClasses.length > 0) {
const parentSelector = parent.tagName.toLowerCase() + '.' + parentClasses.slice(0, 2).join('.');
const parentSelector =
parent.tagName.toLowerCase() +
"." +
parentClasses.slice(0, 2).join(".");
selector = `${parentSelector} > ${selector}`;
}
}
@@ -100,14 +125,14 @@ function saveHiddenElement(element: HTMLElement): void {
const elementInfo = {
selector: selector,
tagName: element.tagName,
className: element.className || '',
textContent: element.textContent?.substring(0, 100) || '',
timestamp: Date.now()
className: element.className || "",
textContent: element.textContent?.substring(0, 100) || "",
timestamp: Date.now(),
};
// Check if element is already saved
const existingIndex = settings.hiddenElements.findIndex(
stored => stored.selector === elementInfo.selector
(stored) => stored.selector === elementInfo.selector,
);
if (existingIndex === -1) {
@@ -119,17 +144,18 @@ function saveHiddenElement(element: HTMLElement): void {
}
}
// Remove hidden element from persistent storage (for unhiding)
function removeSavedElement(element: HTMLElement): void {
const selector = generateElementSelector(element);
const index = settings.hiddenElements.findIndex(stored => stored.selector === selector);
if (index !== -1) {
settings.hiddenElements.splice(index, 1);
trace.log(`Permanently removed: ${selector}`);
trace.log(`Remaining stored: ${settings.hiddenElements.length}`);
}
}
// Remove hidden element from persistent storage (for unhiding) - currently unused
// function removeSavedElement(element: HTMLElement): void {
// const selector = generateElementSelector(element);
// const index = settings.hiddenElements.findIndex(
// (stored) => stored.selector === selector,
// );
// if (index !== -1) {
// settings.hiddenElements.splice(index, 1);
// trace.log(`Permanently removed: ${selector}`);
// trace.log(`Remaining stored: ${settings.hiddenElements.length}`);
// }
// }
// Check if an element matches any stored selector (EXACT match only)
function matchesStoredSelector(element: HTMLElement): boolean {
@@ -154,14 +180,18 @@ function hideElementDirectly(element: HTMLElement): void {
element.classList.add("element-hider-hidden");
hiddenElements.add(element);
hiddenElementsArray.push(element);
trace.log(`Hidden element: ${element.tagName}${element.className ? '.' + element.className.split(' ')[0] : ''}`);
trace.log(
`Hidden element: ${element.tagName}${element.className ? "." + element.className.split(" ")[0] : ""}`,
);
}
// Hide the target element with animation
function hideTargetElement(): void {
if (!targetElement) return;
trace.log(`Hiding with animation: ${targetElement.tagName}${targetElement.className ? '.' + targetElement.className.split(' ')[0] : ''}`);
trace.log(
`Hiding with animation: ${targetElement.tagName}${targetElement.className ? "." + targetElement.className.split(" ")[0] : ""}`,
);
// Add hiding animation class
targetElement.classList.add("element-hider-hiding");
@@ -175,7 +205,10 @@ function hideTargetElement(): void {
// Wait for animation to complete, then hide
setTimeout(() => {
elementToHide.classList.add("element-hider-hidden");
elementToHide.classList.remove("element-hider-hiding", "element-hider-target");
elementToHide.classList.remove(
"element-hider-hiding",
"element-hider-target",
);
hiddenElements.add(elementToHide);
hiddenElementsArray.push(elementToHide);
}, 300);
@@ -186,10 +219,12 @@ function hideTargetElement(): void {
// Unhide all elements permanently (remove from storage)
function unhideAllElements(): void {
trace.log(`Permanently unhiding ${settings.hiddenElements.length} saved elements`);
trace.log(
`Permanently unhiding ${settings.hiddenElements.length} saved elements`,
);
// Show all currently hidden elements
hiddenElementsArray.forEach(element => {
hiddenElementsArray.forEach((element) => {
if (document.body.contains(element)) {
element.classList.remove("element-hider-hidden", "element-hider-hiding");
}
@@ -205,7 +240,9 @@ function unhideAllElements(): void {
function processAllElements(): void {
if (settings.hiddenElements.length === 0) return;
trace.log(`Scanning document for ${settings.hiddenElements.length} stored selectors`);
trace.log(
`Scanning document for ${settings.hiddenElements.length} stored selectors`,
);
let hiddenCount = 0;
// Use querySelectorAll for each stored selector with validation
@@ -217,7 +254,9 @@ function processAllElements(): void {
// Limit to prevent over-hiding (safety check)
if (elements.length > 10) {
trace.warn(`Selector too broad (${elements.length} matches), skipping: ${storedElement.selector}`);
trace.warn(
`Selector too broad (${elements.length} matches), skipping: ${storedElement.selector}`,
);
return;
}
@@ -226,7 +265,9 @@ function processAllElements(): void {
if (!hiddenElements.has(htmlElement)) {
hideElementDirectly(htmlElement);
hiddenCount++;
trace.log(`Hid element ${elemIndex + 1}/${elements.length} for selector ${index + 1}`);
trace.log(
`Hid element ${elemIndex + 1}/${elements.length} for selector ${index + 1}`,
);
}
});
} catch (error) {
@@ -241,7 +282,7 @@ function processAllElements(): void {
// Process new elements that are added to the DOM
function processNewElements(addedNodes: NodeList): void {
addedNodes.forEach(node => {
addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
const element = node as HTMLElement;
@@ -252,8 +293,8 @@ function processNewElements(addedNodes: NodeList): void {
}
// Check all descendant elements
const descendants = element.querySelectorAll('*');
descendants.forEach(descendant => {
const descendants = element.querySelectorAll("*");
descendants.forEach((descendant) => {
if (matchesStoredSelector(descendant as HTMLElement)) {
hideElementDirectly(descendant as HTMLElement);
}
@@ -267,7 +308,7 @@ function setupElementObserver(): void {
elementObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
processNewElements(mutation.addedNodes);
}
});
@@ -275,15 +316,22 @@ function setupElementObserver(): void {
elementObserver.observe(document.body, {
childList: true,
subtree: true
subtree: true,
});
trace.log(`Set up reactive element observer`);
}
// Global functions
(window as any).showAllElementsFromSettings = unhideAllElements;
(window as any).debugElementHider = () => {
declare global {
interface Window {
showAllElementsFromSettings?: () => void;
debugElementHider?: () => void;
}
}
window.showAllElementsFromSettings = unhideAllElements;
window.debugElementHider = () => {
trace.log(`=== Element Hider Debug Info ===`);
trace.log(`Stored elements: ${settings.hiddenElements.length}`);
trace.log(`Currently hidden elements: ${hiddenElementsArray.length}`);
@@ -297,19 +345,19 @@ function setupElementObserver(): void {
// Handle highlighting target element
function highlightElement(element: HTMLElement): void {
// Remove previous highlights
document.querySelectorAll('.element-hider-target').forEach(el => {
el.classList.remove('element-hider-target');
document.querySelectorAll(".element-hider-target").forEach((el) => {
el.classList.remove("element-hider-target");
});
// Highlight current element
element.classList.add('element-hider-target');
element.classList.add("element-hider-target");
targetElement = element;
}
// Remove highlight
function removeHighlight(): void {
if (targetElement) {
targetElement.classList.remove('element-hider-target');
targetElement.classList.remove("element-hider-target");
targetElement = null;
}
}
@@ -321,59 +369,70 @@ let contextMenuTimeout: number | null = null;
let waitingForBuiltInMenu = false;
// Listen for right-click events to capture the target for context menu
document.addEventListener('contextmenu', (event: MouseEvent) => {
const target = event.target as HTMLElement;
document.addEventListener(
"contextmenu",
(event: MouseEvent) => {
const target = event.target as HTMLElement;
// Don't interfere with native context menus on inputs, textareas, etc.
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
currentContextElement = null;
return;
}
// Don't show menu on our own custom menu
if (target.closest(".element-hider-custom-menu")) {
return;
}
// Close any existing custom menu
closeCustomMenu();
// Store the right-clicked element for context menu
currentContextElement = target;
waitingForBuiltInMenu = true;
// Store event coordinates for potential custom menu
const eventX = event.clientX;
const eventY = event.clientY;
// Prevent default immediately if we plan to handle it
event.preventDefault();
// Wait to see if the built-in context menu appears
contextMenuTimeout = window.setTimeout(() => {
// If we're still waiting and no built-in menu appeared, show our custom menu
if (waitingForBuiltInMenu && currentContextElement) {
showCustomMenu(eventX, eventY);
// Don't interfere with native context menus on inputs, textareas, etc.
if (
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable
) {
currentContextElement = null;
return;
}
waitingForBuiltInMenu = false;
}, 150); // Wait 150ms for built-in menu
// Don't prevent default initially - let Luna try to handle the context menu
}, true);
// Don't show menu on our own custom menu
if (target.closest(".element-hider-custom-menu")) {
return;
}
// Close any existing custom menu
closeCustomMenu();
// Store the right-clicked element for context menu
currentContextElement = target;
waitingForBuiltInMenu = true;
// Store event coordinates for potential custom menu
const eventX = event.clientX;
const eventY = event.clientY;
// 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
document.addEventListener('click', (event: MouseEvent) => {
const target = event.target as HTMLElement;
document.addEventListener(
"click",
(event: MouseEvent) => {
const target = event.target as HTMLElement;
// If clicking outside our custom menu, close it
if (customMenu && !target.closest(".element-hider-custom-menu")) {
closeCustomMenu();
removeHighlight();
}
}, true);
// If clicking outside our custom menu, close it
if (customMenu && !target.closest(".element-hider-custom-menu")) {
closeCustomMenu();
removeHighlight();
}
},
true,
);
// Handle escape key to close custom menu and remove highlights
document.addEventListener('keydown', (event: KeyboardEvent) => {
document.addEventListener("keydown", (event: KeyboardEvent) => {
if (event.key === "Escape") {
if (customMenu) {
closeCustomMenu();
@@ -464,8 +523,15 @@ const contextMenuObserver = new MutationObserver((mutations) => {
const element = node as HTMLElement;
// Look for Tidal's context menu
if (element.matches('[data-test="contextmenu"]') || element.querySelector('[data-test="contextmenu"]')) {
const contextMenu = element.matches('[data-test="contextmenu"]') ? element : element.querySelector('[data-test="contextmenu"]') as HTMLElement;
if (
element.matches('[data-test="contextmenu"]') ||
element.querySelector('[data-test="contextmenu"]')
) {
const contextMenu = element.matches('[data-test="contextmenu"]')
? element
: (element.querySelector(
'[data-test="contextmenu"]',
) as HTMLElement);
if (contextMenu && currentContextElement && waitingForBuiltInMenu) {
// Built-in menu appeared, cancel custom menu timeout
@@ -485,8 +551,8 @@ const contextMenuObserver = new MutationObserver((mutations) => {
// Add our options to the existing context menu
function addElementHiderOptions(contextMenu: HTMLElement): void {
// Create hide element button
const hideButton = document.createElement('button');
hideButton.className = 'element-hider-menu-item';
const hideButton = document.createElement("button");
hideButton.className = "element-hider-menu-item";
hideButton.style.cssText = `
display: flex;
align-items: center;
@@ -503,7 +569,7 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
`;
hideButton.innerHTML = `Hide This Element`;
hideButton.addEventListener('click', () => {
hideButton.addEventListener("click", () => {
if (currentContextElement) {
targetElement = currentContextElement;
hideTargetElement();
@@ -511,37 +577,38 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
});
// Add hover effects for highlighting
hideButton.addEventListener('mouseenter', () => {
hideButton.style.background = 'var(--wave-color-background-hover, #3a3a3a)';
hideButton.addEventListener("mouseenter", () => {
hideButton.style.background = "var(--wave-color-background-hover, #3a3a3a)";
if (currentContextElement) {
highlightElement(currentContextElement);
}
});
hideButton.addEventListener('mouseleave', () => {
hideButton.style.background = 'transparent';
hideButton.addEventListener("mouseleave", () => {
hideButton.style.background = "transparent";
removeHighlight();
});
// Create unhide all button
const unhideAllButton = document.createElement('button');
unhideAllButton.className = 'element-hider-menu-item';
const unhideAllButton = document.createElement("button");
unhideAllButton.className = "element-hider-menu-item";
unhideAllButton.style.cssText = hideButton.style.cssText;
unhideAllButton.innerHTML = `Unhide All Elements (${hiddenElementsArray.length})`;
unhideAllButton.addEventListener('click', unhideAllElements);
unhideAllButton.addEventListener("click", unhideAllElements);
// Add hover effects for unhide all button
unhideAllButton.addEventListener('mouseenter', () => {
unhideAllButton.style.background = 'var(--wave-color-background-hover, #3a3a3a)';
unhideAllButton.addEventListener("mouseenter", () => {
unhideAllButton.style.background =
"var(--wave-color-background-hover, #3a3a3a)";
});
unhideAllButton.addEventListener('mouseleave', () => {
unhideAllButton.style.background = 'transparent';
unhideAllButton.addEventListener("mouseleave", () => {
unhideAllButton.style.background = "transparent";
});
// Add a separator if the menu has other items
if (contextMenu.children.length > 0) {
const separator = document.createElement('div');
const separator = document.createElement("div");
separator.style.cssText = `
height: 1px;
background: var(--wave-color-border, #444);
@@ -558,10 +625,10 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
// Start observing for context menus
contextMenuObserver.observe(document.body, {
childList: true,
subtree: true
subtree: true,
});
// Initialize plugin
// Initialize plugin
function initializePlugin() {
trace.log("Initializing plugin...");
@@ -578,8 +645,8 @@ function initializePlugin() {
}
// Run initialization when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePlugin);
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initializePlugin);
} else {
initializePlugin();
}
@@ -600,8 +667,8 @@ unloads.add(() => {
removeHighlight();
// Clean up global functions
(window as any).showAllElementsFromSettings = undefined;
(window as any).debugElementHider = undefined;
window.showAllElementsFromSettings = undefined;
window.debugElementHider = undefined;
trace.log("Plugin unloaded");
});
+36 -34
View File
@@ -2,62 +2,64 @@
/* Custom context menu for elements without built-in menu */
.element-hider-custom-menu {
position: fixed;
background: var(--wave-color-background-elevated, #2a2a2a);
border: 1px solid var(--wave-color-border, #444);
border-radius: 8px;
padding: 8px 0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 999999;
min-width: 180px;
font-family: inherit;
font-size: 14px;
position: fixed;
background: var(--wave-color-background-elevated, #2a2a2a);
border: 1px solid var(--wave-color-border, #444);
border-radius: 8px;
padding: 8px 0;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
z-index: 999999;
min-width: 180px;
font-family: inherit;
font-size: 14px;
}
.element-hider-menu-item {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
color: var(--wave-color-text, #ffffff);
background: transparent;
border: none;
width: 100%;
text-align: left;
transition: background-color 0.15s ease;
font-family: inherit;
font-size: 14px;
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
color: var(--wave-color-text, #ffffff);
background: transparent;
border: none;
width: 100%;
text-align: left;
transition: background-color 0.15s ease;
font-family: inherit;
font-size: 14px;
}
.element-hider-menu-item:hover {
background: var(--wave-color-background-hover, #3a3a3a);
background: var(--wave-color-background-hover, #3a3a3a);
}
.element-hider-menu-item:active {
background: var(--wave-color-background-active, #4a4a4a);
background: var(--wave-color-background-active, #4a4a4a);
}
.element-hider-menu-icon {
margin-right: 8px;
width: 16px;
height: 16px;
margin-right: 8px;
width: 16px;
height: 16px;
}
/* Highlight the target element */
.element-hider-target {
outline: 2px solid #ff6b6b !important;
outline-offset: 2px !important;
box-shadow: 0 0 10px rgba(255, 107, 107, 0.6) !important;
outline: 2px solid #ff6b6b !important;
outline-offset: 2px !important;
box-shadow: 0 0 10px rgba(255, 107, 107, 0.6) !important;
}
/* Hidden elements */
.element-hider-hidden {
display: none !important;
display: none !important;
}
/* Animation for hiding */
.element-hider-hiding {
transition: opacity 0.3s ease, transform 0.3s ease;
opacity: 0;
transform: scale(0.95);
transition:
opacity 0.3s ease,
transform 0.3s ease;
opacity: 0;
transform: scale(0.95);
}
-59
View File
@@ -1,59 +0,0 @@
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
@@ -1,301 +0,0 @@
/*
{
"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
@@ -1,128 +0,0 @@
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
@@ -1,424 +0,0 @@
/*
{
"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;
}
@@ -1,215 +0,0 @@
/*
{
"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,159 +1,167 @@
/* Global Spinning Background Styles - PERFORMANCE OPTIMIZED */
.global-background-container {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
z-index: -3;
pointer-events: none;
overflow: hidden;
/* Hardware acceleration */
transform: translateZ(0);
backface-visibility: hidden;
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
z-index: -3;
pointer-events: none;
overflow: hidden;
/* Hardware acceleration */
transform: translateZ(0);
backface-visibility: hidden;
}
.global-spinning-black-bg {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #000;
z-index: -2;
pointer-events: none;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #000;
z-index: -2;
pointer-events: none;
}
.global-spinning-image {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 150vw;
height: 150vh;
object-fit: cover;
z-index: -1;
filter: blur(80px) brightness(0.4) contrast(1.2) saturate(1);
opacity: 1;
animation: spinGlobal 45s linear infinite;
will-change: transform;
/* Hardware acceleration */
transform-origin: center center;
backface-visibility: hidden;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 150vw;
height: 150vh;
object-fit: cover;
z-index: -1;
filter: blur(80px) brightness(0.4) contrast(1.2) saturate(1);
opacity: 1;
animation: spinGlobal 45s linear infinite;
will-change: transform;
/* Hardware acceleration */
transform-origin: center center;
backface-visibility: hidden;
}
/* Performance mode optimizations - keep spinning but optimize other aspects */
.global-spinning-image.performance-mode-static {
/* Keep animation enabled in performance mode */
/* Lighter blur for performance */
filter: blur(20px) brightness(0.4) contrast(1.2) saturate(1) !important;
/* Smaller size for performance */
width: 120vw !important;
height: 120vh !important;
/* Hide Tidal's native now-playing background color overlay */
[data-test="new-now-playing"] > [class*="_background_"] {
/* biome-ignore lint: Must override native album-art-derived background */
display: none !important;
}
.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;
/* Ensure the now-playing container itself is transparent */
[class*="_nowPlayingContainer"] {
/* biome-ignore lint: Must override any inline background styles */
background: transparent !important;
}
/* Now Playing Background Container Optimization */
.now-playing-background-container {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: -3;
pointer-events: none;
overflow: hidden;
/* Hardware acceleration */
transform: translateZ(0);
backface-visibility: hidden;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
overflow: hidden;
/* Hardware acceleration */
transform: translateZ(0);
backface-visibility: hidden;
}
/* Ensure now-playing content renders above the dynamic background */
[data-test="new-now-playing"] > header,
[data-test="new-now-playing"] > [class*="_content_"],
[data-test="new-now-playing"] > .unhide-ui-button {
position: relative;
z-index: 1;
}
/* Optimized keyframe animations with GPU acceleration */
@keyframes spinGlobal {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
/* Reduced motion for users who prefer it */
@media (prefers-reduced-motion: reduce) {
.global-spinning-image,
.now-playing-background-image {
animation: none !important;
transform: translate(-50%, -50%) !important;
will-change: auto !important;
}
.global-spinning-image,
.now-playing-background-image {
/* biome-ignore lint: Accessibility override needs priority */
animation: none !important;
/* biome-ignore lint: Accessibility override needs priority */
transform: translate(-50%, -50%) !important;
/* biome-ignore lint: Accessibility override needs priority */
will-change: auto !important;
}
}
/* Performance mode: optimize effects but keep spinning */
.performance-mode .global-spinning-image,
.performance-mode .now-playing-background-image {
/* Keep animations but optimize filter effects */
filter: blur(10px) brightness(0.4) contrast(1.1) !important;
/* Keep animations but optimize filter effects */
/* biome-ignore lint: Intentional override of runtime styles */
filter: blur(10px) brightness(0.4) contrast(1.1) !important;
}
/* Make Notification Feed sidebar transparent */
/* Make app chrome transparent for cover-everywhere background */
body,
#wimp,
main,
[class^="_sidebarWrapper"],
[class^="_mainContainer"],
[class*="smallHeader"],
[data-test="main-layout-sidebar-wrapper"],
[data-test="main-layout-header"],
[data-test="feed-sidebar"],
[data-test="stream-metadata"],
[data-test="footer-player"],
/* Notification Feed sidebar specific container */
[class^="_feedSidebarVStack"],
[class^="_feedSidebarSpacer"],
[class^="_feedSidebarItem"],
[class^="_feedSidebarItemDiv"],
[class^="_cellContainer"],
[class^="_cellTextContainer"] {
background: unset !important;
[class^="_cellContainer"] {
/* biome-ignore lint: Ensure background is fully cleared under theme CSS */
background: unset !important;
}
/* Make sidebar and player bar semi-transparent with optimized backdrop-filter */
[data-test="footer-player"],
[data-test="main-layout-sidebar-wrapper"],
[class^="_bar"],
[class^="_sidebarItem"]:hover {
background-color: rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(10px) !important;
-webkit-backdrop-filter: blur(10px) !important;
/* Make sidebar semi-transparent with optimized backdrop-filter */
[data-test="main-layout-sidebar-wrapper"] {
/* biome-ignore lint: Must beat app inline styles for translucency */
background-color: rgba(0, 0, 0, 0.3) !important;
/* biome-ignore lint: Must beat app inline styles for translucency */
backdrop-filter: blur(10px) !important;
/* biome-ignore lint: Must beat app inline styles for translucency */
-webkit-backdrop-filter: blur(10px) !important;
}
/* Performance mode: reduce backdrop blur */
.performance-mode [data-test="footer-player"],
.performance-mode [data-test="main-layout-sidebar-wrapper"],
.performance-mode [class^="_bar"],
.performance-mode [class^="_sidebarItem"]:hover {
backdrop-filter: blur(5px) !important;
-webkit-backdrop-filter: blur(5px) !important;
.performance-mode [data-test="main-layout-sidebar-wrapper"] {
/* biome-ignore lint: Performance mode style requires priority */
backdrop-filter: blur(5px) !important;
/* biome-ignore lint: Performance mode style requires priority */
-webkit-backdrop-filter: blur(5px) !important;
}
/* Feed sidebar panel - black tint background for readability */
[data-test="feed-sidebar"] {
background-color: rgba(0, 0, 0, 0.5) !important;
backdrop-filter: blur(10px) !important;
-webkit-backdrop-filter: blur(10px) !important;
/* biome-ignore lint: Ensure readability over media */
background-color: rgba(0, 0, 0, 0.5) !important;
/* biome-ignore lint: Ensure readability over media */
backdrop-filter: blur(10px) !important;
/* biome-ignore lint: Ensure readability over media */
-webkit-backdrop-filter: blur(10px) !important;
}
/* Performance mode: reduce sidebar backdrop blur */
.performance-mode [data-test="feed-sidebar"] {
backdrop-filter: blur(5px) !important;
-webkit-backdrop-filter: blur(5px) !important;
/* biome-ignore lint: Performance mode style requires priority */
backdrop-filter: blur(5px) !important;
/* biome-ignore lint: Performance mode style requires priority */
-webkit-backdrop-filter: blur(5px) !important;
}
/* Feed sidebar items - transparent */
@@ -162,10 +170,6 @@ main,
[class*="_cellContainer"],
[data-test="feed-interval"],
[data-test="feed-item"] {
background-color: transparent !important;
}
/* Remove bottom gradient */
[class^="_bottomGradient"] {
display: none !important;
/* biome-ignore lint: Match theme transparency */
background-color: transparent !important;
}
@@ -0,0 +1,22 @@
/* Square Player Bar override — injected when floating is disabled */
/* MARKER: Floating Player Bar CSS */
[data-test="footer-player"] {
/* biome-ignore lint: Override native floating position */
bottom: 0 !important;
/* biome-ignore lint: Override native floating position */
left: 0 !important;
/* biome-ignore lint: Override native floating position */
right: 0 !important;
/* biome-ignore lint: Override native floating position */
width: 100% !important;
/* biome-ignore lint: Override native floating position */
margin: 0 !important;
/* biome-ignore lint: Force square corners */
border-radius: 0 !important;
/* biome-ignore lint: Remove floating border */
border: none !important;
/* biome-ignore lint: Remove floating shadow */
box-shadow: none !important;
}
File diff suppressed because it is too large Load Diff
+421 -60
View File
@@ -1,87 +1,448 @@
/* Font imports for lyrics */
@font-face {
font-family: "AbyssFont";
font-weight: 400;
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") format("woff2");
font-family: "AbyssFont";
font-weight: 400;
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2")
format("woff2");
}
@font-face {
font-family: "AbyssFont";
font-weight: 500;
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2");
font-family: "AbyssFont";
font-weight: 500;
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2")
format("woff2");
}
@font-face {
font-family: "AbyssFont";
font-weight: 600;
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2");
font-family: "AbyssFont";
font-weight: 600;
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2")
format("woff2");
}
@font-face {
font-family: "AbyssFont";
font-weight: 700;
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2");
font-family: "AbyssFont";
font-weight: 700;
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2")
format("woff2");
}
/* Enhanced lyrics styling with glow effects */
[class*="_lyricsText"] > div > span[data-current="true"] {
text-shadow: 0 0 2px #fff, 0 0 20px #fff !important;
padding-left: 20px;
transition-duration: 0.7s;
font-size: 55px;
color: white !important;
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-weight: 700;
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"][class*="_current_"] {
text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #fff),
/* biome-ignore lint: Required to override app glow strength */
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #fff) !important;
padding-left: 20px;
transition-duration: 0.7s;
font-size: calc(55px * var(--rl-font-scale, 1));
/* biome-ignore lint: Active lyric uses Colorama color */
color: var(--cl-glow1, #fff) !important;
font-family:
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 700;
}
[class*="_lyricsText"] > div > span {
text-shadow: 0 0 0px transparent, 0 0 0px transparent;
transition-duration: 0.25s;
color: rgba(128, 128, 128, 0.4);
font-size: 40px;
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-weight: 700;
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"] {
text-shadow:
0 0 0px transparent,
0 0 0px transparent;
transition-duration: 0.25s;
color: rgba(255, 255, 255, 0.4);
font-size: calc(40px * var(--rl-font-scale, 1));
font-family:
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 700;
}
[class*="_lyricsText"] > div > span:hover {
text-shadow: 0 0 2px lightgray, 0 0 20px lightgray !important;
color: lightgray !important;
padding-left: 20px;
transition-duration: 0.7s;
}
/* Track title glow */
[data-test="now-playing-track-title"] {
text-shadow: 0 0 1px #fff, 0 0 30px #fff !important;
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"]:hover {
text-shadow:
0 0 var(--rl-glow-inner, 2px) lightgray,
/* biome-ignore lint: Hover glow should override defaults */
0 0 var(--rl-glow-outer, 20px) lightgray !important;
/* biome-ignore lint: Hover color override */
color: lightgray !important;
padding-left: 20px;
transition-duration: 0.7s;
}
/* Current line transitions */
[class*="_lyricsText"] > div > span {
transition: text-shadow 0.7s ease-in-out, color 0.7s ease-in-out, padding 0.7s ease-in-out !important;
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"] {
transition:
text-shadow 0.7s ease-in-out,
color 0.7s ease-in-out,
/* biome-ignore lint: Transition priority needed */
padding 0.7s ease-in-out !important;
}
/* Glow-aware left padding so the glow doesn't clip | Thanks Aya <3*/
.rl-wbw-active {
padding-left: var(--rl-glow-outer) !important;
}
/* Lyrics container styling */
[class^="_lyricsContainer"] > div > div > span {
margin-bottom: 2rem;
opacity: 1;
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-weight: 700;
font-size: 38px !important;
[data-test="now-playing-lyrics"] span[data-test="lyrics-line"] {
margin-bottom: 2rem;
/* biome-ignore lint: Must beat Tidal _hasCues_ opacity */
opacity: 1 !important;
font-family:
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 700;
/* biome-ignore lint: Typography override for readability */
font-size: calc(38px * var(--rl-font-scale, 1)) !important;
}
/* Reset all lyrics styling when disabled */
.lyrics-glow-disabled [class*="_lyricsText"] > div > span[data-current="true"],
.lyrics-glow-disabled [class*="_lyricsText"] > div > span,
.lyrics-glow-disabled [class*="_lyricsText"] > div > span:hover,
.lyrics-glow-disabled [data-test="now-playing-track-title"],
.lyrics-glow-disabled [class^="_lyricsContainer"] > div > div > span {
text-shadow: none !important;
padding-left: 0 !important;
transition: none !important;
font-size: inherit !important;
color: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
margin-bottom: inherit !important;
opacity: inherit !important;
/* Hide the old Musixmatch attribution footer in the lyrics panel */
[data-test="now-playing-lyrics"] [class*="_footer_"] {
display: none !important;
}
/* MARKER: WBW lyrics CSS */
/* hide tidal spans for wbw */
.rl-wbw-active span[data-test="lyrics-line"] {
/* biome-ignore lint: Must hide original lines when word-by-word is on */
display: none !important;
}
/* Active line slide */
.rl-wbw-line {
text-align: left;
padding-left: 0;
padding-right: 0;
filter: none;
transform: translateZ(0);
transform-origin: left;
transition:
filter 0.4s ease,
padding-left 0.7s ease-in-out,
padding-right 0.7s ease-in-out;
overflow: visible;
}
.rl-wbw-line.rl-wbw-spacer {
filter: none;
}
/* Blur Inactive (opt-in via .rl-blur-active on container) */
.rl-blur-active .rl-wbw-line {
filter: blur(0.07em);
}
.rl-blur-active .rl-wbw-line.rl-pos-1 {
filter: blur(0.035em);
}
.rl-blur-active .rl-wbw-line.rl-pos-2 {
filter: blur(0.05em);
}
.rl-blur-active .rl-wbw-line.rl-pos-3 {
filter: blur(0.06em);
}
/* Active line overrides (MUST come after blur rules to win on equal specificity) */
.rl-wbw-line.rl-wbw-line-active,
.rl-blur-active .rl-wbw-line.rl-wbw-line-active {
padding-left: 20px;
filter: none;
}
/* Keep last-active line unblurred during instrumental gaps */
.rl-blur-active .rl-wbw-line.rl-gap-hold {
filter: none;
}
/* Bubbled Lyrics scale (opt-in via .rl-bubbled on container) */
.rl-bubbled .rl-wbw-line {
scale: 0.93 0.93 0.95;
transition:
scale 0.7s ease,
filter 0.4s ease,
padding-left 0.7s ease-in-out,
padding-right 0.7s ease-in-out;
will-change: scale, translate, filter;
}
.rl-bubbled .rl-wbw-line.rl-wbw-spacer {
scale: none;
}
.rl-bubbled .rl-wbw-line.rl-wbw-line-active {
scale: 1;
transition:
scale 0.5s ease,
filter 0.4s ease,
padding-left 0.7s ease-in-out,
padding-right 0.7s ease-in-out;
}
/* Staggered scroll bounce animation (part of Bubbled Lyrics) */
@keyframes rl-scroll-bounce {
from {
translate: 0 var(--rl-scroll-delta);
}
to {
translate: 0 0;
}
}
.rl-wbw-line:not(.rl-scroll-animate) {
animation: none;
}
.rl-scroll-animate {
animation: rl-scroll-bounce 400ms cubic-bezier(0.41, 0, 0.12, 0.99) both;
animation-delay: var(--rl-line-delay, 0ms);
}
/* Word span — opacity override needed to beat Tidal's ._hasCues_ span { opacity:.35 } */
.rl-wbw-word {
text-shadow:
0 0 0px transparent,
0 0 0px transparent;
color: rgba(255, 255, 255, 0.4);
/* biome-ignore lint: Must beat Tidal _hasCues_ opacity */
opacity: 1 !important;
transition:
text-shadow 0.15s ease-out,
color 0.15s ease-out;
}
/* Hover word (Grouped Syllables) */
.rl-wbw-line:not(.rl-wbw-line-active) > .rl-wbw-word:hover,
.rl-wbw-line:not(.rl-wbw-line-active) > .rl-wbw-word.rl-wbw-word-hover,
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-main .rl-wbw-word:hover,
.rl-wbw-line:not(.rl-wbw-line-active)
.rl-wbw-main
.rl-wbw-word.rl-wbw-word-hover {
text-shadow:
0 0 var(--rl-glow-inner, 2px) lightgray,
/* biome-ignore lint: Hover glow should override defaults */
0 0 var(--rl-glow-outer, 20px) lightgray !important;
/* biome-ignore lint: Hover color override */
color: lightgray !important;
cursor: pointer;
}
/* Active word */
.rl-wbw-word.rl-wbw-active {
text-shadow:
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #fff),
/* biome-ignore lint: Glow priority for active word */
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #fff) !important;
/* biome-ignore lint: Active word uses Colorama color */
color: var(--cl-glow1, #fff) !important;
}
/* MARKER: Syllable sweep animation CSS */
@keyframes rl-wipe {
from {
background-size:
0.75em 100%,
0% 100%,
100% 100%;
background-position:
-0.375em 0%,
left,
left;
}
to {
background-size:
0.75em 100%,
100% 100%,
100% 100%;
background-position:
calc(100% + 0.375em) 0%,
left,
left;
}
}
/* Syllable active: gradient sweep L-to-R via background-clip */
.rl-wbw-word.rl-syl-active {
/* biome-ignore lint: Kill base transitions so class swaps are instant */
transition: none !important;
/* biome-ignore lint: Transparent fill so gradient paints the text */
color: transparent !important;
/* biome-ignore lint: Clip gradient to text glyphs */
-webkit-background-clip: text !important;
/* biome-ignore lint: Clip gradient to text glyphs */
background-clip: text !important;
background-repeat: no-repeat;
background-image:
linear-gradient(
90deg,
transparent 0%,
var(--cl-glow1, #fff) 50%,
transparent 100%
),
linear-gradient(90deg, var(--cl-glow1, #fff) 100%, transparent 100%),
linear-gradient(90deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.4));
background-size:
0.75em 100%,
0% 100%,
100% 100%;
background-position:
-0.375em 0%,
left,
left;
/* biome-ignore lint: No glow for syllable mode */
text-shadow: none !important;
/* biome-ignore lint: No glow for syllable mode */
filter: none !important;
}
/* Syllable finished: word stays Colorama color, no glow */
.rl-wbw-word.rl-syl-finished {
/* biome-ignore lint: Kill base transitions so class swaps are instant */
transition: none !important;
/* biome-ignore lint: Finished syllable uses Colorama color */
color: var(--cl-glow1, #fff) !important;
/* biome-ignore lint: No glow for syllable mode */
text-shadow: none !important;
/* biome-ignore lint: No glow for syllable mode */
filter: none !important;
}
/* MARKER: Syllable animations CSS (WIP coming soon) */
/* syllableStyle: 0 = none, 1 = Pop!, 2 = Jump */
@keyframes rl-pop {
0%,
100% {
transform: scale(1);
}
25%,
35% {
transform: scale(1.03) translateY(-0.5%);
}
}
@keyframes rl-jump {
0% {
transform: translateY(8px);
}
50% {
transform: translateY(-3px);
}
100% {
transform: translateY(0);
}
}
/* Pop! for word mode */
.rl-syl-pop .rl-wbw-word.rl-wbw-active {
transform-origin: center bottom;
animation: rl-pop 0.6s ease-out;
}
/* Pop! for syllable mode */
.rl-syl-pop .rl-wbw-word.rl-syl-active {
transform-origin: center bottom;
}
/* Jump for word mode */
.rl-syl-jump .rl-wbw-word.rl-wbw-active {
animation: rl-jump 0.35s ease-out;
}
/* Tidals "..." at the top of the container */
.rl-wbw-active > span:not([data-test="lyrics-line"]) {
display: block;
font-size: calc(40px * var(--rl-font-scale, 1));
font-family:
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 700;
color: rgba(255, 255, 255, 0.4);
/* biome-ignore lint: Must beat Tidal _hasCues_ opacity */
opacity: 1 !important;
text-shadow: 0 0 0px transparent;
margin-bottom: 2rem;
}
/* MARKER: Context Aware Lyrics CSS */
/* Background vocal sub-container */
.rl-wbw-bg-container {
max-height: 0;
overflow: visible;
opacity: 0;
font-size: 0.55em;
padding-top: 0.15em;
transition:
max-height 0.3s ease,
opacity 0.5s ease;
color: rgba(255, 255, 255, 0.4);
}
.rl-wbw-line.rl-wbw-line-active .rl-wbw-bg-container {
max-height: 3em;
opacity: 1;
transition:
max-height 0.5s ease,
opacity 0.5s ease;
}
/* Singer duet positioning */
.rl-wbw-line.rl-singer-right {
text-align: end;
transform-origin: right;
}
.rl-dual-side .rl-wbw-line.rl-singer-left {
padding-right: 20%;
}
.rl-dual-side .rl-wbw-line.rl-singer-right {
padding-left: 20%;
}
.rl-dual-side .rl-wbw-line.rl-singer-right .rl-wbw-bg-container {
text-align: end;
}
.rl-dual-side .rl-wbw-line.rl-singer-right.rl-wbw-line-active {
padding-right: 20px;
}
.rl-dual-side .rl-wbw-line.rl-singer-left.rl-wbw-line-active {
padding-left: 20px;
}
/* Reset glow when disabled */
.lyrics-glow-disabled [data-test="now-playing-lyrics"] span[data-test="lyrics-line"][class*="_current_"],
.lyrics-glow-disabled [data-test="now-playing-lyrics"] span[data-test="lyrics-line"]:hover {
/* biome-ignore lint: Kill glow on active/hover lines */
text-shadow: none !important;
}
/* kill glow on active word */
.lyrics-glow-disabled .rl-wbw-word.rl-wbw-active {
/* biome-ignore lint: Kill glow on active word */
text-shadow: none !important;
}
/* kill glow on hovered word */
.lyrics-glow-disabled .rl-wbw-line:not(.rl-wbw-line-active)
> .rl-wbw-word:hover,
.lyrics-glow-disabled
.rl-wbw-line:not(.rl-wbw-line-active)
> .rl-wbw-word.rl-wbw-word-hover,
.lyrics-glow-disabled
.rl-wbw-line:not(.rl-wbw-line-active)
.rl-wbw-main
.rl-wbw-word:hover,
.lyrics-glow-disabled
.rl-wbw-line:not(.rl-wbw-line-active)
.rl-wbw-main
.rl-wbw-word.rl-wbw-word-hover {
/* biome-ignore lint: Kill glow on hovered word */
text-shadow: none !important;
}
@@ -1,15 +1,15 @@
/* Hide player bar when setting is disabled, but show on hover - only when UI is hidden */
.radiant-lyrics-ui-hidden [data-test="footer-player"] {
opacity: 0 !important;
transition: opacity 0.5s ease-in-out !important;
opacity: 0 !important;
transition: opacity 0.5s ease-in-out !important;
}
.radiant-lyrics-ui-hidden [data-test="footer-player"]:hover {
opacity: 1 !important;
opacity: 1 !important;
}
/* Also show player bar when hovering over the bottom area - only when UI is hidden */
.radiant-lyrics-ui-hidden:has([data-test="footer-player"]:hover) [data-test="footer-player"],
.radiant-lyrics-ui-hidden body.rl-footer-hover [data-test="footer-player"],
.radiant-lyrics-ui-hidden [data-test="footer-player"]:hover {
opacity: 1 !important;
opacity: 1 !important;
}
+273 -204
View File
@@ -1,224 +1,293 @@
/* Only apply styles when UI is hidden */
.radiant-lyrics-ui-hidden [class*="tabItems"] {
opacity: 0 !important;
transition: opacity 0.4s ease-in-out;
/* Sidebar */
[class*="_sidebar_"] {
background-color: transparent !important;
}
/* Default state - visible */
[class*="tabItems"] {
transition: opacity 0.4s ease-in-out;
/* Section header */
[class*="_sectionHeader_"] {
background-color: transparent !important;
}
/* Tab items stay hidden - no hover functionality (if the song changes and it doesnt have lyrics.. and ya want them back.. you can unhide the UI <3) */
.radiant-lyrics-ui-hidden [data-test="header-container"]:not(:has(.hide-ui-button)) {
opacity: 0 !important;
transition: opacity 0.4s ease-in-out;
/* Rounded corners */
[class*="_thumbnail_"],
[class*="_imageWrapper_"],
[class*="_playButton_"] {
border-radius: 5px !important;
}
/* 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;
/* MARKER: HideUI CSS*/
/* Only apply styles when UI is hidden — hide toggle buttons */
.radiant-lyrics-ui-hidden [data-test="toggle-lyrics"],
.radiant-lyrics-ui-hidden [data-test="toggle-credits"],
.radiant-lyrics-ui-hidden [data-test="toggle-similar-tracks"] {
opacity: 0 !important;
transition: opacity 0.4s ease-in-out;
}
/* Default state for header */
[data-test="header-container"] {
transition: opacity 0.4s ease-in-out;
.radiant-lyrics-ui-hidden [data-test="toggle-lyrics"]:hover,
.radiant-lyrics-ui-hidden [data-test="toggle-credits"]:hover,
.radiant-lyrics-ui-hidden [data-test="toggle-similar-tracks"]:hover {
opacity: 1 !important;
}
/* 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;
/* Hide header container (search, minimize, fullscreen) when UI is hidden */
.radiant-lyrics-ui-hidden [data-test="header"] {
opacity: 0 !important;
visibility: hidden !important;
transition: opacity 0.4s ease-in-out, visibility 0s linear 0.4s;
pointer-events: none !important;
}
/* Immediate hide class for unhide button with smooth transition (had issues with the fade out.. so I removed it) */
/* Immediate hide class for unhide button */
.hide-immediately {
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
}
[class^="_bar"] {
background-color: transparent;
}
.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*/
/* Auto-fade styling for unhide button */
.unhide-ui-button.auto-faded {
background-color: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
color: rgba(255, 255, 255, 0.8) !important;
transition: background-color 0.8s ease-in-out, border-color 0.8s ease-in-out, box-shadow 0.8s ease-in-out, backdrop-filter 0.8s ease-in-out, color 0.8s ease-in-out;
background-color: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
color: rgba(255, 255, 255, 0.4) !important;
transition:
background-color 0.8s ease-in-out,
border-color 0.8s ease-in-out,
box-shadow 0.8s ease-in-out,
backdrop-filter 0.8s ease-in-out,
color 0.8s ease-in-out !important;
}
/* Restore button styling on hover */
.unhide-ui-button.auto-faded:hover {
background-color: rgba(255, 255, 255, 0.2) !important;
border-color: rgba(255, 255, 255, 0.3) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(10px) !important;
-webkit-backdrop-filter: blur(10px) !important;
color: white !important;
transition: background-color 0.3s ease-in-out, border-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out, backdrop-filter 0.3s ease-in-out, color 0.3s ease-in-out;
background-color: rgba(255, 255, 255, 0.2) !important;
border-color: rgba(255, 255, 255, 0.3) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(10px) !important;
-webkit-backdrop-filter: blur(10px) !important;
color: white !important;
transition:
background-color 0.3s ease-in-out,
border-color 0.3s ease-in-out,
box-shadow 0.3s ease-in-out,
backdrop-filter 0.3s ease-in-out,
color 0.3s ease-in-out !important;
}
/* MARKER: Sticky Lyrics CSS */
/* Lyrics toggle button */
[data-test="toggle-lyrics"]:has(.sticky-lyrics-trigger) {
position: relative !important;
padding-right: 38px !important;
}
/* Trigger */
.sticky-lyrics-trigger {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 38px;
display: flex;
align-items: center;
justify-content: center;
padding-left: 5px;
padding-right: 0px;
box-sizing: border-box;
cursor: default;
color: #CCCCD1;
transition: color 0.2s ease;
}
/* Divider line */
.sticky-lyrics-trigger::before {
content: "";
position: absolute;
left: 5px;
top: 4px;
bottom: 4px;
width: 1px;
background: transparent;
transition: background 0.2s ease;
}
/* When Lyrics toggle is pressed — show divider & adjust icon */
[data-test="toggle-lyrics"][aria-pressed="true"] .sticky-lyrics-trigger {
color: rgb(30, 30, 30);
cursor: pointer;
}
[data-test="toggle-lyrics"][aria-pressed="true"] .sticky-lyrics-trigger::before {
background: rgba(0, 0, 0, 0.15);
}
[data-test="toggle-lyrics"][aria-pressed="true"] .sticky-lyrics-trigger:hover {
color: rgba(0, 0, 0, 0.5);
}
/* Animate widening when dropdown opens */
[data-test="toggle-lyrics"]:has(.sticky-lyrics-trigger) {
transition: min-width 0.12s ease-out;
}
/* Reduce top rounding, square bottom, and kill hover tint when dropdown is open */
body.rl-dropdown-open [data-test="toggle-lyrics"] {
border-radius: 12px 12px 0 0 !important;
background-color: rgb(255, 255, 255) !important;
min-width: 150px !important;
}
/* Dropdown — right-aligned under the Lyrics button */
.sticky-lyrics-dropdown {
position: fixed;
background: rgb(255, 255, 255);
border-radius: 0 0 12px 12px;
padding: 8px 12px 10px;
box-sizing: border-box;
z-index: 10000;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
clip-path: inset(0 -20px -20px -20px);
animation: stickyLyricsDropdownIn 0.12s ease-out;
}
@keyframes stickyLyricsDropdownIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Row containing label + toggle */
.sticky-lyrics-dropdown-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.sticky-lyrics-label {
font-size: 11px;
font-weight: 600;
color: rgba(0, 0, 0, 0.8);
white-space: nowrap;
}
/* Toggle switch */
.sticky-lyrics-switch {
position: relative;
display: inline-block;
width: 34px;
height: 18px;
flex-shrink: 0;
}
.sticky-lyrics-switch input {
opacity: 0;
width: 0;
height: 0;
}
.sticky-lyrics-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.15);
transition: 0.3s;
border-radius: 18px;
}
.sticky-lyrics-slider::before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 2px;
bottom: 2px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
.sticky-lyrics-switch input:checked + .sticky-lyrics-slider {
background-color: rgb(30, 30, 30);
}
.sticky-lyrics-switch input:checked + .sticky-lyrics-slider::before {
transform: translateX(16px);
background-color: rgb(255, 255, 255);
}
/* Segmented control (Line | Word | Syllable) */
.rl-style-row {
justify-content: center;
margin-top: 6px;
}
.rl-seg-control {
display: flex;
background: rgba(0, 0, 0, 0.06);
border-radius: 10px;
padding: 2px;
gap: 2px;
width: 100%;
}
.rl-seg-btn {
flex: 1;
border: none;
background: transparent;
color: rgba(0, 0, 0, 0.4);
font-size: 10px;
font-weight: 600;
padding: 5px 0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.rl-seg-btn:hover {
color: rgba(0, 0, 0, 0.7);
background: rgba(0, 0, 0, 0.06);
}
.rl-seg-btn.rl-seg-active {
background: rgb(30, 30, 30);
color: rgb(255, 255, 255);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
/* MARKER: PATCHES (Random Fixes for Tidals Changes) */
/* These change allot so i gave them their own section */
/* Remove max-width cap on now-playing content */
[class*="_contentInner"] {
max-width: none !important;
}
/* Round now-playing artwork corners */
[data-test="now-playing-artwork"] {
/* biome-ignore lint: Override flat corners */
border-radius: 10px !important;
}
/* Hide the Overlay Scrollbar (people just use mouse scroll) */
.os-scrollbar {
display: none !important;
pointer-events: none !important;
}