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/ node_modules/
dist/ dist/
dist/itzzexcel.oled-theme.json Notes.md
dist/itzzexcel.oled-theme.mjs /Reference/
dist/itzzexcel.oled-theme.mjs.map
dist/store.json
+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 ## Plugins
### 🎨 OLED Theme ### 🎨 Obsidian
**Location:** `plugins/oled-theme-luna/` **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:** **Features:**
- Applies a dark, OLED-optimized theme - Applies a dark, OLED-optimized theme
- Fetches the latest theme CSS from the GitHub repository
- Reduces battery consumption on OLED displays.. i guess <3 - Reduces battery consumption on OLED displays.. i guess <3
- Modern, sleek dark interface - Modern, sleek dark interface
@@ -34,6 +33,16 @@ Allows users to copy song lyrics by selecting them directly in the interface.
- Automatic clipboard copying of selected lyrics - Automatic clipboard copying of selected lyrics
- Smart lyric span detection - Smart lyric span detection
### 🧽 Element Hider
**Location:** `plugins/element-hider-luna/`
Allows users to hide/remove UI elements by right clicking on them.
**Features:**
- Remove/Hide ANY UI element
- Automagically saves hidden elements
- Allows for elements to be restored
### 🎶 Audio Visualizer ### 🎶 Audio Visualizer
**Location:** `plugins/audio-visualizer-luna/` **Location:** `plugins/audio-visualizer-luna/`
@@ -49,8 +58,21 @@ Allows users to copy song lyrics by selecting them directly in the interface.
## Installation ## Installation
### Batteries Required
1. [TidaLuna](https://github.com/Inrixia/TidaLuna) - Plugin Framework for Tidal (what these plugins are for)
2. Tidal - Streaming Service (if you are here and dont use tidal.. then just enjoy the read <3)
### Installing from Plugin Store (in TidaLuna)
1. Open Tidal (with Luna installed)
2. Navigate to Luna Settings (Top right of Tidal)
3. Click "Plugin Store" Tab
4. Scroll Down and just click on the plugins to install them
5. Naviagte to the "Plugins" Tab
6. And now your done and you can adjust the settings to your liking <3
### Installing from URL ### Installing from URL
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) 2. Navigate to Luna Settings (Top right of Tidal)
3. Click "Plugin Store" Tab 3. Click "Plugin Store" Tab
4. Paste in the "Install from URL" Bar `https://github.com/meowarex/tidalluna-plugins/releases/download/latest/store.json` 4. Paste in the "Install from URL" Bar `https://github.com/meowarex/tidalluna-plugins/releases/download/latest/store.json`
@@ -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 git clone https://github.com/meowarex/tidalluna-plugins
# Change Folder to the Repo # Change Folder to the Repo
cd neptune-projects-fork cd tidalluna-plugins
# Install dependencies # Install dependencies
pnpm install pnpm install
@@ -73,7 +95,7 @@ pnpm run watch
``` ```
### Installing Plugins in TidalLuna ### 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) 2. Navigate to Luna Settings (Top right of Tidal)
3. Click "Plugin Store" Tab 3. Click "Plugin Store" Tab
4. Click Install on the Plugins at the top Labeled with "[Dev]" 4. Click Install on the Plugins at the top Labeled with "[Dev]"
@@ -82,7 +104,7 @@ pnpm run watch
## Development ## Development
This project is made for: 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 ## GitHub Actions
@@ -90,10 +112,7 @@ This project is made for:
- **Release automation** for distributing plugins - **Release automation** for distributing plugins
- **Artifact uploads** for easy plugin distribution - **Artifact uploads** for easy plugin distribution
## Based On <3
- **itzzexcel** - [GitHub](https://github.com/ItzzExcel)
## Credits ## Credits
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", "rimraf": "^6.0.1",
"tsx": "^4.19.4", "tsx": "^4.19.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"
},
"dependencies": {
"pnpm": "^10.14.0"
} }
} }
+126 -45
View File
@@ -1,13 +1,21 @@
import { ReactiveStore } from "@luna/core"; 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"; import React from "react";
export const settings = await ReactiveStore.getPluginStorage("AudioVisualizer", { export const settings = await ReactiveStore.getPluginStorage(
"AudioVisualizer",
{
barCount: 32, barCount: 32,
barColor: "#ffffff", barColor: "#ffffff",
barRounding: true, barRounding: true,
customColors: [] as string[] customColors: [] as string[],
}); },
);
export const Settings = () => { export const Settings = () => {
const [barCount, setBarCount] = React.useState(settings.barCount); const [barCount, setBarCount] = React.useState(settings.barCount);
@@ -18,7 +26,9 @@ export const Settings = () => {
const [shouldRender, setShouldRender] = React.useState(false); const [shouldRender, setShouldRender] = React.useState(false);
const [customInput, setCustomInput] = React.useState(settings.barColor); const [customInput, setCustomInput] = React.useState(settings.barColor);
const [customColors, setCustomColors] = React.useState(settings.customColors); const [customColors, setCustomColors] = React.useState(settings.customColors);
const [hoveredColorIndex, setHoveredColorIndex] = React.useState<number | null>(null); const [hoveredColorIndex, setHoveredColorIndex] = React.useState<
number | null
>(null);
const closeColorPicker = () => { const closeColorPicker = () => {
setIsAnimatingIn(false); setIsAnimatingIn(false);
@@ -43,9 +53,25 @@ export const Settings = () => {
// Common color presets for cool points :D // Common color presets for cool points :D
const colorPresets = [ const colorPresets = [
"#ffffff", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", "#ffffff",
"#ff8800", "#8800ff", "#0088ff", "#88ff00", "#ff0088", "#00ff88", "#ff0000",
"#444444", "#888888", "#cccccc", "#1db954", "#e22134", "#1976d2" "#00ff00",
"#0000ff",
"#ffff00",
"#ff00ff",
"#00ffff",
"#ff8800",
"#8800ff",
"#0088ff",
"#88ff00",
"#ff0088",
"#00ff88",
"#444444",
"#888888",
"#cccccc",
"#1db954",
"#e22134",
"#1976d2",
]; ];
const updateColor = (color: string) => { const updateColor = (color: string) => {
@@ -63,9 +89,11 @@ export const Settings = () => {
// Validate hex color format // Validate hex color format
const hexColorRegex = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i; const hexColorRegex = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
if (hexColorRegex.test(trimmedInput) && if (
hexColorRegex.test(trimmedInput) &&
!colorPresets.includes(trimmedInput) && !colorPresets.includes(trimmedInput) &&
!customColors.includes(trimmedInput)) { !customColors.includes(trimmedInput)
) {
const newCustomColors = [...customColors, trimmedInput]; const newCustomColors = [...customColors, trimmedInput];
setCustomColors(newCustomColors); setCustomColors(newCustomColors);
settings.customColors = newCustomColors; settings.customColors = newCustomColors;
@@ -74,7 +102,9 @@ export const Settings = () => {
}; };
const removeCustomColor = (colorToRemove: string) => { const removeCustomColor = (colorToRemove: string) => {
const newCustomColors = customColors.filter(color => color !== colorToRemove); const newCustomColors = customColors.filter(
(color) => color !== colorToRemove,
);
setCustomColors(newCustomColors); setCustomColors(newCustomColors);
settings.customColors = newCustomColors; settings.customColors = newCustomColors;
@@ -117,19 +147,40 @@ export const Settings = () => {
{/* I'm not sure if this is a good idea, but it works & looks amazing */} {/* I'm not sure if this is a good idea, but it works & looks amazing */}
{/* Sorry @Inrixia <3 */} {/* Sorry @Inrixia <3 */}
<div style={{ <div
style={{
padding: "16px 0", padding: "16px 0",
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center" alignItems: "center",
}}> }}
>
<div> <div>
<div style={{ fontWeight: "normal", fontSize: "1.075rem", marginBottom: "4px" }}>Bar Color</div> <div
<div style={{ opacity: 0.7, fontSize: "14px" }}>Color of the visualizer bars</div> style={{
fontWeight: "normal",
fontSize: "1.075rem",
marginBottom: "4px",
}}
>
Bar Color
</div> </div>
<div style={{ display: "flex", gap: "8px", alignItems: "center", position: "relative" }}> <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 <button
onClick={() => showColorPicker ? closeColorPicker() : openColorPicker()} onClick={() =>
showColorPicker ? closeColorPicker() : openColorPicker()
}
style={{ style={{
width: "32px", width: "32px",
height: "32px", height: "32px",
@@ -140,15 +191,17 @@ export const Settings = () => {
backdropFilter: "blur(10px)", backdropFilter: "blur(10px)",
WebkitBackdropFilter: "blur(10px)", WebkitBackdropFilter: "blur(10px)",
position: "relative", position: "relative",
overflow: "hidden" overflow: "hidden",
}} }}
> >
<div style={{ <div
style={{
position: "absolute", position: "absolute",
inset: 0, inset: 0,
background: "rgba(0,0,0,0.1)", background: "rgba(0,0,0,0.1)",
backdropFilter: "blur(2px)" backdropFilter: "blur(2px)",
}} /> }}
/>
</button> </button>
{/* Custom Color Picker Modal */} {/* Custom Color Picker Modal */}
@@ -165,13 +218,14 @@ export const Settings = () => {
background: "rgba(0,0,0,0.6)", background: "rgba(0,0,0,0.6)",
zIndex: 1000, zIndex: 1000,
opacity: isAnimatingIn ? 1 : 0, opacity: isAnimatingIn ? 1 : 0,
transition: "opacity 0.2s ease" transition: "opacity 0.2s ease",
}} }}
onClick={closeColorPicker} onClick={closeColorPicker}
/> />
{/* Color Picker Panel */} {/* Color Picker Panel */}
<div style={{ <div
style={{
position: "fixed", position: "fixed",
top: "50%", top: "50%",
left: "50%", left: "50%",
@@ -187,20 +241,32 @@ export const Settings = () => {
zIndex: 1001, zIndex: 1001,
boxShadow: "0 20px 40px rgba(0,0,0,0.7)", boxShadow: "0 20px 40px rgba(0,0,0,0.7)",
opacity: isAnimatingIn ? 1 : 0, opacity: isAnimatingIn ? 1 : 0,
transform: isAnimatingIn ? "translate(-50%, -50%) scale(1)" : "translate(-50%, -50%) scale(0.9)", transform: isAnimatingIn
transition: "all 0.2s ease" ? "translate(-50%, -50%) scale(1)"
}}> : "translate(-50%, -50%) scale(0.9)",
<div style={{ marginBottom: "12px", color: "#fff", fontWeight: "bold", fontSize: "14px" }}> transition: "all 0.2s ease",
}}
>
<div
style={{
marginBottom: "12px",
color: "#fff",
fontWeight: "bold",
fontSize: "14px",
}}
>
Choose Color Choose Color
</div> </div>
{/* Color Grid */} {/* Color Grid */}
<div style={{ <div
style={{
display: "grid", display: "grid",
gridTemplateColumns: "repeat(7, 1fr)", gridTemplateColumns: "repeat(7, 1fr)",
gap: "8px", gap: "8px",
marginBottom: "16px" marginBottom: "16px",
}}> }}
>
{allColors.map((color, index) => { {allColors.map((color, index) => {
const isCustomColor = customColors.includes(color); const isCustomColor = customColors.includes(color);
const isHovered = hoveredColorIndex === index; const isHovered = hoveredColorIndex === index;
@@ -211,7 +277,7 @@ export const Settings = () => {
position: "relative", position: "relative",
width: "32px", width: "32px",
height: "32px", height: "32px",
cursor: "pointer" cursor: "pointer",
}} }}
className="color-item" className="color-item"
onMouseEnter={() => setHoveredColorIndex(index)} onMouseEnter={() => setHoveredColorIndex(index)}
@@ -226,10 +292,13 @@ export const Settings = () => {
width: "100%", width: "100%",
height: "100%", height: "100%",
borderRadius: "6px", borderRadius: "6px",
border: barColor === color ? "2px solid #fff" : "1px solid rgba(255,255,255,0.2)", border:
barColor === color
? "2px solid #fff"
: "1px solid rgba(255,255,255,0.2)",
background: color, background: color,
cursor: "pointer", cursor: "pointer",
transition: "all 0.2s ease" transition: "all 0.2s ease",
}} }}
/> />
{isCustomColor && ( {isCustomColor && (
@@ -255,7 +324,7 @@ export const Settings = () => {
justifyContent: "center", justifyContent: "center",
opacity: isHovered ? 1 : 0, opacity: isHovered ? 1 : 0,
transition: "opacity 0.2s ease", transition: "opacity 0.2s ease",
zIndex: 10 zIndex: 10,
}} }}
className="remove-button" className="remove-button"
> >
@@ -269,16 +338,28 @@ export const Settings = () => {
{/* Custom Hex Input */} {/* Custom Hex Input */}
<div style={{ marginBottom: "12px" }}> <div style={{ marginBottom: "12px" }}>
<div style={{ color: "rgba(255,255,255,0.7)", fontSize: "12px", marginBottom: "6px" }}> <div
style={{
color: "rgba(255,255,255,0.7)",
fontSize: "12px",
marginBottom: "6px",
}}
>
Add Custom Color Add Custom Color
</div> </div>
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}> <div
style={{
display: "flex",
gap: "8px",
alignItems: "center",
}}
>
<input <input
type="text" type="text"
value={customInput} value={customInput}
onChange={(e) => setCustomInput(e.target.value)} onChange={(e) => setCustomInput(e.target.value)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') { if (e.key === "Enter") {
updateColor(customInput); updateColor(customInput);
addCustomColor(); addCustomColor();
} }
@@ -293,7 +374,7 @@ export const Settings = () => {
color: "#fff", color: "#fff",
fontSize: "14px", fontSize: "14px",
fontFamily: "monospace", fontFamily: "monospace",
boxSizing: "border-box" boxSizing: "border-box",
}} }}
/> />
<button <button
@@ -313,13 +394,15 @@ export const Settings = () => {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
transition: "all 0.2s ease" transition: "all 0.2s ease",
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.25)"; e.currentTarget.style.background =
"rgba(255,255,255,0.25)";
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.15)"; e.currentTarget.style.background =
"rgba(255,255,255,0.15)";
}} }}
> >
+ +
@@ -338,7 +421,7 @@ export const Settings = () => {
background: "rgba(255,255,255,0.1)", background: "rgba(255,255,255,0.1)",
color: "#fff", color: "#fff",
cursor: "pointer", cursor: "pointer",
fontSize: "12px" fontSize: "12px",
}} }}
> >
Done Done
@@ -348,8 +431,6 @@ export const Settings = () => {
)} )}
</div> </div>
</div> </div>
</LunaSettings> </LunaSettings>
); );
}; };
+148 -141
View File
@@ -7,24 +7,24 @@ import visualizerStyles from "file://styles.css?minify";
export const { trace } = Tracer("[Audio Visualizer]"); export const { trace } = Tracer("[Audio Visualizer]");
// Helper function for consistent logging
const log = (message: string) => console.log(`[Audio Visualizer] ${message}`); const log = (message: string) => console.log(`[Audio Visualizer] ${message}`);
const warn = (message: string) => console.warn(`[Audio Visualizer] ${message}`);
const error = (message: string) => console.error(`[Audio Visualizer] ${message}`);
export { Settings }; export { Settings };
// Basic config with settings
const config = { const config = {
enabled: true, enabled: true,
position: 'left' as 'left' | 'right',
width: 200, width: 200,
height: 40, height: 40,
get barCount() { return settings.barCount; }, get barCount() {
get color() { return settings.barColor; }, return settings.barCount;
get barRounding() { return settings.barRounding; }, },
get color() {
return settings.barColor;
},
get barRounding() {
return settings.barRounding;
},
sensitivity: 1.5, sensitivity: 1.5,
smoothing: 0.8, smoothing: 0.8,
visualizerType: 'bars' as 'bars' | 'waveform' | 'circular'
}; };
// Clean up resources // Clean up resources
@@ -42,30 +42,38 @@ let animationId: number | null = null;
let currentAudioElement: HTMLAudioElement | null = null; let currentAudioElement: HTMLAudioElement | null = null;
let isSourceConnected: boolean = false; let isSourceConnected: boolean = false;
// Canvas and container elements // Each placement gets its own container/canvas/context
let visualizerContainer: HTMLDivElement | null = null; interface VisualizerSlot {
let canvas: HTMLCanvasElement | null = null; container: HTMLDivElement | null;
let canvasContext: CanvasRenderingContext2D | null = null; canvas: HTMLCanvasElement | null;
ctx: CanvasRenderingContext2D | null;
}
const navSlot: VisualizerSlot = { container: null, canvas: null, ctx: null };
const npSlot: VisualizerSlot = { container: null, canvas: null, ctx: null };
// Find the audio element - this is a bit of a hack but it works // Find the audio element - this is a bit of a hack but it works
const findAudioElement = (): HTMLAudioElement | null => { const findAudioElement = (): HTMLAudioElement | null => {
// Try main selectors first // Try main selectors first
const selectors = [ const selectors = [
'audio', "audio",
'video', "video",
'audio[data-test]', "audio[data-test]",
'[data-test="audio-player"] audio' '[data-test="audio-player"] audio',
]; ];
for (const selector of selectors) { for (const selector of selectors) {
const element = document.querySelector(selector) as HTMLAudioElement; const element = document.querySelector(selector) as HTMLAudioElement;
if (element && (element.tagName === 'AUDIO' || element.tagName === 'VIDEO')) { if (
element &&
(element.tagName === "AUDIO" || element.tagName === "VIDEO")
) {
return element; return element;
} }
} }
// Quick scan for any audio elements // Quick scan for any audio elements
const audioElements = document.querySelectorAll('audio, video'); const audioElements = document.querySelectorAll("audio, video");
for (const element of audioElements) { for (const element of audioElements) {
const audioEl = element as HTMLAudioElement; const audioEl = element as HTMLAudioElement;
if (audioEl.src || audioEl.currentSrc) { if (audioEl.src || audioEl.currentSrc) {
@@ -114,7 +122,10 @@ const initializeAudioVisualizer = async (): Promise<void> => {
log("Connected to audio stream with output"); log("Connected to audio stream with output");
} catch (error) { } catch (error) {
// Audio is connected elsewhere - that's fine, we just can't visualize // Audio is connected elsewhere - that's fine, we just can't visualize
if (error instanceof Error && error.message.includes('already connected')) { if (
error instanceof Error &&
error.message.includes("already connected")
) {
log("Audio already connected elsewhere - skipping visualization"); log("Audio already connected elsewhere - skipping visualization");
} }
return; return;
@@ -123,54 +134,29 @@ const initializeAudioVisualizer = async (): Promise<void> => {
// Resume context only if needed and don't wait for it // Resume context only if needed and don't wait for it
// (otherwise it will wait for the audio to start playing) // (otherwise it will wait for the audio to start playing)
if (audioContext.state === 'suspended') { if (audioContext.state === "suspended") {
audioContext.resume().catch(() => {}); // Fire and forget 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 // Start animation only if not already running
if (!animationId) { if (!animationId) {
animate(); animate();
} }
} catch (err) { } catch (err) {
// log errors // log errors
console.error(err); console.error(err);
} }
}; };
// Create the visualizer UI container and canvas const makeSlotElements = (): { container: HTMLDivElement; canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } | null => {
const createVisualizerUI = (): void => { const container = document.createElement("div");
// Remove existing visualizer if it exists container.className = "audio-visualizer-container";
removeVisualizerUI(); container.style.cssText = `
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; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-${config.position === 'left' ? 'right' : 'left'}: 12px;
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
border-radius: 8px; border-radius: 8px;
padding: 4px; padding: 4px;
@@ -178,76 +164,102 @@ const createVisualizerUI = (): void => {
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
`; `;
// Create canvas const cvs = document.createElement("canvas");
canvas = document.createElement('canvas'); cvs.width = config.width;
canvas.width = config.width; cvs.height = config.height;
canvas.height = config.height; cvs.style.cssText = `
canvas.style.cssText = `
width: ${config.width}px; width: ${config.width}px;
height: ${config.height}px; height: ${config.height}px;
border-radius: 4px; border-radius: 4px;
`; `;
visualizerContainer.appendChild(canvas); container.appendChild(cvs);
canvasContext = canvas.getContext('2d'); const ctx = cvs.getContext("2d");
if (!ctx) return null;
// Insert visualizer next to search bar return { container, canvas: cvs, ctx };
if (config.position === 'left') { };
searchContainer.parentElement?.insertBefore(visualizerContainer, searchContainer);
} else { const clearSlot = (slot: VisualizerSlot): void => {
searchContainer.parentElement?.insertBefore(visualizerContainer, searchContainer.nextSibling); slot.container?.remove();
} slot.container = null;
slot.canvas = null;
slot.ctx = null;
};
const ensureNavSlot = (): void => {
if (navSlot.container?.isConnected) return;
clearSlot(navSlot);
const searchField = document.querySelector('input[class*="_searchField"]') as HTMLInputElement;
if (!searchField) return;
const searchContainer = searchField.parentElement;
if (!searchContainer?.parentElement) return;
const els = makeSlotElements();
if (!els) return;
els.container.style.marginRight = "12px";
Object.assign(navSlot, els);
searchContainer.parentElement.insertBefore(els.container, searchContainer);
};
const ensureNpSlot = (): void => {
if (npSlot.container?.isConnected) return;
clearSlot(npSlot);
const artistInfo = document.querySelector('[data-test="artist-info"]');
if (!artistInfo) return;
const leftContent = artistInfo.parentElement;
if (!leftContent) return;
const els = makeSlotElements();
if (!els) return;
els.container.style.marginLeft = "12px";
Object.assign(npSlot, els);
leftContent.insertBefore(els.container, artistInfo.nextSibling);
};
const createVisualizerUI = (): void => {
if (!config.enabled) return;
ensureNavSlot();
ensureNpSlot();
}; };
// Remove visualizer UI
const removeVisualizerUI = (): void => { const removeVisualizerUI = (): void => {
if (visualizerContainer) { clearSlot(navSlot);
visualizerContainer.remove(); clearSlot(npSlot);
visualizerContainer = null;
canvas = null;
canvasContext = null;
}
}; };
// Animation loop for rendering visualizer // Animation loop for rendering visualizer
const animate = (): void => { const animate = (): void => {
if (!canvasContext || !canvas) { // Re-attach slots that got disconnected from the DOM
animationId = null; createVisualizerUI();
const slots = [navSlot, npSlot].filter(s => s.ctx && s.canvas);
if (slots.length === 0) {
animationId = requestAnimationFrame(animate);
return; return;
} }
// Update canvas color in case it changed
canvasContext.fillStyle = config.color;
canvasContext.strokeStyle = config.color;
// Check if we have real audio data - this might not be needed but its a good idea
let hasRealAudio = false; let hasRealAudio = false;
if (analyser && dataArray) { if (analyser && dataArray) {
analyser.getByteFrequencyData(dataArray); analyser.getByteFrequencyData(dataArray);
// Check if there's actual audio signal (not just silence) const avgVolume =
const avgVolume = dataArray.reduce((sum, val) => sum + val, 0) / dataArray.length; dataArray.reduce((sum, val) => sum + val, 0) / dataArray.length;
hasRealAudio = avgVolume > 5; // Threshold for detecting actual audio hasRealAudio = avgVolume > 5;
} }
// Clear canvas for (const slot of slots) {
canvasContext.clearRect(0, 0, canvas.width, canvas.height); const ctx = slot.ctx!;
const cvs = slot.canvas!;
ctx.fillStyle = config.color;
ctx.strokeStyle = config.color;
ctx.clearRect(0, 0, cvs.width, cvs.height);
if (hasRealAudio && analyser && dataArray) { if (hasRealAudio && analyser && dataArray) {
// Draw real audio visualization drawBars(ctx, cvs);
switch (config.visualizerType) {
case 'bars': // Is implemented YAYYY (default)
drawBars();
break;
case 'waveform': // Not implemented yet
drawWaveform();
break;
case 'circular': // Not implemented yet
drawCircular();
break;
}
} else { } else {
// Draw cool scrolling wave effect when no audio drawScrollingWave(ctx, cvs);
drawScrollingWave(); }
} }
animationId = requestAnimationFrame(animate); animationId = requestAnimationFrame(animate);
@@ -257,73 +269,67 @@ const animate = (): void => {
let waveTime = 0; let waveTime = 0;
// Helper function to draw rounded rectangles // Helper function to draw rounded rectangles
const drawRoundedRect = (ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number): void => { const drawRoundedRect = (
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
radius: number,
): void => {
ctx.beginPath(); ctx.beginPath();
ctx.roundRect(x, y, width, height, radius); ctx.roundRect(x, y, width, height, radius);
ctx.fill(); ctx.fill();
}; };
// Draw scrolling wave effect when no audio is detected const drawScrollingWave = (ctx: CanvasRenderingContext2D, cvs: HTMLCanvasElement): void => {
const drawScrollingWave = (): void => { waveTime += 0.05 / [navSlot, npSlot].filter(s => s.ctx).length;
if (!canvasContext || !canvas) return;
waveTime += 0.05; // Speed of wave animation
const barCount = config.barCount; const barCount = config.barCount;
const barWidth = canvas.width / barCount; const barWidth = cvs.width / barCount;
const maxHeight = canvas.height * 0.6; const maxHeight = cvs.height * 0.6;
canvasContext.fillStyle = config.color; ctx.fillStyle = config.color;
for (let i = 0; i < barCount; i++) { for (let i = 0; i < barCount; i++) {
// Create a sine wave that scrolls back and forth
const x = i / barCount; const x = i / barCount;
const wave1 = Math.sin(x * Math.PI * 2 + waveTime) * 0.3; const wave1 = Math.sin(x * Math.PI * 2 + waveTime) * 0.3;
const wave2 = Math.sin(x * Math.PI * 4 + waveTime * 1.3) * 0.2; const wave2 = Math.sin(x * Math.PI * 4 + waveTime * 1.3) * 0.2;
const wave3 = Math.sin(x * Math.PI * 6 + waveTime * 0.7) * 0.1; const wave3 = Math.sin(x * Math.PI * 6 + waveTime * 0.7) * 0.1;
const combinedWave = (wave1 + wave2 + wave3 + 1) / 2;
// Combine waves for complex pattern
const combinedWave = (wave1 + wave2 + wave3 + 1) / 2; // Normalize to 0-1
// Add a traveling wave effect
const travelWave = Math.sin(x * Math.PI * 3 - waveTime * 2) * 0.5 + 0.5; const travelWave = Math.sin(x * Math.PI * 3 - waveTime * 2) * 0.5 + 0.5;
const barHeight = maxHeight * combinedWave * travelWave * 0.8 + 2;
// Final height calculation
const barHeight = maxHeight * combinedWave * travelWave * 0.8 + 2; // Minimum height of 2px
const xPos = i * barWidth; const xPos = i * barWidth;
const yPos = (canvas.height - barHeight) / 2; const yPos = (cvs.height - barHeight) / 2;
// Draw rounded or square bars based on setting
if (config.barRounding) { if (config.barRounding) {
drawRoundedRect(canvasContext, xPos, yPos, barWidth - 1, barHeight, 2); drawRoundedRect(ctx, xPos, yPos, barWidth - 1, barHeight, 2);
} else { } else {
canvasContext.fillRect(xPos, yPos, barWidth - 1, barHeight); ctx.fillRect(xPos, yPos, barWidth - 1, barHeight);
} }
} }
}; };
// Draw frequency bars - default const drawBars = (ctx: CanvasRenderingContext2D, cvs: HTMLCanvasElement): void => {
const drawBars = (): void => { if (!dataArray) return;
if (!canvasContext || !dataArray || !canvas) return;
const barWidth = canvas.width / config.barCount; const barWidth = cvs.width / config.barCount;
const heightScale = canvas.height / 255; const heightScale = cvs.height / 255;
canvasContext.fillStyle = config.color; ctx.fillStyle = config.color;
for (let i = 0; i < config.barCount; i++) { for (let i = 0; i < config.barCount; i++) {
const dataIndex = Math.floor(i * (dataArray.length / config.barCount)); const dataIndex = Math.floor(i * (dataArray.length / config.barCount));
const barHeight = (dataArray[dataIndex] * config.sensitivity * heightScale); const barHeight = dataArray[dataIndex] * config.sensitivity * heightScale;
const x = i * barWidth; const x = i * barWidth;
const y = canvas.height - barHeight; const y = cvs.height - barHeight;
// Draw rounded or square bars based on setting
if (config.barRounding) { if (config.barRounding) {
drawRoundedRect(canvasContext, x, y, barWidth - 1, barHeight, 2); drawRoundedRect(ctx, x, y, barWidth - 1, barHeight, 2);
} else { } else {
canvasContext.fillRect(x, y, barWidth - 1, barHeight); ctx.fillRect(x, y, barWidth - 1, barHeight);
} }
} }
}; };
@@ -384,23 +390,23 @@ const drawBars = (): void => {
// } // }
// }; // };
// Update visualizer settings
const updateAudioVisualizer = (): void => { const updateAudioVisualizer = (): void => {
if (analyser) { if (analyser) {
// use a fixed size that provides enough frequency bins analyser.fftSize = 512;
analyser.fftSize = 512; // Fixed power of 2 - important
analyser.smoothingTimeConstant = config.smoothing; analyser.smoothingTimeConstant = config.smoothing;
dataArray = new Uint8Array(analyser.frequencyBinCount); dataArray = new Uint8Array(analyser.frequencyBinCount);
} }
if (canvas) { for (const slot of [navSlot, npSlot]) {
canvas.width = config.width; if (slot.canvas) {
canvas.height = config.height; slot.canvas.width = config.width;
canvas.style.width = `${config.width}px`; slot.canvas.height = config.height;
canvas.style.height = `${config.height}px`; slot.canvas.style.width = `${config.width}px`;
slot.canvas.style.height = `${config.height}px`;
}
} }
// Recreate UI if position changed removeVisualizerUI();
createVisualizerUI(); createVisualizerUI();
}; };
@@ -456,7 +462,8 @@ const observePlayState = (): void => {
// Start with fast checking, then slow down // Start with fast checking, then slow down
const fastInterval = setInterval(() => { const fastInterval = setInterval(() => {
checkAndInitialize(); checkAndInitialize();
if (checkCount > 10) { // After 10 quick checks, switch to slower if (checkCount > 10) {
// After 10 quick checks, switch to slower
clearInterval(fastInterval); clearInterval(fastInterval);
const slowInterval = setInterval(checkAndInitialize, 2000); const slowInterval = setInterval(checkAndInitialize, 2000);
unloads.add(() => clearInterval(slowInterval)); unloads.add(() => clearInterval(slowInterval));
@@ -507,7 +514,7 @@ const completeCleanup = (): void => {
} }
// Close audio context completely on plugin unload // Close audio context completely on plugin unload
if (audioContext && audioContext.state !== 'closed') { if (audioContext && audioContext.state !== "closed") {
audioContext.close(); audioContext.close();
log("Closed AudioContext"); log("Closed AudioContext");
} }
+11 -17
View File
@@ -1,46 +1,40 @@
/* Audio Visualizer CSS - Only applies to the Visualizer */ /* Audio Visualizer CSS */
#audio-visualizer-container { .audio-visualizer-container {
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
animation: av-fadeIn 0.5s ease-out;
} }
#audio-visualizer-container:hover { .audio-visualizer-container:hover {
transform: scale(1.02); transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
} }
#audio-visualizer-container canvas { .audio-visualizer-container canvas {
display: block; display: block;
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
} }
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
#audio-visualizer-container { .audio-visualizer-container {
margin: 4px; margin: 4px;
padding: 2px; padding: 2px;
} }
#audio-visualizer-container canvas { .audio-visualizer-container canvas {
max-width: 150px; max-width: 150px;
max-height: 30px; max-height: 30px;
} }
} }
/* Where to put the thingy */ .audio-visualizer-container.active {
[class*="_searchField"] {
transition: all 0.3s ease-in-out;
}
/* Shadow when active - doesnt seem to only apply when active but thats better */
#audio-visualizer-container.active {
box-shadow: 0 0 20px rgba(255, 255, 255, 0.3); box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
} }
/* Fade in animation */ @keyframes av-fadeIn {
@keyframes fadeIn {
from { from {
opacity: 0; opacity: 0;
transform: scale(0.8); transform: scale(0.8);
@@ -51,6 +45,6 @@
} }
} }
#audio-visualizer-container { [data-type="search-field"] {
animation: fadeIn 0.5s ease-out; min-width: 220px !important;
} }
@@ -1,6 +1,6 @@
{ {
"name": "@meowarex/oled-theme", "name": "@meowarex/colorama-lyrics",
"description": "A dark OLED-friendly theme plugin that transforms Tidal Luna's appearance.", "description": "Customize lyrics colors: single, gradient & auto from cover art",
"author": { "author": {
"name": "meowarex", "name": "meowarex",
"url": "https://github.com/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;
}
+45 -20
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 { StyleTag } from "@luna/lib";
// Import CSS directly using Luna's file:// syntax - Took me a while to figure out <3 // Import CSS directly using Luna's file:// syntax - Took me a while to figure out <3
@@ -9,8 +9,8 @@ export const { trace } = Tracer("[Copy Lyrics]");
// clean up resources // clean up resources
export const unloads = new Set<LunaUnload>(); export const unloads = new Set<LunaUnload>();
// StyleTag for lyrics selection styling // Style injection via side effect
const lyricsStyleTag = new StyleTag("Copy-Lyrics", unloads, unlockSelection); new StyleTag("Copy-Lyrics", unloads, unlockSelection);
function SetClipboard(text: string): void { function SetClipboard(text: string): void {
const textarea = document.createElement("textarea"); const textarea = document.createElement("textarea");
@@ -31,36 +31,50 @@ function SetClipboard(text: string): void {
let isSelecting = false; let isSelecting = false;
const onMouseDown = function (): void { const onMouseDown = (): void => {
isSelecting = true; isSelecting = true;
}; };
const onMouseUp = function (event: MouseEvent): void { const onMouseUp = (): void => {
if (isSelecting) { if (isSelecting) {
const selection = window.getSelection(); const selection = window.getSelection();
if (selection && selection.toString().length > 0) { if (selection?.toString().length > 0) {
const selectedSpans: HTMLSpanElement[] = []; const selectedSpans: HTMLSpanElement[] = [];
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
let container = range.commonAncestorContainer; let container: Node | null = range.commonAncestorContainer;
// If the container is NOT an element and a document, adjust it. // 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;
}
// If parent has data-current, treat as single-line copy case
if ( if (
container.nodeType !== Node.ELEMENT_NODE && container &&
container.nodeType !== Node.DOCUMENT_NODE container.nodeType === Node.ELEMENT_NODE &&
(container as Element).hasAttribute("data-current")
) { ) {
// Get the parent element if it's a text node const text_ = selection.toString().trim();
const parentElement = container.parentElement;
if (parentElement && parentElement.hasAttribute("data-current")) {
let text_ = selection.toString().trim();
SetClipboard(text_); SetClipboard(text_);
trace.msg.log("Copied to clipboard!"); trace.msg.log("Copied to clipboard!");
return; return;
} }
// Ensure we have an Element or Document before querying
if (
!container ||
(container.nodeType !== Node.ELEMENT_NODE &&
container.nodeType !== Node.DOCUMENT_NODE)
) {
isSelecting = false;
return;
} }
// Get all the spans inside the container. // Get all the spans inside the container.
const spans = (container as Element).getElementsByTagName("span"); const spans = (container as Element | Document).getElementsByTagName(
for (let span of spans) { "span",
);
for (const span of spans) {
if (selection.containsNode(span, true)) { if (selection.containsNode(span, true)) {
selectedSpans.push(span as HTMLSpanElement); selectedSpans.push(span as HTMLSpanElement);
} }
@@ -73,7 +87,11 @@ const onMouseUp = function (event: MouseEvent): void {
if (span.hasAttribute("data-current")) { if (span.hasAttribute("data-current")) {
hasCorrectAttribute = true; hasCorrectAttribute = true;
text += span.textContent + "\n"; text += span.textContent + "\n";
if ([...span.classList].some((className) => className.startsWith("endOfStanza--"))) { if (
[...span.classList].some((className) =>
className.startsWith("endOfStanza--"),
)
) {
text += "\n"; text += "\n";
} }
} }
@@ -91,26 +109,33 @@ const onMouseUp = function (event: MouseEvent): void {
} }
}; };
const onClickHooked = function (event: MouseEvent): boolean | void { const onClickHooked = (event: MouseEvent): boolean | undefined => {
if (!isSelecting) return; if (!isSelecting) return;
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
if (target.tagName.toLowerCase() === "span" && target.hasAttribute("data-current")) { if (
target.tagName.toLowerCase() === "span" &&
target.hasAttribute("data-current")
) {
// Prevent default behavior and stop event propagation // Prevent default behavior and stop event propagation
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
event.stopImmediatePropagation(); event.stopImmediatePropagation();
return false; return false;
} }
return undefined;
}; };
// Add event listener with capture phase to intercept events before they reach other handlers // Add event listener with capture phase to intercept events before they reach other handlers
document.addEventListener("click", onClickHooked, true); document.addEventListener("click", onClickHooked, true);
document.addEventListener("mousedown", onMouseDown); document.addEventListener("mousedown", onMouseDown);
document.addEventListener("mouseup", onMouseUp); document.addEventListener("mouseup", onMouseUp);
// Add cleanup to unloads // Add cleanup to unloads
unloads.add(() => { unloads.add((): void => {
// Remove event listeners // Remove event listeners
document.removeEventListener("click", onClickHooked, true); document.removeEventListener("click", onClickHooked, true);
document.removeEventListener("mousedown", onMouseDown); document.removeEventListener("mousedown", onMouseDown);
+1 -1
View File
@@ -1,4 +1,4 @@
[class^="_lyricsText"]>div>span { [class^="_lyricsText"] > div > span {
user-select: text; user-select: text;
cursor: text; cursor: text;
} }
+1 -1
View File
@@ -9,7 +9,7 @@ export const settings = await ReactiveStore.getPluginStorage("ElementHider", {
className: string; className: string;
textContent: string; textContent: string;
timestamp: number; timestamp: number;
}> }>,
}); });
export const Settings = () => { export const Settings = () => {
+156 -89
View File
@@ -1,5 +1,5 @@
import { LunaUnload, Tracer } from "@luna/core"; import { type LunaUnload, Tracer } from "@luna/core";
import { StyleTag, ContextMenu } from "@luna/lib"; import { StyleTag } from "@luna/lib";
import { settings, Settings } from "./Settings"; import { settings, Settings } from "./Settings";
// Import CSS directly using Luna's file:// syntax // Import CSS directly using Luna's file:// syntax
@@ -13,8 +13,8 @@ export { Settings };
// Clean up resources // Clean up resources
export const unloads = new Set<LunaUnload>(); export const unloads = new Set<LunaUnload>();
// StyleTag for element hider // StyleTag for element hider (side-effect)
const styleTag = new StyleTag("Element-Hider", unloads, styles); new StyleTag("Element-Hider", unloads, styles);
// State management // State management
let targetElement: HTMLElement | null = null; let targetElement: HTMLElement | null = null;
@@ -32,7 +32,7 @@ function generateElementSelector(element: HTMLElement): string {
} }
// Priority 2: data-test attribute (very specific for Tidal <3) // Priority 2: data-test attribute (very specific for Tidal <3)
const dataTest = element.getAttribute('data-test'); const dataTest = element.getAttribute("data-test");
if (dataTest) { if (dataTest) {
return `[data-test="${dataTest}"]`; return `[data-test="${dataTest}"]`;
} }
@@ -41,28 +41,43 @@ function generateElementSelector(element: HTMLElement): string {
let selector = element.tagName.toLowerCase(); let selector = element.tagName.toLowerCase();
// Get filtered classes (exclude our temporary classes) // Get filtered classes (exclude our temporary classes)
const classes = element.className ? element.className.trim().split(/\s+/).filter(cls => { const classes = element.className
return cls.length > 0 && ? element.className
!cls.startsWith('element-hider-') && .trim()
cls !== 'element-hider-target' && .split(/\s+/)
cls !== 'element-hider-hiding' && .filter((cls) => {
cls !== 'element-hider-hidden'; 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 // Only use classes if we have them and they're not generic and dumb
if (classes.length > 0) { if (classes.length > 0) {
// Use ALL classes to be very specific // Use ALL classes to be very specific
selector += '.' + classes.join('.'); selector += "." + classes.join(".");
// Add parent context for extra specificity (for when the element is inside another element) // Add parent context for extra specificity (for when the element is inside another element)
const parent = element.parentElement; const parent = element.parentElement;
if (parent && parent.tagName !== 'BODY' && parent.tagName !== 'HTML') { if (parent && parent.tagName !== "BODY" && parent.tagName !== "HTML") {
const parentClasses = parent.className ? parent.className.trim().split(/\s+/).filter(cls => { const parentClasses = parent.className
return cls.length > 0 && !cls.startsWith('element-hider-'); ? parent.className
}) : []; .trim()
.split(/\s+/)
.filter((cls) => {
return cls.length > 0 && !cls.startsWith("element-hider-");
})
: [];
if (parentClasses.length > 0) { 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}`; selector = `${parentSelector} > ${selector}`;
} }
} }
@@ -70,19 +85,29 @@ function generateElementSelector(element: HTMLElement): string {
// If no useful classes, use position-based selector with parent context // If no useful classes, use position-based selector with parent context
const parent = element.parentElement; const parent = element.parentElement;
if (parent) { if (parent) {
const siblings = Array.from(parent.children).filter(child => child.tagName === element.tagName); const siblings = Array.from(parent.children).filter(
(child) => child.tagName === element.tagName,
);
const index = siblings.indexOf(element); const index = siblings.indexOf(element);
if (index >= 0) { if (index >= 0) {
selector += `:nth-of-type(${index + 1})`; selector += `:nth-of-type(${index + 1})`;
// Add parent context // Add parent context
if (parent.tagName !== 'BODY' && parent.tagName !== 'HTML') { if (parent.tagName !== "BODY" && parent.tagName !== "HTML") {
const parentClasses = parent.className ? parent.className.trim().split(/\s+/).filter(cls => { const parentClasses = parent.className
return cls.length > 0 && !cls.startsWith('element-hider-'); ? parent.className
}) : []; .trim()
.split(/\s+/)
.filter((cls) => {
return cls.length > 0 && !cls.startsWith("element-hider-");
})
: [];
if (parentClasses.length > 0) { 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}`; selector = `${parentSelector} > ${selector}`;
} }
} }
@@ -100,14 +125,14 @@ function saveHiddenElement(element: HTMLElement): void {
const elementInfo = { const elementInfo = {
selector: selector, selector: selector,
tagName: element.tagName, tagName: element.tagName,
className: element.className || '', className: element.className || "",
textContent: element.textContent?.substring(0, 100) || '', textContent: element.textContent?.substring(0, 100) || "",
timestamp: Date.now() timestamp: Date.now(),
}; };
// Check if element is already saved // Check if element is already saved
const existingIndex = settings.hiddenElements.findIndex( const existingIndex = settings.hiddenElements.findIndex(
stored => stored.selector === elementInfo.selector (stored) => stored.selector === elementInfo.selector,
); );
if (existingIndex === -1) { if (existingIndex === -1) {
@@ -119,17 +144,18 @@ function saveHiddenElement(element: HTMLElement): void {
} }
} }
// Remove hidden element from persistent storage (for unhiding) // Remove hidden element from persistent storage (for unhiding) - currently unused
function removeSavedElement(element: HTMLElement): void { // function removeSavedElement(element: HTMLElement): void {
const selector = generateElementSelector(element); // const selector = generateElementSelector(element);
const index = settings.hiddenElements.findIndex(stored => stored.selector === selector); // const index = settings.hiddenElements.findIndex(
// (stored) => stored.selector === selector,
if (index !== -1) { // );
settings.hiddenElements.splice(index, 1); // if (index !== -1) {
trace.log(`Permanently removed: ${selector}`); // settings.hiddenElements.splice(index, 1);
trace.log(`Remaining stored: ${settings.hiddenElements.length}`); // trace.log(`Permanently removed: ${selector}`);
} // trace.log(`Remaining stored: ${settings.hiddenElements.length}`);
} // }
// }
// Check if an element matches any stored selector (EXACT match only) // Check if an element matches any stored selector (EXACT match only)
function matchesStoredSelector(element: HTMLElement): boolean { function matchesStoredSelector(element: HTMLElement): boolean {
@@ -154,14 +180,18 @@ function hideElementDirectly(element: HTMLElement): void {
element.classList.add("element-hider-hidden"); element.classList.add("element-hider-hidden");
hiddenElements.add(element); hiddenElements.add(element);
hiddenElementsArray.push(element); hiddenElementsArray.push(element);
trace.log(`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 // Hide the target element with animation
function hideTargetElement(): void { function hideTargetElement(): void {
if (!targetElement) return; 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 // Add hiding animation class
targetElement.classList.add("element-hider-hiding"); targetElement.classList.add("element-hider-hiding");
@@ -175,7 +205,10 @@ function hideTargetElement(): void {
// Wait for animation to complete, then hide // Wait for animation to complete, then hide
setTimeout(() => { setTimeout(() => {
elementToHide.classList.add("element-hider-hidden"); elementToHide.classList.add("element-hider-hidden");
elementToHide.classList.remove("element-hider-hiding", "element-hider-target"); elementToHide.classList.remove(
"element-hider-hiding",
"element-hider-target",
);
hiddenElements.add(elementToHide); hiddenElements.add(elementToHide);
hiddenElementsArray.push(elementToHide); hiddenElementsArray.push(elementToHide);
}, 300); }, 300);
@@ -186,10 +219,12 @@ function hideTargetElement(): void {
// Unhide all elements permanently (remove from storage) // Unhide all elements permanently (remove from storage)
function unhideAllElements(): void { function unhideAllElements(): void {
trace.log(`Permanently unhiding ${settings.hiddenElements.length} saved elements`); trace.log(
`Permanently unhiding ${settings.hiddenElements.length} saved elements`,
);
// Show all currently hidden elements // Show all currently hidden elements
hiddenElementsArray.forEach(element => { hiddenElementsArray.forEach((element) => {
if (document.body.contains(element)) { if (document.body.contains(element)) {
element.classList.remove("element-hider-hidden", "element-hider-hiding"); element.classList.remove("element-hider-hidden", "element-hider-hiding");
} }
@@ -205,7 +240,9 @@ function unhideAllElements(): void {
function processAllElements(): void { function processAllElements(): void {
if (settings.hiddenElements.length === 0) return; 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; let hiddenCount = 0;
// Use querySelectorAll for each stored selector with validation // Use querySelectorAll for each stored selector with validation
@@ -217,7 +254,9 @@ function processAllElements(): void {
// Limit to prevent over-hiding (safety check) // Limit to prevent over-hiding (safety check)
if (elements.length > 10) { if (elements.length > 10) {
trace.warn(`Selector too broad (${elements.length} matches), skipping: ${storedElement.selector}`); trace.warn(
`Selector too broad (${elements.length} matches), skipping: ${storedElement.selector}`,
);
return; return;
} }
@@ -226,7 +265,9 @@ function processAllElements(): void {
if (!hiddenElements.has(htmlElement)) { if (!hiddenElements.has(htmlElement)) {
hideElementDirectly(htmlElement); hideElementDirectly(htmlElement);
hiddenCount++; 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) { } catch (error) {
@@ -241,7 +282,7 @@ function processAllElements(): void {
// Process new elements that are added to the DOM // Process new elements that are added to the DOM
function processNewElements(addedNodes: NodeList): void { function processNewElements(addedNodes: NodeList): void {
addedNodes.forEach(node => { addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return; if (node.nodeType !== Node.ELEMENT_NODE) return;
const element = node as HTMLElement; const element = node as HTMLElement;
@@ -252,8 +293,8 @@ function processNewElements(addedNodes: NodeList): void {
} }
// Check all descendant elements // Check all descendant elements
const descendants = element.querySelectorAll('*'); const descendants = element.querySelectorAll("*");
descendants.forEach(descendant => { descendants.forEach((descendant) => {
if (matchesStoredSelector(descendant as HTMLElement)) { if (matchesStoredSelector(descendant as HTMLElement)) {
hideElementDirectly(descendant as HTMLElement); hideElementDirectly(descendant as HTMLElement);
} }
@@ -267,7 +308,7 @@ function setupElementObserver(): void {
elementObserver = new MutationObserver((mutations) => { elementObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
processNewElements(mutation.addedNodes); processNewElements(mutation.addedNodes);
} }
}); });
@@ -275,15 +316,22 @@ function setupElementObserver(): void {
elementObserver.observe(document.body, { elementObserver.observe(document.body, {
childList: true, childList: true,
subtree: true subtree: true,
}); });
trace.log(`Set up reactive element observer`); trace.log(`Set up reactive element observer`);
} }
// Global functions // Global functions
(window as any).showAllElementsFromSettings = unhideAllElements; declare global {
(window as any).debugElementHider = () => { interface Window {
showAllElementsFromSettings?: () => void;
debugElementHider?: () => void;
}
}
window.showAllElementsFromSettings = unhideAllElements;
window.debugElementHider = () => {
trace.log(`=== Element Hider Debug Info ===`); trace.log(`=== Element Hider Debug Info ===`);
trace.log(`Stored elements: ${settings.hiddenElements.length}`); trace.log(`Stored elements: ${settings.hiddenElements.length}`);
trace.log(`Currently hidden elements: ${hiddenElementsArray.length}`); trace.log(`Currently hidden elements: ${hiddenElementsArray.length}`);
@@ -297,19 +345,19 @@ function setupElementObserver(): void {
// Handle highlighting target element // Handle highlighting target element
function highlightElement(element: HTMLElement): void { function highlightElement(element: HTMLElement): void {
// Remove previous highlights // Remove previous highlights
document.querySelectorAll('.element-hider-target').forEach(el => { document.querySelectorAll(".element-hider-target").forEach((el) => {
el.classList.remove('element-hider-target'); el.classList.remove("element-hider-target");
}); });
// Highlight current element // Highlight current element
element.classList.add('element-hider-target'); element.classList.add("element-hider-target");
targetElement = element; targetElement = element;
} }
// Remove highlight // Remove highlight
function removeHighlight(): void { function removeHighlight(): void {
if (targetElement) { if (targetElement) {
targetElement.classList.remove('element-hider-target'); targetElement.classList.remove("element-hider-target");
targetElement = null; targetElement = null;
} }
} }
@@ -321,11 +369,17 @@ let contextMenuTimeout: number | null = null;
let waitingForBuiltInMenu = false; let waitingForBuiltInMenu = false;
// Listen for right-click events to capture the target for context menu // Listen for right-click events to capture the target for context menu
document.addEventListener('contextmenu', (event: MouseEvent) => { document.addEventListener(
"contextmenu",
(event: MouseEvent) => {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
// Don't interfere with native context menus on inputs, textareas, etc. // Don't interfere with native context menus on inputs, textareas, etc.
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) { if (
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable
) {
currentContextElement = null; currentContextElement = null;
return; return;
} }
@@ -346,8 +400,7 @@ document.addEventListener('contextmenu', (event: MouseEvent) => {
const eventX = event.clientX; const eventX = event.clientX;
const eventY = event.clientY; const eventY = event.clientY;
// Prevent default immediately if we plan to handle it // Allow native context menu by default; we'll show our custom menu only if needed
event.preventDefault();
// Wait to see if the built-in context menu appears // Wait to see if the built-in context menu appears
contextMenuTimeout = window.setTimeout(() => { contextMenuTimeout = window.setTimeout(() => {
@@ -359,10 +412,14 @@ document.addEventListener('contextmenu', (event: MouseEvent) => {
}, 150); // Wait 150ms for built-in menu }, 150); // Wait 150ms for built-in menu
// Don't prevent default initially - let Luna try to handle the context menu // Don't prevent default initially - let Luna try to handle the context menu
}, true); },
true,
);
// Listen for clicks to close custom menu // Listen for clicks to close custom menu
document.addEventListener('click', (event: MouseEvent) => { document.addEventListener(
"click",
(event: MouseEvent) => {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
// If clicking outside our custom menu, close it // If clicking outside our custom menu, close it
@@ -370,10 +427,12 @@ document.addEventListener('click', (event: MouseEvent) => {
closeCustomMenu(); closeCustomMenu();
removeHighlight(); removeHighlight();
} }
}, true); },
true,
);
// Handle escape key to close custom menu and remove highlights // Handle escape key to close custom menu and remove highlights
document.addEventListener('keydown', (event: KeyboardEvent) => { document.addEventListener("keydown", (event: KeyboardEvent) => {
if (event.key === "Escape") { if (event.key === "Escape") {
if (customMenu) { if (customMenu) {
closeCustomMenu(); closeCustomMenu();
@@ -464,8 +523,15 @@ const contextMenuObserver = new MutationObserver((mutations) => {
const element = node as HTMLElement; const element = node as HTMLElement;
// Look for Tidal's context menu // Look for Tidal's context menu
if (element.matches('[data-test="contextmenu"]') || element.querySelector('[data-test="contextmenu"]')) { if (
const contextMenu = element.matches('[data-test="contextmenu"]') ? element : element.querySelector('[data-test="contextmenu"]') as HTMLElement; 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) { if (contextMenu && currentContextElement && waitingForBuiltInMenu) {
// Built-in menu appeared, cancel custom menu timeout // 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 // Add our options to the existing context menu
function addElementHiderOptions(contextMenu: HTMLElement): void { function addElementHiderOptions(contextMenu: HTMLElement): void {
// Create hide element button // Create hide element button
const hideButton = document.createElement('button'); const hideButton = document.createElement("button");
hideButton.className = 'element-hider-menu-item'; hideButton.className = "element-hider-menu-item";
hideButton.style.cssText = ` hideButton.style.cssText = `
display: flex; display: flex;
align-items: center; align-items: center;
@@ -503,7 +569,7 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
`; `;
hideButton.innerHTML = `Hide This Element`; hideButton.innerHTML = `Hide This Element`;
hideButton.addEventListener('click', () => { hideButton.addEventListener("click", () => {
if (currentContextElement) { if (currentContextElement) {
targetElement = currentContextElement; targetElement = currentContextElement;
hideTargetElement(); hideTargetElement();
@@ -511,37 +577,38 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
}); });
// Add hover effects for highlighting // Add hover effects for highlighting
hideButton.addEventListener('mouseenter', () => { hideButton.addEventListener("mouseenter", () => {
hideButton.style.background = 'var(--wave-color-background-hover, #3a3a3a)'; hideButton.style.background = "var(--wave-color-background-hover, #3a3a3a)";
if (currentContextElement) { if (currentContextElement) {
highlightElement(currentContextElement); highlightElement(currentContextElement);
} }
}); });
hideButton.addEventListener('mouseleave', () => { hideButton.addEventListener("mouseleave", () => {
hideButton.style.background = 'transparent'; hideButton.style.background = "transparent";
removeHighlight(); removeHighlight();
}); });
// Create unhide all button // Create unhide all button
const unhideAllButton = document.createElement('button'); const unhideAllButton = document.createElement("button");
unhideAllButton.className = 'element-hider-menu-item'; unhideAllButton.className = "element-hider-menu-item";
unhideAllButton.style.cssText = hideButton.style.cssText; unhideAllButton.style.cssText = hideButton.style.cssText;
unhideAllButton.innerHTML = `Unhide All Elements (${hiddenElementsArray.length})`; unhideAllButton.innerHTML = `Unhide All Elements (${hiddenElementsArray.length})`;
unhideAllButton.addEventListener('click', unhideAllElements); unhideAllButton.addEventListener("click", unhideAllElements);
// Add hover effects for unhide all button // Add hover effects for unhide all button
unhideAllButton.addEventListener('mouseenter', () => { unhideAllButton.addEventListener("mouseenter", () => {
unhideAllButton.style.background = 'var(--wave-color-background-hover, #3a3a3a)'; unhideAllButton.style.background =
"var(--wave-color-background-hover, #3a3a3a)";
}); });
unhideAllButton.addEventListener('mouseleave', () => { unhideAllButton.addEventListener("mouseleave", () => {
unhideAllButton.style.background = 'transparent'; unhideAllButton.style.background = "transparent";
}); });
// Add a separator if the menu has other items // Add a separator if the menu has other items
if (contextMenu.children.length > 0) { if (contextMenu.children.length > 0) {
const separator = document.createElement('div'); const separator = document.createElement("div");
separator.style.cssText = ` separator.style.cssText = `
height: 1px; height: 1px;
background: var(--wave-color-border, #444); background: var(--wave-color-border, #444);
@@ -558,10 +625,10 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
// Start observing for context menus // Start observing for context menus
contextMenuObserver.observe(document.body, { contextMenuObserver.observe(document.body, {
childList: true, childList: true,
subtree: true subtree: true,
}); });
// Initialize plugin // Initialize plugin
function initializePlugin() { function initializePlugin() {
trace.log("Initializing plugin..."); trace.log("Initializing plugin...");
@@ -578,8 +645,8 @@ function initializePlugin() {
} }
// Run initialization when DOM is ready // Run initialization when DOM is ready
if (document.readyState === 'loading') { if (document.readyState === "loading") {
document.addEventListener('DOMContentLoaded', initializePlugin); document.addEventListener("DOMContentLoaded", initializePlugin);
} else { } else {
initializePlugin(); initializePlugin();
} }
@@ -600,8 +667,8 @@ unloads.add(() => {
removeHighlight(); removeHighlight();
// Clean up global functions // Clean up global functions
(window as any).showAllElementsFromSettings = undefined; window.showAllElementsFromSettings = undefined;
(window as any).debugElementHider = undefined; window.debugElementHider = undefined;
trace.log("Plugin unloaded"); trace.log("Plugin unloaded");
}); });
+3 -1
View File
@@ -57,7 +57,9 @@
/* Animation for hiding */ /* Animation for hiding */
.element-hider-hiding { .element-hider-hiding {
transition: opacity 0.3s ease, transform 0.3s ease; transition:
opacity 0.3s ease,
transform 0.3s ease;
opacity: 0; opacity: 0;
transform: scale(0.95); 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
@@ -43,21 +43,16 @@
backface-visibility: hidden; backface-visibility: hidden;
} }
/* Performance mode optimizations - keep spinning but optimize other aspects */ /* Hide Tidal's native now-playing background color overlay */
.global-spinning-image.performance-mode-static { [data-test="new-now-playing"] > [class*="_background_"] {
/* Keep animation enabled in performance mode */ /* biome-ignore lint: Must override native album-art-derived background */
/* Lighter blur for performance */ display: none !important;
filter: blur(20px) brightness(0.4) contrast(1.2) saturate(1) !important;
/* Smaller size for performance */
width: 120vw !important;
height: 120vh !important;
} }
.now-playing-background-image.performance-mode-static { /* Ensure the now-playing container itself is transparent */
/* Keep animation enabled in performance mode */ [class*="_nowPlayingContainer"] {
/* Optimized size and effects for performance */ /* biome-ignore lint: Must override any inline background styles */
width: 80vw !important; background: transparent !important;
height: 80vh !important;
} }
/* Now Playing Background Container Optimization */ /* Now Playing Background Container Optimization */
@@ -67,7 +62,7 @@
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: -3; z-index: 0;
pointer-events: none; pointer-events: none;
overflow: hidden; overflow: hidden;
/* Hardware acceleration */ /* Hardware acceleration */
@@ -75,6 +70,14 @@
backface-visibility: hidden; backface-visibility: hidden;
} }
/* Ensure now-playing content renders above the dynamic background */
[data-test="new-now-playing"] > header,
[data-test="new-now-playing"] > [class*="_content_"],
[data-test="new-now-playing"] > .unhide-ui-button {
position: relative;
z-index: 1;
}
/* Optimized keyframe animations with GPU acceleration */ /* Optimized keyframe animations with GPU acceleration */
@keyframes spinGlobal { @keyframes spinGlobal {
from { from {
@@ -89,8 +92,11 @@
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.global-spinning-image, .global-spinning-image,
.now-playing-background-image { .now-playing-background-image {
/* biome-ignore lint: Accessibility override needs priority */
animation: none !important; animation: none !important;
/* biome-ignore lint: Accessibility override needs priority */
transform: translate(-50%, -50%) !important; transform: translate(-50%, -50%) !important;
/* biome-ignore lint: Accessibility override needs priority */
will-change: auto !important; will-change: auto !important;
} }
} }
@@ -99,60 +105,62 @@
.performance-mode .global-spinning-image, .performance-mode .global-spinning-image,
.performance-mode .now-playing-background-image { .performance-mode .now-playing-background-image {
/* Keep animations but optimize filter effects */ /* Keep animations but optimize filter effects */
/* biome-ignore lint: Intentional override of runtime styles */
filter: blur(10px) brightness(0.4) contrast(1.1) !important; filter: blur(10px) brightness(0.4) contrast(1.1) !important;
} }
/* Make Notification Feed sidebar transparent */ /* Make app chrome transparent for cover-everywhere background */
body, body,
#wimp, #wimp,
main, main,
[class^="_sidebarWrapper"], [class^="_sidebarWrapper"],
[class^="_mainContainer"], [class^="_mainContainer"],
[class*="smallHeader"],
[data-test="main-layout-sidebar-wrapper"], [data-test="main-layout-sidebar-wrapper"],
[data-test="main-layout-header"], [data-test="main-layout-header"],
[data-test="feed-sidebar"], [data-test="feed-sidebar"],
[data-test="stream-metadata"],
[data-test="footer-player"], [data-test="footer-player"],
/* Notification Feed sidebar specific container */
[class^="_feedSidebarVStack"], [class^="_feedSidebarVStack"],
[class^="_feedSidebarSpacer"], [class^="_feedSidebarSpacer"],
[class^="_feedSidebarItem"], [class^="_feedSidebarItem"],
[class^="_feedSidebarItemDiv"], [class^="_feedSidebarItemDiv"],
[class^="_cellContainer"], [class^="_cellContainer"] {
[class^="_cellTextContainer"] { /* biome-ignore lint: Ensure background is fully cleared under theme CSS */
background: unset !important; background: unset !important;
} }
/* Make sidebar and player bar semi-transparent with optimized backdrop-filter */ /* Make sidebar semi-transparent with optimized backdrop-filter */
[data-test="footer-player"], [data-test="main-layout-sidebar-wrapper"] {
[data-test="main-layout-sidebar-wrapper"], /* biome-ignore lint: Must beat app inline styles for translucency */
[class^="_bar"],
[class^="_sidebarItem"]:hover {
background-color: rgba(0, 0, 0, 0.3) !important; background-color: rgba(0, 0, 0, 0.3) !important;
/* biome-ignore lint: Must beat app inline styles for translucency */
backdrop-filter: blur(10px) !important; backdrop-filter: blur(10px) !important;
/* biome-ignore lint: Must beat app inline styles for translucency */
-webkit-backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important;
} }
/* Performance mode: reduce backdrop blur */ /* Performance mode: reduce backdrop blur */
.performance-mode [data-test="footer-player"], .performance-mode [data-test="main-layout-sidebar-wrapper"] {
.performance-mode [data-test="main-layout-sidebar-wrapper"], /* biome-ignore lint: Performance mode style requires priority */
.performance-mode [class^="_bar"],
.performance-mode [class^="_sidebarItem"]:hover {
backdrop-filter: blur(5px) !important; backdrop-filter: blur(5px) !important;
/* biome-ignore lint: Performance mode style requires priority */
-webkit-backdrop-filter: blur(5px) !important; -webkit-backdrop-filter: blur(5px) !important;
} }
/* Feed sidebar panel - black tint background for readability */ /* Feed sidebar panel - black tint background for readability */
[data-test="feed-sidebar"] { [data-test="feed-sidebar"] {
/* biome-ignore lint: Ensure readability over media */
background-color: rgba(0, 0, 0, 0.5) !important; background-color: rgba(0, 0, 0, 0.5) !important;
/* biome-ignore lint: Ensure readability over media */
backdrop-filter: blur(10px) !important; backdrop-filter: blur(10px) !important;
/* biome-ignore lint: Ensure readability over media */
-webkit-backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important;
} }
/* Performance mode: reduce sidebar backdrop blur */ /* Performance mode: reduce sidebar backdrop blur */
.performance-mode [data-test="feed-sidebar"] { .performance-mode [data-test="feed-sidebar"] {
/* biome-ignore lint: Performance mode style requires priority */
backdrop-filter: blur(5px) !important; backdrop-filter: blur(5px) !important;
/* biome-ignore lint: Performance mode style requires priority */
-webkit-backdrop-filter: blur(5px) !important; -webkit-backdrop-filter: blur(5px) !important;
} }
@@ -162,10 +170,6 @@ main,
[class*="_cellContainer"], [class*="_cellContainer"],
[data-test="feed-interval"], [data-test="feed-interval"],
[data-test="feed-item"] { [data-test="feed-item"] {
/* biome-ignore lint: Match theme transparency */
background-color: transparent !important; background-color: transparent !important;
} }
/* Remove bottom gradient */
[class^="_bottomGradient"] {
display: none !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
+402 -41
View File
@@ -2,86 +2,447 @@
@font-face { @font-face {
font-family: "AbyssFont"; font-family: "AbyssFont";
font-weight: 400; font-weight: 400;
src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2") format("woff2"); src: url("https://excel.lexploits.top/extra/tidal/LyricsRegular.woff2")
format("woff2");
} }
@font-face { @font-face {
font-family: "AbyssFont"; font-family: "AbyssFont";
font-weight: 500; font-weight: 500;
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2"); src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2")
format("woff2");
} }
@font-face { @font-face {
font-family: "AbyssFont"; font-family: "AbyssFont";
font-weight: 600; font-weight: 600;
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2"); src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2")
format("woff2");
} }
@font-face { @font-face {
font-family: "AbyssFont"; font-family: "AbyssFont";
font-weight: 700; font-weight: 700;
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2"); src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2")
format("woff2");
} }
/* Enhanced lyrics styling with glow effects */ /* Enhanced lyrics styling with glow effects */
[class*="_lyricsText"] > div > span[data-current="true"] { [data-test="now-playing-lyrics"] span[data-test="lyrics-line"][class*="_current_"] {
text-shadow: 0 0 2px #fff, 0 0 20px #fff !important; 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; padding-left: 20px;
transition-duration: 0.7s; transition-duration: 0.7s;
font-size: 55px; font-size: calc(55px * var(--rl-font-scale, 1));
color: white !important; /* biome-ignore lint: Active lyric uses Colorama color */
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 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; font-weight: 700;
} }
[class*="_lyricsText"] > div > span { [data-test="now-playing-lyrics"] span[data-test="lyrics-line"] {
text-shadow: 0 0 0px transparent, 0 0 0px transparent; text-shadow:
0 0 0px transparent,
0 0 0px transparent;
transition-duration: 0.25s; transition-duration: 0.25s;
color: rgba(128, 128, 128, 0.4); color: rgba(255, 255, 255, 0.4);
font-size: 40px; 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-family:
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
font-weight: 700; font-weight: 700;
} }
[class*="_lyricsText"] > div > span:hover { [data-test="now-playing-lyrics"] span[data-test="lyrics-line"]:hover {
text-shadow: 0 0 2px lightgray, 0 0 20px lightgray !important; 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; color: lightgray !important;
padding-left: 20px; padding-left: 20px;
transition-duration: 0.7s; transition-duration: 0.7s;
} }
/* Track title glow */ /* Current line transitions */
[data-test="now-playing-track-title"] { [data-test="now-playing-lyrics"] span[data-test="lyrics-line"] {
text-shadow: 0 0 1px #fff, 0 0 30px #fff !important; 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;
} }
/* Current line transitions */ /* Glow-aware left padding so the glow doesn't clip | Thanks Aya <3*/
[class*="_lyricsText"] > div > span { .rl-wbw-active {
transition: text-shadow 0.7s ease-in-out, color 0.7s ease-in-out, padding 0.7s ease-in-out !important; padding-left: var(--rl-glow-outer) !important;
} }
/* Lyrics container styling */ /* Lyrics container styling */
[class^="_lyricsContainer"] > div > div > span { [data-test="now-playing-lyrics"] span[data-test="lyrics-line"] {
margin-bottom: 2rem; margin-bottom: 2rem;
opacity: 1; /* biome-ignore lint: Must beat Tidal _hasCues_ opacity */
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 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; font-weight: 700;
font-size: 38px !important; /* biome-ignore lint: Typography override for readability */
font-size: calc(38px * var(--rl-font-scale, 1)) !important;
} }
/* Reset all lyrics styling when disabled */ /* Hide the old Musixmatch attribution footer in the lyrics panel */
.lyrics-glow-disabled [class*="_lyricsText"] > div > span[data-current="true"], [data-test="now-playing-lyrics"] [class*="_footer_"] {
.lyrics-glow-disabled [class*="_lyricsText"] > div > span, display: none !important;
.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 { /* MARKER: WBW lyrics CSS */
text-shadow: none !important;
padding-left: 0 !important; /* hide tidal spans for wbw */
transition: none !important; .rl-wbw-active span[data-test="lyrics-line"] {
font-size: inherit !important; /* biome-ignore lint: Must hide original lines when word-by-word is on */
color: inherit !important; display: none !important;
font-family: inherit !important; }
font-weight: inherit !important;
margin-bottom: inherit !important; /* Active line slide */
opacity: inherit !important; .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;
} }
@@ -9,7 +9,7 @@
} }
/* Also show player bar when hovering over the bottom area - only when UI is hidden */ /* Also show player bar when hovering over the bottom area - only when UI is hidden */
.radiant-lyrics-ui-hidden: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 { .radiant-lyrics-ui-hidden [data-test="footer-player"]:hover {
opacity: 1 !important; opacity: 1 !important;
} }
+262 -193
View File
@@ -1,218 +1,68 @@
/* Only apply styles when UI is hidden */ /* Sidebar */
.radiant-lyrics-ui-hidden [class*="tabItems"] { [class*="_sidebar_"] {
background-color: transparent !important;
}
/* Section header */
[class*="_sectionHeader_"] {
background-color: transparent !important;
}
/* Rounded corners */
[class*="_thumbnail_"],
[class*="_imageWrapper_"],
[class*="_playButton_"] {
border-radius: 5px !important;
}
/* 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; opacity: 0 !important;
transition: opacity 0.4s ease-in-out; transition: opacity 0.4s ease-in-out;
} }
/* Default state - visible */ .radiant-lyrics-ui-hidden [data-test="toggle-lyrics"]:hover,
[class*="tabItems"] { .radiant-lyrics-ui-hidden [data-test="toggle-credits"]:hover,
transition: opacity 0.4s ease-in-out; .radiant-lyrics-ui-hidden [data-test="toggle-similar-tracks"]:hover {
opacity: 1 !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) */ /* Hide header container (search, minimize, fullscreen) when UI is hidden */
.radiant-lyrics-ui-hidden [data-test="header"] {
.radiant-lyrics-ui-hidden [data-test="header-container"]:not(:has(.hide-ui-button)) {
opacity: 0 !important; opacity: 0 !important;
transition: opacity 0.4s ease-in-out; visibility: hidden !important;
transition: opacity 0.4s ease-in-out, visibility 0s linear 0.4s;
pointer-events: none !important;
} }
/* Keep header visible if it contains the Hide UI button, but hide its other children */ /* Immediate hide class for unhide button */
.radiant-lyrics-ui-hidden [data-test="header-container"]:has(.hide-ui-button) > *:not(.hide-ui-button) {
opacity: 0 !important;
transition: opacity 0.4s ease-in-out;
}
/* Default state for header */
[data-test="header-container"] {
transition: opacity 0.4s ease-in-out;
}
/* Only prevent specific text elements in player bar from being affected by margin adjustments */
[data-test="footer-player"] [class*="_trackTitle"],
[data-test="footer-player"] [class*="_artistName"],
[data-test="footer-player"] [class*="_trackInfo"],
[data-test="footer-player"] [class*="_trackContainer"] {
margin-top: 0 !important;
transform: none !important;
}
/* Immediate hide class for unhide button with smooth transition (had issues with the fade out.. so I removed it) */
.hide-immediately { .hide-immediately {
opacity: 0 !important; opacity: 0 !important;
visibility: hidden !important; visibility: hidden !important;
pointer-events: none !important; pointer-events: none !important;
} }
[class^="_bar"] { /* Auto-fade styling for unhide button */
background-color: transparent;
}
.radiant-lyrics-ui-hidden [class^="_bar"]>*:not(.hide-ui-button) {
opacity: 0 !important;
pointer-events: none !important;
transition: opacity 0.4s ease-in-out;
}
/* Default state for bar elements */
[class^="_bar"]>* {
transition: opacity 0.4s ease-in-out;
}
/* Hide search box and make it non-interactive */
.radiant-lyrics-ui-hidden [data-test="search-input"],
.radiant-lyrics-ui-hidden [class*="_searchInput"],
.radiant-lyrics-ui-hidden [class*="searchInput"],
.radiant-lyrics-ui-hidden [class*="_search"],
.radiant-lyrics-ui-hidden [class*="search"],
.radiant-lyrics-ui-hidden input[type="search"],
.radiant-lyrics-ui-hidden input[type="text"],
.radiant-lyrics-ui-hidden input[placeholder*="Search"],
.radiant-lyrics-ui-hidden input[placeholder*="search"],
.radiant-lyrics-ui-hidden [placeholder*="Search"],
.radiant-lyrics-ui-hidden [data-test="main-layout-header"] input,
.radiant-lyrics-ui-hidden [data-test="main-layout-header"] [class*="input"],
.radiant-lyrics-ui-hidden header input,
.radiant-lyrics-ui-hidden nav input {
pointer-events: none !important;
cursor: default !important;
user-select: none !important;
}
/* Hide bottom left controls completely - no hover functionality */
/* Exclude heart button in player bar and make sure hidden buttons can't be clicked */
.radiant-lyrics-ui-hidden [data-test="add-to-playlist"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [data-test="remove-from-playlist"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [data-test="like-toggle"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [data-test="dislike-toggle"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [data-test="favorite-toggle"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [data-test="heart-button"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [data-test="playlist-add"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [class*="_trackActions"],
.radiant-lyrics-ui-hidden [class*="_bottomLeftControls"],
.radiant-lyrics-ui-hidden [class*="_actionButtons"],
.radiant-lyrics-ui-hidden [class*="_favoriteButton"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden [class*="_addToPlaylist"],
.radiant-lyrics-ui-hidden [class*="_lowerLeft"],
.radiant-lyrics-ui-hidden [class*="_bottomActions"],
.radiant-lyrics-ui-hidden [class*="_mediaControls"] > div:first-child,
.radiant-lyrics-ui-hidden button[title*="Add to"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[title*="Remove from"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[title*="Like"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[title*="Favorite"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[title*="Heart"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[aria-label*="Add to"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[aria-label*="Remove from"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[aria-label*="Like"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[aria-label*="Favorite"]:not([data-test="footer-player"] *),
.radiant-lyrics-ui-hidden button[aria-label*="Heart"]:not([data-test="footer-player"] *),
/* Target buttons in bottom left area specifically - (idk if this is needed.. but it's here) */
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] button[class*="_button"]:not(.unhide-ui-button),
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] [class*="_iconButton"]:not(.unhide-ui-button),
/* Additional catch-all for bottom left area buttons - (idk if this is needed.. but it's here) */
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] > div > div:first-child button:not(.unhide-ui-button),
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] > div:first-child button:not(.unhide-ui-button) {
opacity: 0 !important;
pointer-events: none !important;
transition: opacity 0.5s ease-in-out !important;
}
/* No hover functionality in Hide UI Mode - buttons stay hidden.. yea thats right, you heard me */
/* Default state for control buttons */
[data-test="add-to-playlist"],
[data-test="remove-from-playlist"],
[data-test="like-toggle"],
[data-test="dislike-toggle"],
[data-test="favorite-toggle"],
[data-test="heart-button"],
[data-test="playlist-add"],
[class*="_trackActions"],
[class*="_bottomLeftControls"],
[class*="_actionButtons"],
[class*="_favoriteButton"],
[class*="_addToPlaylist"],
[class*="_lowerLeft"],
[class*="_bottomActions"],
[class*="_mediaControls"] > div:first-child,
button[title*="Add to"],
button[title*="Remove from"],
button[title*="Like"],
button[title*="Favorite"],
button[title*="Heart"],
button[aria-label*="Add to"],
button[aria-label*="Remove from"],
button[aria-label*="Like"],
button[aria-label*="Favorite"],
button[aria-label*="Heart"],
[class*="_nowPlayingContainer"] button[class*="_button"]:not(.unhide-ui-button),
[class*="_nowPlayingContainer"] [class*="_iconButton"]:not(.unhide-ui-button),
[class*="_nowPlayingContainer"] > div > div:first-child button:not(.unhide-ui-button),
[class*="_nowPlayingContainer"] > div:first-child button:not(.unhide-ui-button) {
transition: opacity 0.5s ease-in-out;
}
/* Smooth cover art movement when UI is hidden */
[class*="_albumImage"],
[class*="_coverArt"],
figure[class*="_albumImage"] {
transition: transform 0.6s ease-in-out;
}
.radiant-lyrics-ui-hidden [class*="_albumImage"],
.radiant-lyrics-ui-hidden [class*="_coverArt"],
.radiant-lyrics-ui-hidden figure[class*="_albumImage"] {
transform: translateX(80px) !important;
}
/* Smooth track info wrapper movement when UI is hidden (Arists & Track Title) */
[class*="_infoWrapper"],
[class*="_textContainer"] {
transition: transform 0.6s ease-in-out;
}
.radiant-lyrics-ui-hidden [class*="_infoWrapper"],
.radiant-lyrics-ui-hidden [class*="_textContainer"] {
transform: translateX(40px) !important;
}
/* Move parent containers instead of lyrics container directly to preserve gradient fade */
[data-test="stream-metadata"],
[class*="_rightColumn"],
[class*="_rightSide"],
[class*="_contentRight"],
[class*="_sidePanel"],
[class*="_lyricsSection"],
[class*="_lyricsWrapper"] {
transition: transform 0.6s ease-in-out;
}
.radiant-lyrics-ui-hidden [data-test="stream-metadata"],
.radiant-lyrics-ui-hidden [class*="_rightColumn"],
.radiant-lyrics-ui-hidden [class*="_rightSide"],
.radiant-lyrics-ui-hidden [class*="_contentRight"],
.radiant-lyrics-ui-hidden [class*="_sidePanel"],
.radiant-lyrics-ui-hidden [class*="_lyricsSection"],
.radiant-lyrics-ui-hidden [class*="_lyricsWrapper"] {
transform: translateX(60px) translateY(-70px) !important;
}
/* Hide UI button base styling - just the transition */
.hide-ui-button {
transition: opacity 0.5s ease-in-out, visibility 0.5s ease-in-out, background-color 0.2s ease-in-out, transform 0.2s ease-in-out !important;
}
/* Auto-fade styling for unhide button - (Keeps Text Visible, just not full opacity) | Cheers @Zyhn for the idea*/
.unhide-ui-button.auto-faded { .unhide-ui-button.auto-faded {
background-color: transparent !important; background-color: transparent !important;
border-color: transparent !important; border-color: transparent !important;
box-shadow: none !important; box-shadow: none !important;
backdrop-filter: none !important; backdrop-filter: none !important;
-webkit-backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
color: rgba(255, 255, 255, 0.8) !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; 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 { .unhide-ui-button.auto-faded:hover {
background-color: rgba(255, 255, 255, 0.2) !important; background-color: rgba(255, 255, 255, 0.2) !important;
border-color: rgba(255, 255, 255, 0.3) !important; border-color: rgba(255, 255, 255, 0.3) !important;
@@ -220,5 +70,224 @@ figure[class*="_albumImage"] {
backdrop-filter: blur(10px) !important; backdrop-filter: blur(10px) !important;
-webkit-backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important;
color: white !important; color: white !important;
transition: 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; 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;
} }