mirror of
https://github.com/meowarex/TidaLuna-Plugins.git
synced 2026-06-18 03:43:10 +10:00
Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50c40e6978 | |||
| 5189d2bbea | |||
| ec42f5c287 | |||
| 1ab2eda25c | |||
| db6310cef4 | |||
| 5e00accc7f | |||
| ee2243443e | |||
| 765c8baf96 | |||
| d6c2d3ac88 | |||
| 7748f2fe08 | |||
| 2deda8aed1 | |||
| 7ad4bbb332 | |||
| b403c3a80c | |||
| b493624bda | |||
| 48c8738bcd | |||
| 651e5cbc14 | |||
| 7627bd7051 | |||
| 3e51ac45f8 | |||
| 20a2c2b7f7 | |||
| 76b1e264f8 | |||
| f0139165a9 | |||
| c88ddef2f9 | |||
| 38cdc156d6 | |||
| e4df0a8c64 | |||
| 055fff6d47 | |||
| 8ee9717f25 | |||
| 00eaf37dfa | |||
| 5ead825b3d | |||
| c6e916e6f6 | |||
| 1a2e25c717 | |||
| 64dfe47592 | |||
| ef4c73037f | |||
| a2cb822a2c | |||
| ec25abf6f5 | |||
| e223f933c6 | |||
| 7d2f3d3c1a | |||
| 031bb107f8 | |||
| e766bac0fa | |||
| a6371240ef | |||
| d07444e102 | |||
| 92697d7396 | |||
| 56c73abc05 | |||
| 5e6e897395 | |||
| 20adbd26dc | |||
| 4749f50b95 | |||
| ff417f5472 | |||
| b48d248cda | |||
| 0a694a5bc0 | |||
| 4af872133e | |||
| 84af1a40f6 | |||
| 0f9d5a75d8 | |||
| adcbadcf49 | |||
| 764cb1aa96 | |||
| af4cd80c7c | |||
| 256dd3d724 | |||
| e062b4bd02 | |||
| d6a3b26b41 | |||
| 9f01ecd1ff | |||
| df80ef748e | |||
| 68fc92b2db | |||
| e59121968d | |||
| 1aa12e9fd3 | |||
| 8196ed6778 | |||
| 8fbb48f8fe | |||
| 6af3b93272 | |||
| b351fa859a | |||
| 422d03a54e | |||
| b27f0ca165 | |||
| cd35fee3f0 | |||
| 353b72e1e1 | |||
| bce5ddba54 | |||
| c648f3df95 | |||
| 9c537fa877 | |||
| e376fb745b | |||
| 6981cc8315 | |||
| ca085ce31b | |||
| 56b7476e92 | |||
| 34e0a51bcd | |||
| 09857b6b54 | |||
| 5e700692e7 | |||
| 8fdfff10e7 | |||
| dc82194a90 | |||
| 36257a954e | |||
| fbd0c2b696 | |||
| e62944a0df | |||
| 4ad4b5879c | |||
| 6d9184e5eb | |||
| 081b4cbdd8 | |||
| 4ca99ebd72 | |||
| 047d4de2f4 | |||
| 764c71b45f | |||
| d83a786de3 | |||
| 0356ea6b76 | |||
| 1876a37185 | |||
| b9a9588f9d | |||
| fa0a7b7f56 | |||
| f2c31bb33a | |||
| 78d960588c | |||
| 2ea44bd3cc | |||
| 9c9b47c930 | |||
| d53fd08ee8 | |||
| 11d08b6403 | |||
| 0d9b378e43 | |||
| 99661096d5 | |||
| 8c27eebd88 | |||
| 9fd8208996 | |||
| 8178699d81 | |||
| a1ddb0ede6 | |||
| 82dfb39ff5 | |||
| 0b9c27eaaf | |||
| 40ed89dd34 | |||
| c0255acb4c | |||
| 411e20b9f7 | |||
| 1fda054d2a | |||
| 50215fa0f5 | |||
| cf9bbb62e6 | |||
| 7de6a98d8e | |||
| 5761c01973 | |||
| 2e7e51b7eb | |||
| fe3f0011eb | |||
| 62e15b0d3d | |||
| 13cbe01bd8 |
+2
-4
@@ -1,6 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
dist/itzzexcel.oled-theme.json
|
||||
dist/itzzexcel.oled-theme.mjs
|
||||
dist/itzzexcel.oled-theme.mjs.map
|
||||
dist/store.json
|
||||
Notes.md
|
||||
/Reference/
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"snyk.advanced.organization": "5e35d31d-0df9-4f3c-b4b3-168a24114801",
|
||||
"snyk.advanced.autoSelectOrganization": true
|
||||
}
|
||||
@@ -4,14 +4,13 @@ A collection of Luna plugins for Tidal, ported from Neptune framework.
|
||||
|
||||
## Plugins
|
||||
|
||||
### 🎨 OLED Theme
|
||||
**Location:** `plugins/oled-theme-luna/`
|
||||
### 🎨 Obsidian
|
||||
**Location:** `plugins/obsidian-theme-luna/`
|
||||
|
||||
A dark OLED-friendly theme plugin that transforms Tidal Luna's appearance.
|
||||
A dark OLED-friendly theme that transforms Tidal Luna's appearance.
|
||||
|
||||
**Features:**
|
||||
- Applies a dark, OLED-optimized theme
|
||||
- Fetches the latest theme CSS from the GitHub repository
|
||||
- Reduces battery consumption on OLED displays.. i guess <3
|
||||
- Modern, sleek dark interface
|
||||
|
||||
@@ -34,6 +33,16 @@ Allows users to copy song lyrics by selecting them directly in the interface.
|
||||
- Automatic clipboard copying of selected lyrics
|
||||
- Smart lyric span detection
|
||||
|
||||
### 🧽 Element Hider
|
||||
**Location:** `plugins/element-hider-luna/`
|
||||
|
||||
Allows users to hide/remove UI elements by right clicking on them.
|
||||
|
||||
**Features:**
|
||||
- Remove/Hide ANY UI element
|
||||
- Automagically saves hidden elements
|
||||
- Allows for elements to be restored
|
||||
|
||||
### 🎶 Audio Visualizer
|
||||
**Location:** `plugins/audio-visualizer-luna/`
|
||||
|
||||
@@ -49,8 +58,21 @@ Allows users to copy song lyrics by selecting them directly in the interface.
|
||||
|
||||
## Installation
|
||||
|
||||
### Batteries Required
|
||||
1. [TidaLuna](https://github.com/Inrixia/TidaLuna) - Plugin Framework for Tidal (what these plugins are for)
|
||||
2. Tidal - Streaming Service (if you are here and dont use tidal.. then just enjoy the read <3)
|
||||
|
||||
### Installing from Plugin Store (in TidaLuna)
|
||||
1. Open Tidal (with Luna installed)
|
||||
2. Navigate to Luna Settings (Top right of Tidal)
|
||||
3. Click "Plugin Store" Tab
|
||||
4. Scroll Down and just click on the plugins to install them
|
||||
5. Naviagte to the "Plugins" Tab
|
||||
6. And now your done and you can adjust the settings to your liking <3
|
||||
|
||||
### Installing from URL
|
||||
1. Open TidalLuna after Building & Serving
|
||||
### (They are in the store by default now)
|
||||
1. Open TidaLuna after Building & Serving
|
||||
2. Navigate to Luna Settings (Top right of Tidal)
|
||||
3. Click "Plugin Store" Tab
|
||||
4. Paste in the "Install from URL" Bar `https://github.com/meowarex/tidalluna-plugins/releases/download/latest/store.json`
|
||||
@@ -63,7 +85,7 @@ Allows users to copy song lyrics by selecting them directly in the interface.
|
||||
git clone https://github.com/meowarex/tidalluna-plugins
|
||||
|
||||
# Change Folder to the Repo
|
||||
cd neptune-projects-fork
|
||||
cd tidalluna-plugins
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
@@ -73,7 +95,7 @@ pnpm run watch
|
||||
```
|
||||
|
||||
### Installing Plugins in TidalLuna
|
||||
1. Open TidalLuna after Building & Serving
|
||||
1. Open TidaLuna after Building & Serving
|
||||
2. Navigate to Luna Settings (Top right of Tidal)
|
||||
3. Click "Plugin Store" Tab
|
||||
4. Click Install on the Plugins at the top Labeled with "[Dev]"
|
||||
@@ -82,7 +104,7 @@ pnpm run watch
|
||||
## Development
|
||||
|
||||
This project is made for:
|
||||
- **TidalLuna** - Modern plugin framework for Tidal | Inrixia
|
||||
- **[TidaLuna](https://github.com/Inrixia/TidaLuna)** - Modern plugin framework for Tidal | Inrixia
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
@@ -90,10 +112,7 @@ This project is made for:
|
||||
- **Release automation** for distributing plugins
|
||||
- **Artifact uploads** for easy plugin distribution
|
||||
|
||||
## Based On <3
|
||||
|
||||
- **itzzexcel** - [GitHub](https://github.com/ItzzExcel)
|
||||
|
||||
## Credits
|
||||
|
||||
Original Neptune versions by itzzexcel. Ported to Luna framework following the Luna plugin template structure by meowarex with help from Inrixia <3
|
||||
Inrixia | [TidalLuna](https://github.com/Inrixia/TidaLuna) Plugin Framework (The successor Neptune)
|
||||
ItzzExcel | The Original Neptune version of "Radiant Lyrics" (Which was ported to Luna and Rewritten by me!)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"linter": {
|
||||
"rules": {
|
||||
"complexity": {
|
||||
"useArrowFunction": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+2356
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,34 @@
|
||||
import { ReactiveStore } from "@luna/core";
|
||||
import { LunaNumberSetting, LunaSettings, LunaSwitchSetting } from "@luna/ui";
|
||||
import {
|
||||
LunaSettings,
|
||||
LunaNumberSetting,
|
||||
LunaSwitchSetting,
|
||||
LunaTextSetting,
|
||||
} from "@luna/ui";
|
||||
import React from "react";
|
||||
const isWindows = navigator.userAgent.includes("Windows"); // tidal changes it in reqs navigator supplies the default electron one
|
||||
export const settings = await ReactiveStore.getPluginStorage("AudioVisualizer", {
|
||||
|
||||
export const settings = await ReactiveStore.getPluginStorage(
|
||||
"AudioVisualizer",
|
||||
{
|
||||
barCount: 32,
|
||||
barColor: "#ffffff",
|
||||
barRounding: true,
|
||||
customColors: [] as string[],
|
||||
spotifyAPI: isWindows
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const Settings = () => {
|
||||
const [barCount, setBarCount] = React.useState(settings.barCount);
|
||||
const [barColor, setBarColor] = React.useState(settings.barColor);
|
||||
const [barRounding, setBarRounding] = React.useState(settings.barRounding);
|
||||
const [spotifyAPI, setSpotifyAPI] = React.useState(settings.spotifyAPI);
|
||||
const [showColorPicker, setShowColorPicker] = React.useState(false);
|
||||
const [isAnimatingIn, setIsAnimatingIn] = React.useState(false);
|
||||
const [shouldRender, setShouldRender] = React.useState(false);
|
||||
const [customInput, setCustomInput] = React.useState(settings.barColor);
|
||||
const [customColors, setCustomColors] = React.useState(settings.customColors);
|
||||
const [hoveredColorIndex, setHoveredColorIndex] = React.useState<number | null>(null);
|
||||
const [hoveredColorIndex, setHoveredColorIndex] = React.useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const closeColorPicker = () => {
|
||||
setIsAnimatingIn(false);
|
||||
@@ -45,9 +53,25 @@ export const Settings = () => {
|
||||
|
||||
// Common color presets for cool points :D
|
||||
const colorPresets = [
|
||||
"#ffffff", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff",
|
||||
"#ff8800", "#8800ff", "#0088ff", "#88ff00", "#ff0088", "#00ff88",
|
||||
"#444444", "#888888", "#cccccc", "#1db954", "#e22134", "#1976d2"
|
||||
"#ffffff",
|
||||
"#ff0000",
|
||||
"#00ff00",
|
||||
"#0000ff",
|
||||
"#ffff00",
|
||||
"#ff00ff",
|
||||
"#00ffff",
|
||||
"#ff8800",
|
||||
"#8800ff",
|
||||
"#0088ff",
|
||||
"#88ff00",
|
||||
"#ff0088",
|
||||
"#00ff88",
|
||||
"#444444",
|
||||
"#888888",
|
||||
"#cccccc",
|
||||
"#1db954",
|
||||
"#e22134",
|
||||
"#1976d2",
|
||||
];
|
||||
|
||||
const updateColor = (color: string) => {
|
||||
@@ -65,9 +89,11 @@ export const Settings = () => {
|
||||
// Validate hex color format
|
||||
const hexColorRegex = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
|
||||
|
||||
if (hexColorRegex.test(trimmedInput) &&
|
||||
if (
|
||||
hexColorRegex.test(trimmedInput) &&
|
||||
!colorPresets.includes(trimmedInput) &&
|
||||
!customColors.includes(trimmedInput)) {
|
||||
!customColors.includes(trimmedInput)
|
||||
) {
|
||||
const newCustomColors = [...customColors, trimmedInput];
|
||||
setCustomColors(newCustomColors);
|
||||
settings.customColors = newCustomColors;
|
||||
@@ -76,7 +102,9 @@ export const Settings = () => {
|
||||
};
|
||||
|
||||
const removeCustomColor = (colorToRemove: string) => {
|
||||
const newCustomColors = customColors.filter(color => color !== colorToRemove);
|
||||
const newCustomColors = customColors.filter(
|
||||
(color) => color !== colorToRemove,
|
||||
);
|
||||
setCustomColors(newCustomColors);
|
||||
settings.customColors = newCustomColors;
|
||||
|
||||
@@ -89,26 +117,11 @@ export const Settings = () => {
|
||||
const allColors = [...colorPresets, ...customColors];
|
||||
|
||||
return (
|
||||
<LunaSettings> <LunaSwitchSetting
|
||||
title="Spotify API"
|
||||
desc="Use Spotify's audio analysis API instead of real-time audio data (Required for Windows)"
|
||||
// @ts-expect-error no idea why this errosr wth
|
||||
checked={spotifyAPI}
|
||||
disabled={isWindows} // Disable on non-Windows platforms
|
||||
// @ts-expect-error no idea why this errosr wth
|
||||
onChange={(_, checked) => {
|
||||
setSpotifyAPI(checked);
|
||||
settings.spotifyAPI = checked;
|
||||
(window as any).updateAudioVisualizer?.();
|
||||
}}
|
||||
/>
|
||||
|
||||
<LunaSettings>
|
||||
<LunaSwitchSetting
|
||||
title="Bar Roundness"
|
||||
desc="Enable rounded corners on visualizer bars"
|
||||
// @ts-expect-error no idea why this errosr wth
|
||||
checked={barRounding}
|
||||
// @ts-expect-error no idea why this errosr wth
|
||||
onChange={(_, checked) => {
|
||||
setBarRounding(checked);
|
||||
settings.barRounding = checked;
|
||||
@@ -134,19 +147,40 @@ export const Settings = () => {
|
||||
{/* I'm not sure if this is a good idea, but it works & looks amazing */}
|
||||
{/* Sorry @Inrixia <3 */}
|
||||
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
padding: "16px 0",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}>
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontWeight: "normal", fontSize: "1.075rem", marginBottom: "4px" }}>Bar Color</div>
|
||||
<div style={{ opacity: 0.7, fontSize: "14px" }}>Color of the visualizer bars</div>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "normal",
|
||||
fontSize: "1.075rem",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
Bar Color
|
||||
</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
|
||||
onClick={() => showColorPicker ? closeColorPicker() : openColorPicker()}
|
||||
onClick={() =>
|
||||
showColorPicker ? closeColorPicker() : openColorPicker()
|
||||
}
|
||||
style={{
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
@@ -157,15 +191,17 @@ export const Settings = () => {
|
||||
backdropFilter: "blur(10px)",
|
||||
WebkitBackdropFilter: "blur(10px)",
|
||||
position: "relative",
|
||||
overflow: "hidden"
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
background: "rgba(0,0,0,0.1)",
|
||||
backdropFilter: "blur(2px)"
|
||||
}} />
|
||||
backdropFilter: "blur(2px)",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Custom Color Picker Modal */}
|
||||
@@ -182,13 +218,14 @@ export const Settings = () => {
|
||||
background: "rgba(0,0,0,0.6)",
|
||||
zIndex: 1000,
|
||||
opacity: isAnimatingIn ? 1 : 0,
|
||||
transition: "opacity 0.2s ease"
|
||||
transition: "opacity 0.2s ease",
|
||||
}}
|
||||
onClick={closeColorPicker}
|
||||
/>
|
||||
|
||||
{/* Color Picker Panel */}
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
@@ -204,20 +241,32 @@ export const Settings = () => {
|
||||
zIndex: 1001,
|
||||
boxShadow: "0 20px 40px rgba(0,0,0,0.7)",
|
||||
opacity: isAnimatingIn ? 1 : 0,
|
||||
transform: isAnimatingIn ? "translate(-50%, -50%) scale(1)" : "translate(-50%, -50%) scale(0.9)",
|
||||
transition: "all 0.2s ease"
|
||||
}}>
|
||||
<div style={{ marginBottom: "12px", color: "#fff", fontWeight: "bold", fontSize: "14px" }}>
|
||||
transform: isAnimatingIn
|
||||
? "translate(-50%, -50%) scale(1)"
|
||||
: "translate(-50%, -50%) scale(0.9)",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "12px",
|
||||
color: "#fff",
|
||||
fontWeight: "bold",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
Choose Color
|
||||
</div>
|
||||
|
||||
{/* Color Grid */}
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(7, 1fr)",
|
||||
gap: "8px",
|
||||
marginBottom: "16px"
|
||||
}}>
|
||||
marginBottom: "16px",
|
||||
}}
|
||||
>
|
||||
{allColors.map((color, index) => {
|
||||
const isCustomColor = customColors.includes(color);
|
||||
const isHovered = hoveredColorIndex === index;
|
||||
@@ -228,7 +277,7 @@ export const Settings = () => {
|
||||
position: "relative",
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
cursor: "pointer"
|
||||
cursor: "pointer",
|
||||
}}
|
||||
className="color-item"
|
||||
onMouseEnter={() => setHoveredColorIndex(index)}
|
||||
@@ -243,10 +292,13 @@ export const Settings = () => {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
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,
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s ease"
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
/>
|
||||
{isCustomColor && (
|
||||
@@ -272,7 +324,7 @@ export const Settings = () => {
|
||||
justifyContent: "center",
|
||||
opacity: isHovered ? 1 : 0,
|
||||
transition: "opacity 0.2s ease",
|
||||
zIndex: 10
|
||||
zIndex: 10,
|
||||
}}
|
||||
className="remove-button"
|
||||
>
|
||||
@@ -286,16 +338,28 @@ export const Settings = () => {
|
||||
|
||||
{/* Custom Hex Input */}
|
||||
<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
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={customInput}
|
||||
onChange={(e) => setCustomInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === "Enter") {
|
||||
updateColor(customInput);
|
||||
addCustomColor();
|
||||
}
|
||||
@@ -310,7 +374,7 @@ export const Settings = () => {
|
||||
color: "#fff",
|
||||
fontSize: "14px",
|
||||
fontFamily: "monospace",
|
||||
boxSizing: "border-box"
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
@@ -330,13 +394,15 @@ export const Settings = () => {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
transition: "all 0.2s ease"
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = "rgba(255,255,255,0.25)";
|
||||
e.currentTarget.style.background =
|
||||
"rgba(255,255,255,0.25)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = "rgba(255,255,255,0.15)";
|
||||
e.currentTarget.style.background =
|
||||
"rgba(255,255,255,0.15)";
|
||||
}}
|
||||
>
|
||||
+
|
||||
@@ -355,7 +421,7 @@ export const Settings = () => {
|
||||
background: "rgba(255,255,255,0.1)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
fontSize: "12px"
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
Done
|
||||
@@ -365,8 +431,6 @@ export const Settings = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</LunaSettings>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ftch, LunaUnload, Tracer } from "@luna/core";
|
||||
import { PlayState, StyleTag } from "@luna/lib";
|
||||
import { LunaUnload, Tracer } from "@luna/core";
|
||||
import { StyleTag, PlayState } from "@luna/lib";
|
||||
import { settings, Settings } from "./Settings";
|
||||
|
||||
// Import CSS styles for the visualizer
|
||||
@@ -8,23 +8,30 @@ import visualizerStyles from "file://styles.css?minify";
|
||||
export const { trace } = Tracer("[Audio Visualizer]");
|
||||
|
||||
// Helper function for consistent logging
|
||||
const log = (message: string) => trace.log(message);
|
||||
const warn = (message: string) => trace.warn(message);
|
||||
const error = (message: string) => trace.err(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 };
|
||||
|
||||
// Basic config with settings
|
||||
const config = {
|
||||
enabled: true,
|
||||
position: 'left' as 'left' | 'right',
|
||||
position: "left" as "left" | "right",
|
||||
width: 200,
|
||||
height: 40,
|
||||
get barCount() { return settings.barCount; },
|
||||
get color() { return settings.barColor; },
|
||||
get barRounding() { return settings.barRounding; },
|
||||
get barCount() {
|
||||
return settings.barCount;
|
||||
},
|
||||
get color() {
|
||||
return settings.barColor;
|
||||
},
|
||||
get barRounding() {
|
||||
return settings.barRounding;
|
||||
},
|
||||
sensitivity: 1.5,
|
||||
smoothing: 0.8,
|
||||
visualizerType: 'bars' as 'bars' | 'waveform' | 'circular'
|
||||
visualizerType: "bars" as "bars" | "waveform" | "circular",
|
||||
};
|
||||
|
||||
// Clean up resources
|
||||
@@ -33,80 +40,6 @@ export const unloads = new Set<LunaUnload>();
|
||||
// StyleTag for CSS
|
||||
const styleTag = new StyleTag("AudioVisualizer", unloads, visualizerStyles);
|
||||
|
||||
interface SpotifyAudioAnalysis {
|
||||
meta: {
|
||||
analyzer_version: string;
|
||||
platform: string;
|
||||
detailed_status: string;
|
||||
status_code: number;
|
||||
timestamp: number;
|
||||
analysis_time: number;
|
||||
input_process: string;
|
||||
};
|
||||
track: {
|
||||
num_samples: number;
|
||||
duration: number;
|
||||
sample_md5: string;
|
||||
offset_seconds: number;
|
||||
window_seconds: number;
|
||||
analysis_sample_rate: number;
|
||||
analysis_channels: number;
|
||||
end_of_fade_in: number;
|
||||
start_of_fade_out: number;
|
||||
loudness: number;
|
||||
tempo: number;
|
||||
tempo_confidence: number;
|
||||
time_signature: number;
|
||||
time_signature_confidence: number;
|
||||
key: number;
|
||||
key_confidence: number;
|
||||
mode: number;
|
||||
mode_confidence: number;
|
||||
codestring: string;
|
||||
code_version: number;
|
||||
echoprintstring: string;
|
||||
echoprint_version: number;
|
||||
synchstring: string;
|
||||
synch_version: number;
|
||||
rhythmstring: string;
|
||||
rhythm_version: number;
|
||||
};
|
||||
bars: Array<{
|
||||
start: number;
|
||||
duration: number;
|
||||
confidence: number;
|
||||
}>;
|
||||
beats: Array<{
|
||||
start: number;
|
||||
duration: number;
|
||||
confidence: number;
|
||||
}>;
|
||||
sections: Array<{
|
||||
[key: string]: number;
|
||||
}>;
|
||||
segments: Array<{
|
||||
start: number;
|
||||
duration: number;
|
||||
confidence: number;
|
||||
loudness_start: number;
|
||||
loudness_max_time: number;
|
||||
loudness_max: number;
|
||||
loudness_end: number;
|
||||
pitches: number[];
|
||||
timbre: number[];
|
||||
}>;
|
||||
tatums: Array<{
|
||||
start: number;
|
||||
duration: number;
|
||||
confidence: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
let spotifyAudioAnalysis: SpotifyAudioAnalysis | null = null;
|
||||
let currentTrackId: string | null = null;
|
||||
let lastSpotifyFetchTime = 0;
|
||||
const SPOTIFY_FETCH_THROTTLE = 1000;
|
||||
|
||||
// Audio context and analyzer
|
||||
let audioContext: AudioContext | null = null;
|
||||
let analyser: AnalyserNode | null = null;
|
||||
@@ -116,73 +49,33 @@ let animationId: number | null = null;
|
||||
let currentAudioElement: HTMLAudioElement | null = null;
|
||||
let isSourceConnected: boolean = false;
|
||||
|
||||
let smoothedBars: number[] = [];
|
||||
let previousBars: number[] = [];
|
||||
const smoothingFactor = 0.15;
|
||||
// Canvas and container elements
|
||||
let visualizerContainer: HTMLDivElement | null = null;
|
||||
let canvas: HTMLCanvasElement | null = null;
|
||||
let canvasContext: CanvasRenderingContext2D | null = null;
|
||||
|
||||
const fetchSpotifyAudioAnalysis = async (): Promise<void> => {
|
||||
try {
|
||||
const trackId = PlayState.playbackContext?.actualProductId;
|
||||
if (!trackId) {
|
||||
warn("No track ID available for Spotify API");
|
||||
return;
|
||||
}
|
||||
if (currentTrackId === trackId && spotifyAudioAnalysis) {
|
||||
log("Using cached Spotify audio analysis");
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now - lastSpotifyFetchTime < SPOTIFY_FETCH_THROTTLE) {
|
||||
log("Throttling Spotify API call");
|
||||
return;
|
||||
}
|
||||
lastSpotifyFetchTime = now;
|
||||
|
||||
log(`Fetching Spotify audio analysis for track: ${trackId}`);
|
||||
|
||||
const data = await ftch.json<{
|
||||
audioAnalysis: SpotifyAudioAnalysis;
|
||||
}>(`https://api.vmohammad.dev/lyrics?tidal_id=${trackId}&filter=audioAnalysis`);
|
||||
|
||||
if (!data.audioAnalysis) {
|
||||
warn("No audio analysis data in API response");
|
||||
return;
|
||||
}
|
||||
|
||||
spotifyAudioAnalysis = data.audioAnalysis;
|
||||
currentTrackId = trackId;
|
||||
log("Successfully fetched Spotify audio analysis");
|
||||
|
||||
} catch (err) {
|
||||
error(`Failed to fetch Spotify audio analysis: ${err}`);
|
||||
spotifyAudioAnalysis = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Find the audio element - this is a bit of a hack but it works
|
||||
const findAudioElement = (): HTMLAudioElement | null => {
|
||||
// Try main selectors first
|
||||
const selectors = [
|
||||
'audio',
|
||||
// 'video',
|
||||
'audio[data-test]',
|
||||
'[data-test="audio-player"] audio'
|
||||
"audio",
|
||||
"video",
|
||||
"audio[data-test]",
|
||||
'[data-test="audio-player"] audio',
|
||||
];
|
||||
|
||||
for (const selector of selectors) {
|
||||
const element = document.querySelector(selector) as HTMLAudioElement;
|
||||
if (element && (element.tagName === 'AUDIO' || element.tagName === 'VIDEO')) {
|
||||
if (
|
||||
element &&
|
||||
(element.tagName === "AUDIO" || element.tagName === "VIDEO")
|
||||
) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
// Quick scan for any audio elements
|
||||
const audioElements = document.querySelectorAll('audio');
|
||||
const audioElements = document.querySelectorAll("audio, video");
|
||||
for (const element of audioElements) {
|
||||
const audioEl = element as HTMLAudioElement;
|
||||
if (audioEl.src || audioEl.currentSrc) {
|
||||
@@ -196,10 +89,6 @@ const findAudioElement = (): HTMLAudioElement | null => {
|
||||
// Initialize audio visualization
|
||||
const initializeAudioVisualizer = async (): Promise<void> => {
|
||||
try {
|
||||
if (settings.spotifyAPI) {
|
||||
await fetchSpotifyAudioAnalysis();
|
||||
log("Using Spotify API - skipping audio element connection");
|
||||
} else {
|
||||
// Find the audio element
|
||||
const audioElement = findAudioElement();
|
||||
if (!audioElement) {
|
||||
@@ -217,8 +106,7 @@ const initializeAudioVisualizer = async (): Promise<void> => {
|
||||
analyser = audioContext.createAnalyser();
|
||||
analyser.fftSize = 512; // Fixed power of 2 that provides enough frequency bins
|
||||
analyser.smoothingTimeConstant = config.smoothing;
|
||||
const buffer = new ArrayBuffer(analyser.frequencyBinCount);
|
||||
dataArray = new Uint8Array(buffer);
|
||||
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
||||
log("Created AnalyserNode");
|
||||
}
|
||||
|
||||
@@ -236,7 +124,10 @@ const initializeAudioVisualizer = async (): Promise<void> => {
|
||||
log("Connected to audio stream with output");
|
||||
} catch (error) {
|
||||
// Audio is connected elsewhere - that's fine, we just can't visualize
|
||||
if (error instanceof Error && error.message.includes('already connected')) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("already connected")
|
||||
) {
|
||||
log("Audio already connected elsewhere - skipping visualization");
|
||||
}
|
||||
return;
|
||||
@@ -245,10 +136,9 @@ const initializeAudioVisualizer = async (): Promise<void> => {
|
||||
|
||||
// Resume context only if needed and don't wait for it
|
||||
// (otherwise it will wait for the audio to start playing)
|
||||
if (audioContext.state === 'suspended') {
|
||||
if (audioContext.state === "suspended") {
|
||||
audioContext.resume().catch(() => {}); // Fire and forget
|
||||
}
|
||||
}
|
||||
|
||||
// Create UI only if it doesn't exist
|
||||
if (!visualizerContainer) {
|
||||
@@ -259,7 +149,6 @@ const initializeAudioVisualizer = async (): Promise<void> => {
|
||||
if (!animationId) {
|
||||
animate();
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
// log errors
|
||||
console.error(err);
|
||||
@@ -274,7 +163,9 @@ const createVisualizerUI = (): void => {
|
||||
if (!config.enabled) return;
|
||||
|
||||
// Find the search bar
|
||||
const searchField = document.querySelector('input[class*="_searchField"]') as HTMLInputElement;
|
||||
const searchField = document.querySelector(
|
||||
'input[class*="_searchField"]',
|
||||
) as HTMLInputElement;
|
||||
if (!searchField) {
|
||||
warn("Search field not found");
|
||||
return;
|
||||
@@ -287,13 +178,13 @@ const createVisualizerUI = (): void => {
|
||||
}
|
||||
|
||||
// Create visualizer container
|
||||
visualizerContainer = document.createElement('div');
|
||||
visualizerContainer.id = 'audio-visualizer-container';
|
||||
visualizerContainer = document.createElement("div");
|
||||
visualizerContainer.id = "audio-visualizer-container";
|
||||
visualizerContainer.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-${config.position === 'left' ? 'right' : 'left'}: 12px;
|
||||
margin-${config.position === "left" ? "right" : "left"}: 12px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
@@ -302,7 +193,7 @@ const createVisualizerUI = (): void => {
|
||||
`;
|
||||
|
||||
// Create canvas
|
||||
canvas = document.createElement('canvas');
|
||||
canvas = document.createElement("canvas");
|
||||
canvas.width = config.width;
|
||||
canvas.height = config.height;
|
||||
canvas.style.cssText = `
|
||||
@@ -312,13 +203,19 @@ const createVisualizerUI = (): void => {
|
||||
`;
|
||||
|
||||
visualizerContainer.appendChild(canvas);
|
||||
canvasContext = canvas.getContext('2d');
|
||||
canvasContext = canvas.getContext("2d");
|
||||
|
||||
// Insert visualizer next to search bar
|
||||
if (config.position === 'left') {
|
||||
searchContainer.parentElement?.insertBefore(visualizerContainer, searchContainer);
|
||||
if (config.position === "left") {
|
||||
searchContainer.parentElement?.insertBefore(
|
||||
visualizerContainer,
|
||||
searchContainer,
|
||||
);
|
||||
} else {
|
||||
searchContainer.parentElement?.insertBefore(visualizerContainer, searchContainer.nextSibling);
|
||||
searchContainer.parentElement?.insertBefore(
|
||||
visualizerContainer,
|
||||
searchContainer.nextSibling,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -332,17 +229,6 @@ const removeVisualizerUI = (): void => {
|
||||
}
|
||||
};
|
||||
|
||||
const lerp = (start: number, end: number, factor: number): number => {
|
||||
return start + (end - start) * factor;
|
||||
};
|
||||
|
||||
const initializeSmoothingArrays = (barCount: number): void => {
|
||||
if (smoothedBars.length !== barCount) {
|
||||
smoothedBars = new Array(barCount).fill(0);
|
||||
previousBars = new Array(barCount).fill(0);
|
||||
}
|
||||
};
|
||||
|
||||
// Animation loop for rendering visualizer
|
||||
const animate = (): void => {
|
||||
if (!canvasContext || !canvas) {
|
||||
@@ -353,48 +239,37 @@ const animate = (): void => {
|
||||
// Update canvas color in case it changed
|
||||
canvasContext.fillStyle = config.color;
|
||||
canvasContext.strokeStyle = config.color;
|
||||
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (settings.spotifyAPI && spotifyAudioAnalysis) {
|
||||
switch (config.visualizerType) {
|
||||
case 'bars':
|
||||
drawSpotifyBars();
|
||||
break;
|
||||
case 'waveform':
|
||||
// drawWaveform();
|
||||
break;
|
||||
case 'circular':
|
||||
// drawCircular();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Check if we have real audio data - this might not be needed but its a good idea
|
||||
let hasRealAudio = false;
|
||||
if (analyser && dataArray) {
|
||||
analyser.getByteFrequencyData(dataArray as any);
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
// Check if there's actual audio signal (not just silence)
|
||||
const avgVolume = dataArray.reduce((sum, val) => sum + val, 0) / dataArray.length;
|
||||
const avgVolume =
|
||||
dataArray.reduce((sum, val) => sum + val, 0) / dataArray.length;
|
||||
hasRealAudio = avgVolume > 5; // Threshold for detecting actual audio
|
||||
}
|
||||
|
||||
// Clear canvas
|
||||
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (hasRealAudio && analyser && dataArray) {
|
||||
// Draw real audio visualization
|
||||
switch (config.visualizerType) {
|
||||
case 'bars': // Is implemented YAYYY (default)
|
||||
case "bars": // Is implemented YAYYY (default)
|
||||
drawBars();
|
||||
break;
|
||||
case 'waveform': // Not implemented yet
|
||||
// drawWaveform();
|
||||
case "waveform": // Not implemented yet
|
||||
drawWaveform();
|
||||
break;
|
||||
case 'circular': // Not implemented yet
|
||||
// drawCircular();
|
||||
case "circular": // Not implemented yet
|
||||
drawCircular();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Draw cool scrolling wave effect when no audio
|
||||
drawScrollingWave();
|
||||
}
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(animate);
|
||||
};
|
||||
@@ -403,7 +278,14 @@ const animate = (): void => {
|
||||
let waveTime = 0;
|
||||
|
||||
// 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.roundRect(x, y, width, height, radius);
|
||||
ctx.fill();
|
||||
@@ -416,8 +298,6 @@ const drawScrollingWave = (): void => {
|
||||
waveTime += 0.05; // Speed of wave animation
|
||||
|
||||
const barCount = config.barCount;
|
||||
initializeSmoothingArrays(barCount);
|
||||
|
||||
const barWidth = canvas.width / barCount;
|
||||
const maxHeight = canvas.height * 0.6;
|
||||
|
||||
@@ -437,18 +317,16 @@ const drawScrollingWave = (): void => {
|
||||
const travelWave = Math.sin(x * Math.PI * 3 - waveTime * 2) * 0.5 + 0.5;
|
||||
|
||||
// Final height calculation
|
||||
const targetHeight = maxHeight * combinedWave * travelWave * 0.8 + 2; // Minimum height of 2px
|
||||
|
||||
smoothedBars[i] = lerp(smoothedBars[i], targetHeight, smoothingFactor * 2); // Faster smoothing for wave effect
|
||||
const barHeight = maxHeight * combinedWave * travelWave * 0.8 + 2; // Minimum height of 2px
|
||||
|
||||
const xPos = i * barWidth;
|
||||
const yPos = (canvas.height - smoothedBars[i]) / 2;
|
||||
const yPos = (canvas.height - barHeight) / 2;
|
||||
|
||||
// Draw rounded or square bars based on setting
|
||||
if (config.barRounding) {
|
||||
drawRoundedRect(canvasContext, xPos, yPos, barWidth - 1, smoothedBars[i], 2);
|
||||
drawRoundedRect(canvasContext, xPos, yPos, barWidth - 1, barHeight, 2);
|
||||
} else {
|
||||
canvasContext.fillRect(xPos, yPos, barWidth - 1, smoothedBars[i]);
|
||||
canvasContext.fillRect(xPos, yPos, barWidth - 1, barHeight);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -457,181 +335,82 @@ const drawScrollingWave = (): void => {
|
||||
const drawBars = (): void => {
|
||||
if (!canvasContext || !dataArray || !canvas) return;
|
||||
|
||||
const barCount = config.barCount;
|
||||
initializeSmoothingArrays(barCount);
|
||||
|
||||
const barWidth = canvas.width / barCount;
|
||||
const barWidth = canvas.width / config.barCount;
|
||||
const heightScale = canvas.height / 255;
|
||||
|
||||
canvasContext.fillStyle = config.color;
|
||||
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const dataIndex = Math.floor(i * (dataArray.length / barCount));
|
||||
const targetHeight = (dataArray[dataIndex] * config.sensitivity * heightScale);
|
||||
|
||||
smoothedBars[i] = lerp(smoothedBars[i], targetHeight, smoothingFactor);
|
||||
for (let i = 0; i < config.barCount; i++) {
|
||||
const dataIndex = Math.floor(i * (dataArray.length / config.barCount));
|
||||
const barHeight = dataArray[dataIndex] * config.sensitivity * heightScale;
|
||||
|
||||
const x = i * barWidth;
|
||||
const y = canvas.height - smoothedBars[i];
|
||||
const y = canvas.height - barHeight;
|
||||
|
||||
// Draw rounded or square bars based on setting
|
||||
if (config.barRounding) {
|
||||
drawRoundedRect(canvasContext, x, y, barWidth - 1, smoothedBars[i], 2);
|
||||
drawRoundedRect(canvasContext, x, y, barWidth - 1, barHeight, 2);
|
||||
} else {
|
||||
canvasContext.fillRect(x, y, barWidth - 1, smoothedBars[i]);
|
||||
canvasContext.fillRect(x, y, barWidth - 1, barHeight);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let currentTime = 0;
|
||||
let previousTime = 0;
|
||||
let lastUpdated = 0;
|
||||
const drawSpotifyBars = (): void => {
|
||||
if (!canvasContext || !canvas || !spotifyAudioAnalysis) return;
|
||||
// Draw waveform visualization - NOT IMPLEMENTED YET
|
||||
// const drawWaveform = (): void => {
|
||||
// if (!canvasContext || !dataArray || !canvas) return;
|
||||
|
||||
const audioElement = findAudioElement();
|
||||
if (audioElement && audioElement.currentTime) {
|
||||
currentTime = audioElement.currentTime;
|
||||
previousTime = -1;
|
||||
} else {
|
||||
const progressBar = document.querySelector('[data-test="progress-bar"]') as HTMLElement;
|
||||
if (progressBar) {
|
||||
const ariaValueNow = progressBar.getAttribute('aria-valuenow');
|
||||
if (ariaValueNow !== null) {
|
||||
const progressTime = Number.parseInt(ariaValueNow);
|
||||
const now = Date.now();
|
||||
// const centerY = canvas.height / 2;
|
||||
// const amplitudeScale = canvas.height / 512;
|
||||
|
||||
if (progressTime !== previousTime) {
|
||||
currentTime = progressTime;
|
||||
previousTime = progressTime;
|
||||
lastUpdated = now;
|
||||
} else if (PlayState.playing) {
|
||||
const elapsedSeconds = (now - lastUpdated) / 1000;
|
||||
currentTime = progressTime + elapsedSeconds;
|
||||
}
|
||||
} else {
|
||||
warn("Progress bar not found or aria-valuenow is null");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// canvasContext.strokeStyle = config.color;
|
||||
// canvasContext.lineWidth = 2;
|
||||
// canvasContext.beginPath();
|
||||
|
||||
if (currentTime < 0) return;
|
||||
// for (let i = 0; i < config.barCount; i++) {
|
||||
// const dataIndex = Math.floor(i * (dataArray.length / config.barCount));
|
||||
// const amplitude = (dataArray[dataIndex] - 128) * config.sensitivity * amplitudeScale;
|
||||
|
||||
const barCount = config.barCount;
|
||||
initializeSmoothingArrays(barCount);
|
||||
// const x = (i / config.barCount) * canvas.width;
|
||||
// const y = centerY + amplitude;
|
||||
|
||||
const barWidth = canvas.width / barCount;
|
||||
canvasContext.fillStyle = config.color;
|
||||
// if (i === 0) {
|
||||
// canvasContext.moveTo(x, y);
|
||||
// } else {
|
||||
// canvasContext.lineTo(x, y);
|
||||
// }
|
||||
// }
|
||||
|
||||
const segments = spotifyAudioAnalysis.segments;
|
||||
const beats = spotifyAudioAnalysis.beats;
|
||||
// canvasContext.stroke();
|
||||
// };
|
||||
|
||||
if (!segments || segments.length === 0) {
|
||||
warn("No segments data available in Spotify audio analysis");
|
||||
return;
|
||||
}
|
||||
// Draw circular visualization - NOT IMPLEMENTED YET
|
||||
// const drawCircular = (): void => {
|
||||
// if (!canvasContext || !dataArray || !canvas) return;
|
||||
|
||||
let currentSegmentIndex = segments.findIndex(segment =>
|
||||
currentTime >= segment.start && currentTime < (segment.start + segment.duration)
|
||||
);
|
||||
// const centerX = canvas.width / 2;
|
||||
// const centerY = canvas.height / 2;
|
||||
// const radius = Math.min(centerX, centerY) - 10;
|
||||
|
||||
if (currentSegmentIndex === -1) {
|
||||
currentSegmentIndex = segments.reduce((closestIndex, segment, index) => {
|
||||
const closestDiff = Math.abs(segments[closestIndex].start - currentTime);
|
||||
const segmentDiff = Math.abs(segment.start - currentTime);
|
||||
return segmentDiff < closestDiff ? index : closestIndex;
|
||||
}, 0);
|
||||
}
|
||||
// canvasContext.strokeStyle = config.color;
|
||||
// canvasContext.lineWidth = 2;
|
||||
|
||||
const currentSegment = segments[currentSegmentIndex];
|
||||
if (!currentSegment) return;
|
||||
// for (let i = 0; i < config.barCount; i++) {
|
||||
// const dataIndex = Math.floor(i * (dataArray.length / config.barCount));
|
||||
// const amplitude = (dataArray[dataIndex] * config.sensitivity) / 255;
|
||||
|
||||
const nextSegment = segments[currentSegmentIndex + 1];
|
||||
|
||||
const segmentProgress = (currentTime - currentSegment.start) / currentSegment.duration;
|
||||
const interpolationFactor = Math.max(0, Math.min(1, segmentProgress));
|
||||
|
||||
const currentBeat = beats?.find(beat =>
|
||||
currentTime >= beat.start && currentTime < (beat.start + beat.duration)
|
||||
);
|
||||
|
||||
let beatIntensity = 1.0;
|
||||
if (currentBeat) {
|
||||
const beatProgress = (currentTime - currentBeat.start) / currentBeat.duration;
|
||||
beatIntensity = 1.0 + (1.0 - beatProgress) * currentBeat.confidence;
|
||||
}
|
||||
|
||||
const getInterpolatedValue = (currentValue: number, nextValue?: number): number => {
|
||||
if (!nextValue || !nextSegment) return currentValue;
|
||||
return lerp(currentValue, nextValue, interpolationFactor * 0.3); // Gentle interpolation
|
||||
};
|
||||
|
||||
const currentLoudness = currentSegment.loudness_max;
|
||||
const nextLoudness = nextSegment?.loudness_max ?? currentLoudness;
|
||||
const interpolatedLoudness = getInterpolatedValue(currentLoudness, nextLoudness);
|
||||
const loudnessMultiplier = Math.max(0.3, Math.min(1.2, (interpolatedLoudness + 80) / 80));
|
||||
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
let targetHeight = 0;
|
||||
|
||||
const pitches = currentSegment.pitches;
|
||||
const timbre = currentSegment.timbre;
|
||||
const nextPitches = nextSegment?.pitches;
|
||||
const nextTimbre = nextSegment?.timbre;
|
||||
|
||||
if (i < 12 && pitches.length >= 12) {
|
||||
const currentPitch = pitches[i];
|
||||
const nextPitch = nextPitches?.[i];
|
||||
const interpolatedPitch = getInterpolatedValue(currentPitch, nextPitch);
|
||||
|
||||
targetHeight = Math.pow(interpolatedPitch, 1.2) * canvas.height * config.sensitivity * 0.6;
|
||||
|
||||
} else if (i < 24 && timbre.length >= 12) {
|
||||
const timbreIndex = (i - 12) % timbre.length;
|
||||
const currentTimbreValue = timbre[timbreIndex];
|
||||
const nextTimbreValue = nextTimbre?.[timbreIndex];
|
||||
const interpolatedTimbre = getInterpolatedValue(currentTimbreValue, nextTimbreValue);
|
||||
const normalizedTimbre = Math.max(0, Math.min(1, (interpolatedTimbre + 200) / 400));
|
||||
targetHeight = Math.pow(normalizedTimbre, 1.0) * canvas.height * config.sensitivity * 0.4;
|
||||
|
||||
} else {
|
||||
const harmonicIndex = i % 12;
|
||||
const harmonicMultiplier = Math.max(0.2, 1.0 - (Math.floor(i / 12) * 0.3));
|
||||
|
||||
const basePitch = pitches[harmonicIndex] || 0;
|
||||
const nextBasePitch = nextPitches?.[harmonicIndex];
|
||||
const interpolatedPitch = getInterpolatedValue(basePitch, nextBasePitch);
|
||||
|
||||
targetHeight = Math.pow(interpolatedPitch, 1.3) * canvas.height * config.sensitivity * harmonicMultiplier * 0.5;
|
||||
}
|
||||
|
||||
targetHeight *= loudnessMultiplier * beatIntensity;
|
||||
|
||||
const frequencyVariance = 1.0 + Math.sin((i / barCount) * Math.PI * 2 + currentTime * 0.5) * 0.05;
|
||||
targetHeight *= frequencyVariance;
|
||||
|
||||
if (targetHeight > canvas.height * 0.6) {
|
||||
targetHeight = canvas.height * 0.6 + (targetHeight - canvas.height * 0.6) * 0.2;
|
||||
}
|
||||
|
||||
targetHeight = Math.max(2, Math.min(targetHeight, canvas.height));
|
||||
|
||||
const changeFactor = Math.abs(targetHeight - (smoothedBars[i] || 0)) / canvas.height;
|
||||
const adaptiveSmoothingFactor = smoothingFactor * (1 + changeFactor * 0.5);
|
||||
smoothedBars[i] = lerp(smoothedBars[i] || 0, targetHeight, Math.min(adaptiveSmoothingFactor, 0.3));
|
||||
|
||||
const x = i * barWidth;
|
||||
const y = canvas.height - smoothedBars[i];
|
||||
|
||||
if (config.barRounding) {
|
||||
drawRoundedRect(canvasContext, x, y, barWidth - 1, smoothedBars[i], 2);
|
||||
} else {
|
||||
canvasContext.fillRect(x, y, barWidth - 1, smoothedBars[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
// const angle = (i / config.barCount) * Math.PI * 2;
|
||||
// const startX = centerX + Math.cos(angle) * radius * 0.7;
|
||||
// const startY = centerY + Math.sin(angle) * radius * 0.7;
|
||||
// const endX = centerX + Math.cos(angle) * radius * (0.7 + amplitude * 0.3);
|
||||
// const endY = centerY + Math.sin(angle) * radius * (0.7 + amplitude * 0.3);
|
||||
|
||||
// canvasContext.beginPath();
|
||||
// canvasContext.moveTo(startX, startY);
|
||||
// canvasContext.lineTo(endX, endY);
|
||||
// canvasContext.stroke();
|
||||
// }
|
||||
// };
|
||||
|
||||
// Update visualizer settings
|
||||
const updateAudioVisualizer = (): void => {
|
||||
@@ -639,8 +418,7 @@ const updateAudioVisualizer = (): void => {
|
||||
// use a fixed size that provides enough frequency bins
|
||||
analyser.fftSize = 512; // Fixed power of 2 - important
|
||||
analyser.smoothingTimeConstant = config.smoothing;
|
||||
const buffer = new ArrayBuffer(analyser.frequencyBinCount); // buffer like ahh
|
||||
dataArray = new Uint8Array(buffer);
|
||||
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
||||
}
|
||||
|
||||
if (canvas) {
|
||||
@@ -650,23 +428,6 @@ const updateAudioVisualizer = (): void => {
|
||||
canvas.style.height = `${config.height}px`;
|
||||
}
|
||||
|
||||
smoothedBars = [];
|
||||
previousBars = [];
|
||||
|
||||
if (settings.spotifyAPI) {
|
||||
const currentSpotifyTrackId = PlayState.playbackContext?.actualProductId;
|
||||
if (currentSpotifyTrackId && currentSpotifyTrackId !== currentTrackId) {
|
||||
log("Spotify API enabled, fetching audio analysis");
|
||||
fetchSpotifyAudioAnalysis().catch(err => {
|
||||
error(`Failed to fetch Spotify data: ${err}`);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
spotifyAudioAnalysis = null;
|
||||
currentTrackId = null;
|
||||
log("Spotify API disabled, cleared audio analysis data");
|
||||
}
|
||||
|
||||
// Recreate UI if position changed
|
||||
createVisualizerUI();
|
||||
};
|
||||
@@ -692,35 +453,24 @@ const cleanupAudioVisualizer = (): void => {
|
||||
const observePlayState = (): void => {
|
||||
let hasTriedInitialization = false;
|
||||
let checkCount = 0;
|
||||
let lastTrackIdForSpotify: string | null = null;
|
||||
|
||||
const checkAndInitialize = () => {
|
||||
checkCount++;
|
||||
if (settings.spotifyAPI) {
|
||||
const currentSpotifyTrackId = PlayState.playbackContext?.actualProductId;
|
||||
if (currentSpotifyTrackId && currentSpotifyTrackId !== lastTrackIdForSpotify) {
|
||||
lastTrackIdForSpotify = currentSpotifyTrackId;
|
||||
log(`Track changed, fetching Spotify data for: ${currentSpotifyTrackId}`);
|
||||
fetchSpotifyAudioAnalysis().catch(err => {
|
||||
error(`Failed to fetch Spotify data: ${err}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Only try to initialize once when music starts playing
|
||||
if ((PlayState.playing || settings.spotifyAPI) && !hasTriedInitialization) {
|
||||
if (PlayState.playing && !hasTriedInitialization) {
|
||||
hasTriedInitialization = true;
|
||||
log("Initializing audio visualizer...");
|
||||
|
||||
// Initialize immediately - no delay (after audio starts playing ofc)
|
||||
initializeAudioVisualizer().then(() => {
|
||||
if (settings.spotifyAPI || (audioContext && analyser)) {
|
||||
if (audioContext && analyser) {
|
||||
log("Audio visualizer ready!");
|
||||
} else {
|
||||
hasTriedInitialization = false; // Allow retry if failed
|
||||
}
|
||||
});
|
||||
} else if (!PlayState.playing && !settings.spotifyAPI && hasTriedInitialization) {
|
||||
} else if (!PlayState.playing && hasTriedInitialization) {
|
||||
// Reset try flag when music stops so it can try again next time (otherwise it explode)
|
||||
hasTriedInitialization = false;
|
||||
}
|
||||
@@ -734,7 +484,8 @@ const observePlayState = (): void => {
|
||||
// Start with fast checking, then slow down
|
||||
const fastInterval = setInterval(() => {
|
||||
checkAndInitialize();
|
||||
if (checkCount > 10) { // After 10 quick checks, switch to slower
|
||||
if (checkCount > 10) {
|
||||
// After 10 quick checks, switch to slower
|
||||
clearInterval(fastInterval);
|
||||
const slowInterval = setInterval(checkAndInitialize, 2000);
|
||||
unloads.add(() => clearInterval(slowInterval));
|
||||
@@ -785,7 +536,7 @@ const completeCleanup = (): void => {
|
||||
}
|
||||
|
||||
// Close audio context completely on plugin unload
|
||||
if (audioContext && audioContext.state !== 'closed') {
|
||||
if (audioContext && audioContext.state !== "closed") {
|
||||
audioContext.close();
|
||||
log("Closed AudioContext");
|
||||
}
|
||||
@@ -797,11 +548,6 @@ const completeCleanup = (): void => {
|
||||
dataArray = null;
|
||||
currentAudioElement = null;
|
||||
isSourceConnected = false;
|
||||
smoothedBars = [];
|
||||
previousBars = [];
|
||||
spotifyAudioAnalysis = null;
|
||||
currentTrackId = null;
|
||||
log("Cleaned up Spotify API data");
|
||||
};
|
||||
|
||||
// Register cleanup
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
[data-type="search-field"] {
|
||||
min-width: 220px !important;
|
||||
}
|
||||
|
||||
/* Shadow when active - doesnt seem to only apply when active but thats better */
|
||||
#audio-visualizer-container.active {
|
||||
box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@meowarex/oled-theme",
|
||||
"description": "A dark OLED-friendly theme plugin that transforms Tidal Luna's appearance.",
|
||||
"name": "@meowarex/colorama-lyrics",
|
||||
"description": "Customize lyrics colors: single, gradient & auto from cover art",
|
||||
"author": {
|
||||
"name": "meowarex",
|
||||
"url": "https://github.com/meowarex",
|
||||
@@ -0,0 +1,813 @@
|
||||
import { ReactiveStore } from "@luna/core";
|
||||
import { LunaSettings, LunaSwitchSetting } from "@luna/ui";
|
||||
import React from "react";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
applyColoramaLyrics?: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
// Define a typed onChange signature for the switch
|
||||
type SwitchChangeHandler = (
|
||||
event: React.ChangeEvent<HTMLInputElement> | null,
|
||||
checked: boolean,
|
||||
) => void;
|
||||
|
||||
export type ColoramaMode =
|
||||
| "single"
|
||||
| "gradient-experimental"
|
||||
| "cover"
|
||||
| "cover-gradient";
|
||||
|
||||
export const settings = await ReactiveStore.getPluginStorage("ColoramaLyrics", {
|
||||
enabled: true,
|
||||
mode: "single" as ColoramaMode,
|
||||
// Store colors as RGB hex (#RRGGBB) and opacity separately (0-100)
|
||||
singleColor: "#FFFFFF",
|
||||
singleAlpha: 100,
|
||||
gradientStart: "#FFFFFF",
|
||||
gradientStartAlpha: 100,
|
||||
gradientEnd: "#AAFFFF",
|
||||
gradientEndAlpha: 100,
|
||||
gradientAngle: 0,
|
||||
customColors: [] as string[],
|
||||
excludeInactive: false,
|
||||
});
|
||||
|
||||
export const Settings = () => {
|
||||
// const [enabled, setEnabled] = React.useState(settings.enabled);
|
||||
const [mode, setMode] = React.useState<ColoramaMode>(settings.mode);
|
||||
const [singleColor, setSingleColor] = React.useState(settings.singleColor);
|
||||
const [singleAlpha, setSingleAlpha] = React.useState<number>(
|
||||
settings.singleAlpha ?? 100,
|
||||
);
|
||||
const [gradientStart, setGradientStart] = React.useState(
|
||||
settings.gradientStart,
|
||||
);
|
||||
const [gradientStartAlpha, setGradientStartAlpha] = React.useState<number>(
|
||||
settings.gradientStartAlpha ?? 100,
|
||||
);
|
||||
const [gradientEnd, setGradientEnd] = React.useState(settings.gradientEnd);
|
||||
const [gradientEndAlpha, setGradientEndAlpha] = React.useState<number>(
|
||||
settings.gradientEndAlpha ?? 100,
|
||||
);
|
||||
const [gradientAngle, setGradientAngle] = React.useState(
|
||||
settings.gradientAngle,
|
||||
);
|
||||
const [customInput, setCustomInput] = React.useState(settings.singleColor);
|
||||
const [customColors, setCustomColors] = React.useState(settings.customColors);
|
||||
const [showPicker, setShowPicker] = React.useState(false);
|
||||
const [isAnimatingIn, setIsAnimatingIn] = React.useState(false);
|
||||
const [shouldRender, setShouldRender] = React.useState(false);
|
||||
const [excludeInactive, setExcludeInactive] = React.useState(
|
||||
settings.excludeInactive,
|
||||
);
|
||||
const [activeEndpoint, setActiveEndpoint] = React.useState<
|
||||
"single" | "start" | "end"
|
||||
>("single");
|
||||
const AnySwitch = LunaSwitchSetting as unknown as React.ComponentType<{
|
||||
title: string;
|
||||
desc?: string;
|
||||
checked: boolean;
|
||||
onChange: SwitchChangeHandler;
|
||||
}>;
|
||||
|
||||
// Helper for HEX normalization
|
||||
const normalizeToRGB = (
|
||||
hex: string,
|
||||
fallback: string = "#FFFFFF",
|
||||
): string => {
|
||||
let v = hex.trim().toLowerCase();
|
||||
if (!v.startsWith("#")) v = `#${v}`;
|
||||
// #rgb or #rgba -> expand
|
||||
if (/^#([0-9a-f]{3,4})$/.test(v)) {
|
||||
const m = v.slice(1);
|
||||
const r = m[0];
|
||||
const g = m[1];
|
||||
const b = m[2];
|
||||
// ignore alpha if provided (#rgba)
|
||||
return `#${r}${r}${g}${g}${b}${b}`.toUpperCase();
|
||||
}
|
||||
// #aarrggbb -> strip alpha
|
||||
if (/^#([0-9a-f]{8})$/.test(v)) {
|
||||
const rrggbb = v.slice(3);
|
||||
return `#${rrggbb}`.toUpperCase();
|
||||
}
|
||||
// #rrggbb
|
||||
if (/^#([0-9a-f]{6})$/.test(v)) return v.toUpperCase();
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const colorPresets = [
|
||||
"#FFFFFF",
|
||||
"#FF0000",
|
||||
"#00FF00",
|
||||
"#0000FF",
|
||||
"#FFFF00",
|
||||
"#FF00FF",
|
||||
"#00FFFF",
|
||||
"#FF8800",
|
||||
"#8800FF",
|
||||
"#0088FF",
|
||||
"#88FF00",
|
||||
"#FF0088",
|
||||
"#00FF88",
|
||||
"#444444",
|
||||
"#888888",
|
||||
"#CCCCCC",
|
||||
"#1DB954",
|
||||
"#E22134",
|
||||
"#1976D2",
|
||||
];
|
||||
|
||||
const openPicker = (endpoint: "single" | "start" | "end" = "single") => {
|
||||
setActiveEndpoint(endpoint);
|
||||
setShowPicker(true);
|
||||
setShouldRender(true);
|
||||
setTimeout(() => setIsAnimatingIn(true), 10);
|
||||
};
|
||||
const closePicker = () => {
|
||||
setIsAnimatingIn(false);
|
||||
setTimeout(() => {
|
||||
setShowPicker(false);
|
||||
setShouldRender(false);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const hexColorRegex = /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3,4})$/i;
|
||||
|
||||
const applyCustomInputColor = (raw: string, updateInput: boolean): void => {
|
||||
const trimmed = raw.trim();
|
||||
if (!hexColorRegex.test(trimmed)) return;
|
||||
if (mode === "single") {
|
||||
const next = normalizeToRGB(trimmed);
|
||||
settings.singleColor = next;
|
||||
setSingleColor(next);
|
||||
if (updateInput) setCustomInput(next);
|
||||
} else if (mode === "gradient-experimental") {
|
||||
const next = normalizeToRGB(trimmed);
|
||||
if (activeEndpoint === "end") {
|
||||
settings.gradientEnd = next;
|
||||
setGradientEnd(next);
|
||||
} else {
|
||||
settings.gradientStart = next;
|
||||
setGradientStart(next);
|
||||
}
|
||||
if (updateInput) setCustomInput(next);
|
||||
}
|
||||
requestApply();
|
||||
};
|
||||
|
||||
const addCustomColor = () => {
|
||||
const trimmed = customInput.trim();
|
||||
if (
|
||||
hexColorRegex.test(trimmed) &&
|
||||
!colorPresets.includes(trimmed) &&
|
||||
!customColors.includes(normalizeToRGB(trimmed))
|
||||
) {
|
||||
const updated = [...customColors, normalizeToRGB(trimmed)];
|
||||
setCustomColors(updated);
|
||||
settings.customColors = updated;
|
||||
}
|
||||
};
|
||||
|
||||
// const removeCustomColor = (color: string) => {
|
||||
// const updated = customColors.filter((c) => c !== color);
|
||||
// setCustomColors(updated);
|
||||
// settings.customColors = updated;
|
||||
// };
|
||||
|
||||
const allColors = [...colorPresets, ...customColors];
|
||||
|
||||
const requestApply = () => {
|
||||
window.applyColoramaLyrics?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<LunaSettings>
|
||||
{/* Mode selection via dropdown (aligned right) */}
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 0",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<div style={{ fontWeight: "normal", fontSize: "1.075rem" }}>Mode</div>
|
||||
<div style={{ opacity: 0.7, fontSize: 14 }}>
|
||||
Choose how lyrics are colored
|
||||
</div>
|
||||
</div>
|
||||
<select
|
||||
value={mode}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value as ColoramaMode;
|
||||
settings.mode = next;
|
||||
setMode(next);
|
||||
requestApply();
|
||||
}}
|
||||
style={{
|
||||
padding: "6px 10px",
|
||||
borderRadius: 6,
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
marginLeft: "auto",
|
||||
minWidth: 180,
|
||||
}}
|
||||
>
|
||||
<option value="single" style={{ color: "#000", background: "#fff" }}>
|
||||
Single
|
||||
</option>
|
||||
<option
|
||||
value="gradient-experimental"
|
||||
style={{ color: "#000", background: "#fff" }}
|
||||
>
|
||||
Gradient - Experimental
|
||||
</option>
|
||||
<option value="cover" style={{ color: "#000", background: "#fff" }}>
|
||||
Cover - Experimental
|
||||
</option>
|
||||
<option
|
||||
value="cover-gradient"
|
||||
style={{ color: "#000", background: "#fff" }}
|
||||
>
|
||||
Cover (Gradient) - Experimental
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Single color */}
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 0",
|
||||
display: mode === "single" ? "flex" : "none",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "normal",
|
||||
fontSize: "1.075rem",
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
Lyrics Color
|
||||
</div>
|
||||
<div style={{ opacity: 0.7, fontSize: 14 }}>Set lyrics color</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 8,
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (showPicker ? closePicker() : openPicker("single"))}
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
borderRadius: 6,
|
||||
cursor: "pointer",
|
||||
background: normalizeToRGB(singleColor),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gradient controls (open picker) */}
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 0",
|
||||
display: mode === "gradient-experimental" ? "flex" : "none",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "normal",
|
||||
fontSize: "1.075rem",
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
Gradient (Experimental)
|
||||
</div>
|
||||
<div style={{ opacity: 0.7, fontSize: 14 }}>Set colors & angle</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setCustomInput(gradientStart);
|
||||
openPicker("start");
|
||||
}}
|
||||
style={{
|
||||
padding: "8px 12px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Configure
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Cover gradient controls (open picker for angle) */}
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 0",
|
||||
display: mode === "cover-gradient" ? "flex" : "none",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: "normal",
|
||||
fontSize: "1.075rem",
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
Cover (Gradient) - Experimental
|
||||
</div>
|
||||
<div style={{ opacity: 0.7, fontSize: 14 }}>Set angle</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openPicker("start")}
|
||||
style={{
|
||||
padding: "8px 12px",
|
||||
borderRadius: 8,
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Configure
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Modal for picking and managing colors (reused) */}
|
||||
{shouldRender && (
|
||||
<>
|
||||
<button
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: "rgba(0,0,0,0.6)",
|
||||
zIndex: 1000,
|
||||
opacity: isAnimatingIn ? 1 : 0,
|
||||
transition: "opacity 0.2s ease",
|
||||
}}
|
||||
type="button"
|
||||
aria-label="Close color picker"
|
||||
onClick={closePicker}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === "Escape") closePicker();
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
background: "rgba(20,20,20,0.98)",
|
||||
backdropFilter: "blur(20px)",
|
||||
WebkitBackdropFilter: "blur(20px)",
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
minWidth: 320,
|
||||
maxWidth: "90vw",
|
||||
maxHeight: "90vh",
|
||||
zIndex: 1001,
|
||||
boxShadow: "0 20px 40px rgba(0,0,0,0.7)",
|
||||
opacity: isAnimatingIn ? 1 : 0,
|
||||
transform: isAnimatingIn
|
||||
? "translate(-50%, -50%) scale(1)"
|
||||
: "translate(-50%, -50%) scale(0.9)",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 12,
|
||||
color: "#fff",
|
||||
fontWeight: "bold",
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{mode === "single" ? "Single Color" : "Gradient Colors"}
|
||||
</div>
|
||||
{mode === "gradient-experimental" && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 8,
|
||||
alignItems: "center",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ color: "rgba(255,255,255,0.7)", fontSize: 12 }}>
|
||||
Editing
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveEndpoint("start");
|
||||
setCustomInput(gradientStart);
|
||||
}}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "6px 10px",
|
||||
borderRadius: 8,
|
||||
border:
|
||||
activeEndpoint === "start"
|
||||
? "1px solid rgba(255,255,255,0.5)"
|
||||
: "1px solid rgba(255,255,255,0.2)",
|
||||
background: "rgba(255,255,255,0.05)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: 3,
|
||||
background: normalizeToRGB(gradientStart),
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: 12 }}>Start</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveEndpoint("end");
|
||||
setCustomInput(gradientEnd);
|
||||
}}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "6px 10px",
|
||||
borderRadius: 8,
|
||||
border:
|
||||
activeEndpoint === "end"
|
||||
? "1px solid rgba(255,255,255,0.5)"
|
||||
: "1px solid rgba(255,255,255,0.2)",
|
||||
background: "rgba(255,255,255,0.05)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: 3,
|
||||
background: normalizeToRGB(gradientEnd),
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: 12 }}>End</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{mode !== "cover-gradient" && (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(7, 1fr)",
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
{allColors.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const next = normalizeToRGB(color);
|
||||
if (mode === "single") {
|
||||
settings.singleColor = next;
|
||||
setSingleColor(next);
|
||||
} else if (mode === "gradient-experimental") {
|
||||
if (activeEndpoint === "end") {
|
||||
settings.gradientEnd = next;
|
||||
setGradientEnd(next);
|
||||
} else {
|
||||
settings.gradientStart = next;
|
||||
setGradientStart(next);
|
||||
}
|
||||
}
|
||||
setCustomInput(next);
|
||||
requestApply();
|
||||
}}
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 6,
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
background: normalizeToRGB(color),
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{mode !== "cover-gradient" && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
fontSize: 12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
Custom Hex (#RRGGBB)
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||
<input
|
||||
type="text"
|
||||
value={customInput}
|
||||
onChange={(e) => setCustomInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
applyCustomInputColor(customInput, true);
|
||||
addCustomColor();
|
||||
}
|
||||
}}
|
||||
placeholder="#RRGGBB"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "8px 12px",
|
||||
borderRadius: 6,
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
background: "rgba(255,255,255,0.1)",
|
||||
color: "#fff",
|
||||
fontSize: 14,
|
||||
fontFamily: "monospace",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
applyCustomInputColor(customInput, false);
|
||||
addCustomColor();
|
||||
}}
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 6,
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
background: "rgba(255,255,255,0.15)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
fontSize: 16,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Sliders inside picker based on mode */}
|
||||
{mode === "single" && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
fontSize: 12,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
Alpha
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={singleAlpha}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
settings.singleAlpha = value;
|
||||
setSingleAlpha(value);
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === "gradient-experimental" && (
|
||||
<div style={{ marginBottom: 16, display: "grid", gap: 16 }}>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 3,
|
||||
background: normalizeToRGB(gradientStart),
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}
|
||||
>
|
||||
Start Alpha
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={gradientStartAlpha}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
settings.gradientStartAlpha = value;
|
||||
setGradientStartAlpha(value);
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 3,
|
||||
background: normalizeToRGB(gradientEnd),
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}
|
||||
>
|
||||
End Alpha
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={gradientEndAlpha}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
settings.gradientEndAlpha = value;
|
||||
setGradientEndAlpha(value);
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}
|
||||
>
|
||||
Angle
|
||||
</div>
|
||||
<div
|
||||
style={{ color: "rgba(255,255,255,0.6)", fontSize: 12 }}
|
||||
>
|
||||
{gradientAngle}°
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={360}
|
||||
step={1}
|
||||
value={gradientAngle}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
settings.gradientAngle = value;
|
||||
setGradientAngle(value);
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === "cover-gradient" && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
<div style={{ color: "rgba(255,255,255,0.8)", fontSize: 12 }}>
|
||||
Angle
|
||||
</div>
|
||||
<div style={{ color: "rgba(255,255,255,0.6)", fontSize: 12 }}>
|
||||
{gradientAngle}°
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={360}
|
||||
step={1}
|
||||
value={gradientAngle}
|
||||
onChange={(e) => {
|
||||
const value = Number(e.target.value);
|
||||
settings.gradientAngle = value;
|
||||
setGradientAngle(value);
|
||||
requestApply();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={closePicker}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: 8,
|
||||
borderRadius: 6,
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
background: "rgba(255,255,255,0.1)",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
fontSize: 12,
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<AnySwitch
|
||||
title="Exclude Inactive"
|
||||
desc="Apply color/gradient only to the currently active lyric line"
|
||||
checked={excludeInactive}
|
||||
onChange={(_event: React.ChangeEvent<HTMLInputElement> | null, checked: boolean) => {
|
||||
settings.excludeInactive = checked;
|
||||
setExcludeInactive(checked);
|
||||
requestApply();
|
||||
}}
|
||||
/>
|
||||
</LunaSettings>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,235 @@
|
||||
import { LunaUnload, Tracer } from "@luna/core";
|
||||
import { StyleTag, PlayState } from "@luna/lib";
|
||||
import { settings, Settings } from "./Settings";
|
||||
|
||||
import styles from "file://styles.css?minify";
|
||||
|
||||
export const { trace } = Tracer("[Colorama Lyrics]");
|
||||
export { Settings };
|
||||
|
||||
export const unloads = new Set<LunaUnload>();
|
||||
|
||||
new StyleTag("ColoramaLyrics", unloads, styles);
|
||||
|
||||
// Simple dominant color extraction from current cover art
|
||||
async function getCoverArtElement(): Promise<HTMLImageElement | null> {
|
||||
const img = document.querySelector(
|
||||
'figure[class*="_albumImage"] > div > div > div > img',
|
||||
) as HTMLImageElement | null;
|
||||
if (img) return img;
|
||||
const video = document.querySelector(
|
||||
'figure[class*="_albumImage"] > div > div > div > video',
|
||||
) as HTMLVideoElement | null;
|
||||
if (video) {
|
||||
const poster = video.getAttribute("poster");
|
||||
if (!poster) return null;
|
||||
const tempImg = new Image();
|
||||
tempImg.crossOrigin = "anonymous";
|
||||
tempImg.src = poster;
|
||||
await new Promise<void>((resolve) => {
|
||||
tempImg.onload = () => resolve();
|
||||
tempImg.onerror = () => resolve();
|
||||
});
|
||||
return tempImg as unknown as HTMLImageElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDominantColorsFromImage(
|
||||
img: HTMLImageElement,
|
||||
count: number = 2,
|
||||
): string[] {
|
||||
try {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return ["#ffffff", "#88aaff"]; // fallback
|
||||
const w = 64;
|
||||
const h = 64;
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
const data = ctx.getImageData(0, 0, w, h).data;
|
||||
|
||||
// Simple k-means-ish binning into 16 buckets per channel
|
||||
const buckets = new Map<string, number>();
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const r = data[i];
|
||||
const g = data[i + 1];
|
||||
const b = data[i + 2];
|
||||
const key = `${Math.round(r / 16)},${Math.round(g / 16)},${Math.round(b / 16)}`;
|
||||
buckets.set(key, (buckets.get(key) ?? 0) + 1);
|
||||
}
|
||||
const sorted = [...buckets.entries()].sort((a, b) => b[1] - a[1]);
|
||||
const picked = sorted.slice(0, Math.max(1, count)).map(([key]) => {
|
||||
const [r, g, b] = key.split(",").map((v) => parseInt(v, 10) * 16);
|
||||
return `#${[r, g, b].map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("")}`;
|
||||
});
|
||||
return picked;
|
||||
} catch {
|
||||
return ["#ffffff", "#88aaff"]; // fallback
|
||||
}
|
||||
}
|
||||
|
||||
// build rgba() from hex + alpha percentage
|
||||
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
||||
let v = hex.trim();
|
||||
if (!v.startsWith("#")) v = `#${v}`;
|
||||
if (/^#([0-9a-fA-F]{3})$/.test(v)) {
|
||||
const r = parseInt(v[1] + v[1], 16);
|
||||
const g = parseInt(v[2] + v[2], 16);
|
||||
const b = parseInt(v[3] + v[3], 16);
|
||||
return { r, g, b };
|
||||
}
|
||||
if (/^#([0-9a-fA-F]{6})$/.test(v)) {
|
||||
const r = parseInt(v.slice(1, 3), 16);
|
||||
const g = parseInt(v.slice(3, 5), 16);
|
||||
const b = parseInt(v.slice(5, 7), 16);
|
||||
return { r, g, b };
|
||||
}
|
||||
// 8-digit hex expects #AARRGGBB. Indices 1-3 are the alpha byte (ignored here),
|
||||
// so r/g/b are extracted from v.slice(3,5), v.slice(5,7), v.slice(7,9) respectively.
|
||||
if (/^#([0-9a-fA-F]{8})$/.test(v)) {
|
||||
const r = parseInt(v.slice(3, 5), 16);
|
||||
const g = parseInt(v.slice(5, 7), 16);
|
||||
const b = parseInt(v.slice(7, 9), 16);
|
||||
return { r, g, b };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function rgbaFromHexAndAlpha(
|
||||
hex: string,
|
||||
alphaPercent: number | undefined,
|
||||
): string {
|
||||
const rgb = hexToRgb(hex);
|
||||
const a = Math.max(0.05, Math.min(100, alphaPercent ?? 100)) / 100;
|
||||
if (!rgb) return `rgba(255,255,255,${a})`;
|
||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`;
|
||||
}
|
||||
|
||||
function applySingleColor(color: string) {
|
||||
const alpha = (settings as any).singleAlpha ?? 100;
|
||||
const rgba = rgbaFromHexAndAlpha(color, alpha);
|
||||
document.documentElement.style.setProperty("--cl-lyrics-color", rgba);
|
||||
document.documentElement.style.setProperty("--cl-glow1", rgba);
|
||||
document.documentElement.style.setProperty("--cl-glow2", rgba);
|
||||
document.documentElement.style.removeProperty("--cl-grad-start");
|
||||
document.documentElement.style.removeProperty("--cl-grad-end");
|
||||
document.documentElement.style.removeProperty("--cl-grad-angle");
|
||||
document.body.classList.remove("colorama-gradient");
|
||||
document.body.classList.add("colorama-single");
|
||||
}
|
||||
|
||||
function applyGradient(start: string, end: string, angle: number) {
|
||||
const startAlpha = (settings as any).gradientStartAlpha ?? 100;
|
||||
const endAlpha = (settings as any).gradientEndAlpha ?? 100;
|
||||
const startRgba = rgbaFromHexAndAlpha(start, startAlpha);
|
||||
const endRgba = rgbaFromHexAndAlpha(end, endAlpha);
|
||||
document.documentElement.style.setProperty("--cl-grad-start", startRgba);
|
||||
document.documentElement.style.setProperty("--cl-grad-end", endRgba);
|
||||
document.documentElement.style.setProperty("--cl-grad-angle", `${angle}deg`);
|
||||
document.documentElement.style.setProperty("--cl-glow1", startRgba);
|
||||
document.documentElement.style.setProperty("--cl-glow2", endRgba);
|
||||
document.body.classList.remove("colorama-single");
|
||||
document.body.classList.add("colorama-gradient");
|
||||
}
|
||||
|
||||
function resetModeClasses(): void {
|
||||
document.body.classList.remove("colorama-single", "colorama-gradient");
|
||||
}
|
||||
|
||||
async function applyCoverColors(gradient: boolean) {
|
||||
const img = await getCoverArtElement();
|
||||
if (!img) return;
|
||||
const colors = getDominantColorsFromImage(img, gradient ? 2 : 1);
|
||||
if (gradient) {
|
||||
const start = colors[0] ?? settings.gradientStart;
|
||||
const end = colors[1] ?? settings.gradientEnd;
|
||||
applyGradient(start, end, settings.gradientAngle);
|
||||
} else {
|
||||
const color = colors[0] ?? settings.singleColor;
|
||||
applySingleColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
function applyColoramaLyrics(): void {
|
||||
if (!settings.enabled) {
|
||||
document.body.classList.remove("colorama-single", "colorama-gradient");
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle only-active-line mode class
|
||||
if (settings.excludeInactive) {
|
||||
document.body.classList.add("colorama-only-active");
|
||||
} else {
|
||||
document.body.classList.remove("colorama-only-active");
|
||||
}
|
||||
resetModeClasses();
|
||||
switch (settings.mode) {
|
||||
case "single":
|
||||
applySingleColor(settings.singleColor);
|
||||
break;
|
||||
case "gradient-experimental":
|
||||
applyGradient(
|
||||
settings.gradientStart,
|
||||
settings.gradientEnd,
|
||||
settings.gradientAngle,
|
||||
);
|
||||
break;
|
||||
case "cover":
|
||||
applyCoverColors(false);
|
||||
break;
|
||||
case "cover-gradient":
|
||||
applyCoverColors(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).applyColoramaLyrics = applyColoramaLyrics;
|
||||
|
||||
// Re-apply on track changes (for auto modes)
|
||||
function observeTrackChanges(): void {
|
||||
let lastTrackId: string | null = null;
|
||||
const check = () => {
|
||||
const currentTrackId = PlayState.playbackContext?.actualProductId;
|
||||
if (currentTrackId && currentTrackId !== lastTrackId) {
|
||||
lastTrackId = currentTrackId;
|
||||
if (settings.mode === "cover" || settings.mode === "cover-gradient") {
|
||||
setTimeout(() => applyColoramaLyrics(), 200);
|
||||
}
|
||||
}
|
||||
};
|
||||
const interval = setInterval(check, 500);
|
||||
unloads.add(() => clearInterval(interval));
|
||||
check();
|
||||
}
|
||||
|
||||
// Initial apply and observers
|
||||
setTimeout(() => applyColoramaLyrics(), 200);
|
||||
observeTrackChanges();
|
||||
|
||||
// for some reason, re-apply after Radiant updates its styles/backgrounds
|
||||
function hookRadiantUpdates(): void {
|
||||
const w = window as any;
|
||||
const wrap = (name: string) => {
|
||||
const fn = w[name];
|
||||
if (typeof fn === "function" && !fn.__coloramaPatched) {
|
||||
const orig = fn.bind(w);
|
||||
const patched = (...args: unknown[]) => {
|
||||
const result = orig(...args);
|
||||
try {
|
||||
applyColoramaLyrics();
|
||||
} catch {}
|
||||
return result;
|
||||
};
|
||||
(patched as any).__coloramaPatched = true;
|
||||
w[name] = patched;
|
||||
}
|
||||
};
|
||||
wrap("updateRadiantLyricsStyles");
|
||||
wrap("updateRadiantLyricsNowPlayingBackground");
|
||||
wrap("updateRadiantLyricsGlobalBackground");
|
||||
wrap("updateRadiantLyricsTextGlow");
|
||||
}
|
||||
|
||||
setTimeout(() => hookRadiantUpdates(), 0);
|
||||
@@ -0,0 +1,230 @@
|
||||
/* Variables used by Colorama Lyrics */
|
||||
:root {
|
||||
--cl-lyrics-color: #ffffff;
|
||||
--cl-grad-start: #ffffff;
|
||||
--cl-grad-end: #88aaff;
|
||||
--cl-grad-angle: 0deg;
|
||||
--cl-glow1: #ffffff;
|
||||
--cl-glow2: #ffffff;
|
||||
}
|
||||
|
||||
/* Apply solid color to lyrics text */
|
||||
.colorama-single [class*="_lyricsText"] > div > span,
|
||||
.colorama-single [class*="_lyricsText"] > div > span[data-current="true"],
|
||||
.colorama-single [class^="_lyricsContainer"] > div > div > span,
|
||||
.colorama-single
|
||||
[class^="_lyricsContainer"]
|
||||
> div
|
||||
> div
|
||||
> span[data-current="true"] {
|
||||
color: var(--cl-lyrics-color) !important;
|
||||
background: none !important;
|
||||
-webkit-background-clip: initial !important;
|
||||
background-clip: initial !important;
|
||||
-webkit-text-fill-color: initial !important;
|
||||
}
|
||||
|
||||
/* Apply gradient to lyrics text */
|
||||
.colorama-gradient [class*="_lyricsText"] > div > span,
|
||||
.colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"],
|
||||
.colorama-gradient [class^="_lyricsContainer"] > div > div > span,
|
||||
.colorama-gradient
|
||||
[class^="_lyricsContainer"]
|
||||
> div
|
||||
> div
|
||||
> span[data-current="true"] {
|
||||
background: linear-gradient(
|
||||
var(--cl-grad-angle),
|
||||
var(--cl-grad-start),
|
||||
var(--cl-grad-end)
|
||||
) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
background-clip: text !important;
|
||||
color: transparent !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Only-active: apply container class only on the active line via JS */
|
||||
|
||||
/* Slight emphasis on current line (uniform to single mode) */
|
||||
.colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"],
|
||||
.colorama-gradient
|
||||
[class^="_lyricsContainer"]
|
||||
> div
|
||||
> div
|
||||
> span[data-current="true"] {
|
||||
filter: brightness(1.1) !important;
|
||||
}
|
||||
|
||||
/* Keep song title color unchanged; its glow is controlled in Radiant CSS */
|
||||
|
||||
/* Color Radiant glow shadows using Colorama colors (respect RL sizes) */
|
||||
.colorama-single [class*="_lyricsText"] > div > span[data-current="true"],
|
||||
.colorama-single
|
||||
[class^="_lyricsContainer"]
|
||||
> div
|
||||
> div
|
||||
> span[data-current="true"],
|
||||
.colorama-gradient [class*="_lyricsText"] > div > span[data-current="true"],
|
||||
.colorama-gradient
|
||||
[class^="_lyricsContainer"]
|
||||
> div
|
||||
> div
|
||||
> span[data-current="true"],
|
||||
.colorama-gradient
|
||||
[class^="_lyricsContainer"]
|
||||
> div
|
||||
> div
|
||||
> span[data-current="true"] {
|
||||
text-shadow:
|
||||
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
|
||||
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
|
||||
}
|
||||
|
||||
/* Hover: force glow color to match Colorama settings for inactive lines */
|
||||
.colorama-single [class*="_lyricsText"] > div > span:hover,
|
||||
.colorama-single [class^="_lyricsContainer"] > div > div > span:hover {
|
||||
color: var(--cl-lyrics-color) !important;
|
||||
text-shadow:
|
||||
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
|
||||
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
|
||||
}
|
||||
|
||||
.colorama-gradient [class*="_lyricsText"] > div > span:hover,
|
||||
.colorama-gradient [class^="_lyricsContainer"] > div > div > span:hover {
|
||||
background: linear-gradient(
|
||||
var(--cl-grad-angle),
|
||||
var(--cl-grad-start),
|
||||
var(--cl-grad-end)
|
||||
) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
background-clip: text !important;
|
||||
color: transparent !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
/* Do not increase glow strength on hover for gradients */
|
||||
}
|
||||
|
||||
/* MARKER: Radiant WBW Lyrics Support */
|
||||
|
||||
/* Single color: active wbw words & syllable finished */
|
||||
.colorama-single .rl-wbw-word.rl-wbw-active,
|
||||
.colorama-single .rl-wbw-word.rl-syl-finished {
|
||||
color: var(--cl-lyrics-color) !important;
|
||||
background: none !important;
|
||||
-webkit-background-clip: initial !important;
|
||||
background-clip: initial !important;
|
||||
-webkit-text-fill-color: initial !important;
|
||||
}
|
||||
|
||||
/* Single color: glow on active wbw words */
|
||||
.colorama-single .rl-wbw-word.rl-wbw-active {
|
||||
text-shadow:
|
||||
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
|
||||
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
|
||||
}
|
||||
|
||||
/* Gradient: active wbw words */
|
||||
.colorama-gradient .rl-wbw-word.rl-wbw-active {
|
||||
background: linear-gradient(
|
||||
var(--cl-grad-angle),
|
||||
var(--cl-grad-start),
|
||||
var(--cl-grad-end)
|
||||
) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
background-clip: text !important;
|
||||
color: transparent !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Gradient: syllable finished (solid color — gradient conflicts with sweep animation) */
|
||||
.colorama-gradient .rl-wbw-word.rl-syl-finished {
|
||||
color: var(--cl-glow1, #ffffff) !important;
|
||||
}
|
||||
|
||||
/* Gradient: active wbw word glow */
|
||||
.colorama-gradient .rl-wbw-word.rl-wbw-active {
|
||||
text-shadow:
|
||||
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
|
||||
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
|
||||
}
|
||||
|
||||
/* Hover: wbw words pick up Colorama colors */
|
||||
.colorama-single .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover,
|
||||
.colorama-single .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word.rl-wbw-word-hover {
|
||||
color: var(--cl-lyrics-color) !important;
|
||||
text-shadow:
|
||||
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #ffffff),
|
||||
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #ffffff) !important;
|
||||
}
|
||||
|
||||
.colorama-gradient .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover,
|
||||
.colorama-gradient .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word.rl-wbw-word-hover {
|
||||
background: linear-gradient(
|
||||
var(--cl-grad-angle),
|
||||
var(--cl-grad-start),
|
||||
var(--cl-grad-end)
|
||||
) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
background-clip: text !important;
|
||||
color: transparent !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Only-active: wbw words on inactive lines stay default */
|
||||
body.colorama-only-active.colorama-single
|
||||
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word,
|
||||
body.colorama-only-active.colorama-gradient
|
||||
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word {
|
||||
color: rgba(128, 128, 128, 0.4) !important;
|
||||
background: none !important;
|
||||
-webkit-background-clip: initial !important;
|
||||
background-clip: initial !important;
|
||||
-webkit-text-fill-color: initial !important;
|
||||
text-shadow: initial !important;
|
||||
}
|
||||
|
||||
/* Only-active: hover on inactive wbw lines keeps default */
|
||||
body.colorama-only-active.colorama-single
|
||||
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover,
|
||||
body.colorama-only-active.colorama-gradient
|
||||
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-word:hover {
|
||||
color: lightgray !important;
|
||||
background: none !important;
|
||||
-webkit-background-clip: initial !important;
|
||||
background-clip: initial !important;
|
||||
-webkit-text-fill-color: initial !important;
|
||||
text-shadow: initial !important;
|
||||
}
|
||||
|
||||
/* Only color active line mode */
|
||||
body.colorama-only-active.colorama-single [class*="_lyricsText"]
|
||||
> div
|
||||
> span:not([data-current="true"]),
|
||||
body.colorama-only-active.colorama-gradient
|
||||
[class*="_lyricsText"]
|
||||
> div
|
||||
> span:not([data-current="true"]) {
|
||||
/* Match Radiant inactive styling */
|
||||
color: rgba(128, 128, 128, 0.4) !important;
|
||||
background: none !important;
|
||||
-webkit-background-clip: initial !important;
|
||||
background-clip: initial !important;
|
||||
-webkit-text-fill-color: initial !important;
|
||||
text-shadow: initial !important;
|
||||
}
|
||||
|
||||
/* In only-active mode, keep TIDAL defaults even on hover for inactive lines */
|
||||
body.colorama-only-active.colorama-single [class*="_lyricsText"]
|
||||
> div
|
||||
> span:not([data-current="true"]):hover,
|
||||
body.colorama-only-active.colorama-gradient
|
||||
[class*="_lyricsText"]
|
||||
> div
|
||||
> span:not([data-current="true"]):hover {
|
||||
color: lightgray !important;
|
||||
background: none !important;
|
||||
-webkit-background-clip: initial !important;
|
||||
background-clip: initial !important;
|
||||
-webkit-text-fill-color: initial !important;
|
||||
text-shadow: initial !important;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LunaUnload, Tracer } from "@luna/core";
|
||||
import { type LunaUnload, Tracer } from "@luna/core";
|
||||
import { StyleTag } from "@luna/lib";
|
||||
|
||||
// Import CSS directly using Luna's file:// syntax - Took me a while to figure out <3
|
||||
@@ -9,8 +9,8 @@ export const { trace } = Tracer("[Copy Lyrics]");
|
||||
// clean up resources
|
||||
export const unloads = new Set<LunaUnload>();
|
||||
|
||||
// StyleTag for lyrics selection styling
|
||||
const lyricsStyleTag = new StyleTag("Copy-Lyrics", unloads, unlockSelection);
|
||||
// Style injection via side effect
|
||||
new StyleTag("Copy-Lyrics", unloads, unlockSelection);
|
||||
|
||||
function SetClipboard(text: string): void {
|
||||
const textarea = document.createElement("textarea");
|
||||
@@ -31,36 +31,50 @@ function SetClipboard(text: string): void {
|
||||
|
||||
let isSelecting = false;
|
||||
|
||||
const onMouseDown = function (): void {
|
||||
const onMouseDown = (): void => {
|
||||
isSelecting = true;
|
||||
};
|
||||
|
||||
const onMouseUp = function (event: MouseEvent): void {
|
||||
const onMouseUp = (): void => {
|
||||
if (isSelecting) {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.toString().length > 0) {
|
||||
if (selection?.toString().length > 0) {
|
||||
const selectedSpans: HTMLSpanElement[] = [];
|
||||
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 (
|
||||
container.nodeType !== Node.ELEMENT_NODE &&
|
||||
container.nodeType !== Node.DOCUMENT_NODE
|
||||
container &&
|
||||
container.nodeType === Node.ELEMENT_NODE &&
|
||||
(container as Element).hasAttribute("data-current")
|
||||
) {
|
||||
// Get the parent element if it's a text node
|
||||
const parentElement = container.parentElement;
|
||||
if (parentElement && parentElement.hasAttribute("data-current")) {
|
||||
let text_ = selection.toString().trim();
|
||||
const text_ = selection.toString().trim();
|
||||
SetClipboard(text_);
|
||||
trace.msg.log("Copied to clipboard!");
|
||||
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.
|
||||
const spans = (container as Element).getElementsByTagName("span");
|
||||
for (let span of spans) {
|
||||
const spans = (container as Element | Document).getElementsByTagName(
|
||||
"span",
|
||||
);
|
||||
for (const span of spans) {
|
||||
if (selection.containsNode(span, true)) {
|
||||
selectedSpans.push(span as HTMLSpanElement);
|
||||
}
|
||||
@@ -73,7 +87,11 @@ const onMouseUp = function (event: MouseEvent): void {
|
||||
if (span.hasAttribute("data-current")) {
|
||||
hasCorrectAttribute = true;
|
||||
text += span.textContent + "\n";
|
||||
if ([...span.classList].some((className) => className.startsWith("endOfStanza--"))) {
|
||||
if (
|
||||
[...span.classList].some((className) =>
|
||||
className.startsWith("endOfStanza--"),
|
||||
)
|
||||
) {
|
||||
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;
|
||||
|
||||
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
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
return false;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Add event listener with capture phase to intercept events before they reach other handlers
|
||||
|
||||
document.addEventListener("click", onClickHooked, true);
|
||||
|
||||
document.addEventListener("mousedown", onMouseDown);
|
||||
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
|
||||
// Add cleanup to unloads
|
||||
unloads.add(() => {
|
||||
unloads.add((): void => {
|
||||
// Remove event listeners
|
||||
document.removeEventListener("click", onClickHooked, true);
|
||||
document.removeEventListener("mousedown", onMouseDown);
|
||||
|
||||
@@ -9,7 +9,7 @@ export const settings = await ReactiveStore.getPluginStorage("ElementHider", {
|
||||
className: string;
|
||||
textContent: string;
|
||||
timestamp: number;
|
||||
}>
|
||||
}>,
|
||||
});
|
||||
|
||||
export const Settings = () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LunaUnload, Tracer } from "@luna/core";
|
||||
import { StyleTag, ContextMenu } from "@luna/lib";
|
||||
import { type LunaUnload, Tracer } from "@luna/core";
|
||||
import { StyleTag } from "@luna/lib";
|
||||
import { settings, Settings } from "./Settings";
|
||||
|
||||
// Import CSS directly using Luna's file:// syntax
|
||||
@@ -13,8 +13,8 @@ export { Settings };
|
||||
// Clean up resources
|
||||
export const unloads = new Set<LunaUnload>();
|
||||
|
||||
// StyleTag for element hider
|
||||
const styleTag = new StyleTag("Element-Hider", unloads, styles);
|
||||
// StyleTag for element hider (side-effect)
|
||||
new StyleTag("Element-Hider", unloads, styles);
|
||||
|
||||
// State management
|
||||
let targetElement: HTMLElement | null = null;
|
||||
@@ -32,7 +32,7 @@ function generateElementSelector(element: HTMLElement): string {
|
||||
}
|
||||
|
||||
// Priority 2: data-test attribute (very specific for Tidal <3)
|
||||
const dataTest = element.getAttribute('data-test');
|
||||
const dataTest = element.getAttribute("data-test");
|
||||
if (dataTest) {
|
||||
return `[data-test="${dataTest}"]`;
|
||||
}
|
||||
@@ -41,28 +41,43 @@ function generateElementSelector(element: HTMLElement): string {
|
||||
let selector = element.tagName.toLowerCase();
|
||||
|
||||
// Get filtered classes (exclude our temporary classes)
|
||||
const classes = element.className ? element.className.trim().split(/\s+/).filter(cls => {
|
||||
return cls.length > 0 &&
|
||||
!cls.startsWith('element-hider-') &&
|
||||
cls !== 'element-hider-target' &&
|
||||
cls !== 'element-hider-hiding' &&
|
||||
cls !== 'element-hider-hidden';
|
||||
}) : [];
|
||||
const classes = element.className
|
||||
? element.className
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((cls) => {
|
||||
return (
|
||||
cls.length > 0 &&
|
||||
!cls.startsWith("element-hider-") &&
|
||||
cls !== "element-hider-target" &&
|
||||
cls !== "element-hider-hiding" &&
|
||||
cls !== "element-hider-hidden"
|
||||
);
|
||||
})
|
||||
: [];
|
||||
|
||||
// Only use classes if we have them and they're not generic and dumb
|
||||
if (classes.length > 0) {
|
||||
// Use ALL classes to be very specific
|
||||
selector += '.' + classes.join('.');
|
||||
selector += "." + classes.join(".");
|
||||
|
||||
// Add parent context for extra specificity (for when the element is inside another element)
|
||||
const parent = element.parentElement;
|
||||
if (parent && parent.tagName !== 'BODY' && parent.tagName !== 'HTML') {
|
||||
const parentClasses = parent.className ? parent.className.trim().split(/\s+/).filter(cls => {
|
||||
return cls.length > 0 && !cls.startsWith('element-hider-');
|
||||
}) : [];
|
||||
if (parent && parent.tagName !== "BODY" && parent.tagName !== "HTML") {
|
||||
const parentClasses = parent.className
|
||||
? parent.className
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((cls) => {
|
||||
return cls.length > 0 && !cls.startsWith("element-hider-");
|
||||
})
|
||||
: [];
|
||||
|
||||
if (parentClasses.length > 0) {
|
||||
const parentSelector = parent.tagName.toLowerCase() + '.' + parentClasses.slice(0, 2).join('.');
|
||||
const parentSelector =
|
||||
parent.tagName.toLowerCase() +
|
||||
"." +
|
||||
parentClasses.slice(0, 2).join(".");
|
||||
selector = `${parentSelector} > ${selector}`;
|
||||
}
|
||||
}
|
||||
@@ -70,19 +85,29 @@ function generateElementSelector(element: HTMLElement): string {
|
||||
// If no useful classes, use position-based selector with parent context
|
||||
const parent = element.parentElement;
|
||||
if (parent) {
|
||||
const siblings = Array.from(parent.children).filter(child => child.tagName === element.tagName);
|
||||
const siblings = Array.from(parent.children).filter(
|
||||
(child) => child.tagName === element.tagName,
|
||||
);
|
||||
const index = siblings.indexOf(element);
|
||||
if (index >= 0) {
|
||||
selector += `:nth-of-type(${index + 1})`;
|
||||
|
||||
// Add parent context
|
||||
if (parent.tagName !== 'BODY' && parent.tagName !== 'HTML') {
|
||||
const parentClasses = parent.className ? parent.className.trim().split(/\s+/).filter(cls => {
|
||||
return cls.length > 0 && !cls.startsWith('element-hider-');
|
||||
}) : [];
|
||||
if (parent.tagName !== "BODY" && parent.tagName !== "HTML") {
|
||||
const parentClasses = parent.className
|
||||
? parent.className
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((cls) => {
|
||||
return cls.length > 0 && !cls.startsWith("element-hider-");
|
||||
})
|
||||
: [];
|
||||
|
||||
if (parentClasses.length > 0) {
|
||||
const parentSelector = parent.tagName.toLowerCase() + '.' + parentClasses.slice(0, 2).join('.');
|
||||
const parentSelector =
|
||||
parent.tagName.toLowerCase() +
|
||||
"." +
|
||||
parentClasses.slice(0, 2).join(".");
|
||||
selector = `${parentSelector} > ${selector}`;
|
||||
}
|
||||
}
|
||||
@@ -100,14 +125,14 @@ function saveHiddenElement(element: HTMLElement): void {
|
||||
const elementInfo = {
|
||||
selector: selector,
|
||||
tagName: element.tagName,
|
||||
className: element.className || '',
|
||||
textContent: element.textContent?.substring(0, 100) || '',
|
||||
timestamp: Date.now()
|
||||
className: element.className || "",
|
||||
textContent: element.textContent?.substring(0, 100) || "",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// Check if element is already saved
|
||||
const existingIndex = settings.hiddenElements.findIndex(
|
||||
stored => stored.selector === elementInfo.selector
|
||||
(stored) => stored.selector === elementInfo.selector,
|
||||
);
|
||||
|
||||
if (existingIndex === -1) {
|
||||
@@ -119,17 +144,18 @@ function saveHiddenElement(element: HTMLElement): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove hidden element from persistent storage (for unhiding)
|
||||
function removeSavedElement(element: HTMLElement): void {
|
||||
const selector = generateElementSelector(element);
|
||||
const index = settings.hiddenElements.findIndex(stored => stored.selector === selector);
|
||||
|
||||
if (index !== -1) {
|
||||
settings.hiddenElements.splice(index, 1);
|
||||
trace.log(`Permanently removed: ${selector}`);
|
||||
trace.log(`Remaining stored: ${settings.hiddenElements.length}`);
|
||||
}
|
||||
}
|
||||
// Remove hidden element from persistent storage (for unhiding) - currently unused
|
||||
// function removeSavedElement(element: HTMLElement): void {
|
||||
// const selector = generateElementSelector(element);
|
||||
// const index = settings.hiddenElements.findIndex(
|
||||
// (stored) => stored.selector === selector,
|
||||
// );
|
||||
// if (index !== -1) {
|
||||
// settings.hiddenElements.splice(index, 1);
|
||||
// trace.log(`Permanently removed: ${selector}`);
|
||||
// trace.log(`Remaining stored: ${settings.hiddenElements.length}`);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Check if an element matches any stored selector (EXACT match only)
|
||||
function matchesStoredSelector(element: HTMLElement): boolean {
|
||||
@@ -154,14 +180,18 @@ function hideElementDirectly(element: HTMLElement): void {
|
||||
element.classList.add("element-hider-hidden");
|
||||
hiddenElements.add(element);
|
||||
hiddenElementsArray.push(element);
|
||||
trace.log(`Hidden element: ${element.tagName}${element.className ? '.' + element.className.split(' ')[0] : ''}`);
|
||||
trace.log(
|
||||
`Hidden element: ${element.tagName}${element.className ? "." + element.className.split(" ")[0] : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Hide the target element with animation
|
||||
function hideTargetElement(): void {
|
||||
if (!targetElement) return;
|
||||
|
||||
trace.log(`Hiding with animation: ${targetElement.tagName}${targetElement.className ? '.' + targetElement.className.split(' ')[0] : ''}`);
|
||||
trace.log(
|
||||
`Hiding with animation: ${targetElement.tagName}${targetElement.className ? "." + targetElement.className.split(" ")[0] : ""}`,
|
||||
);
|
||||
|
||||
// Add hiding animation class
|
||||
targetElement.classList.add("element-hider-hiding");
|
||||
@@ -175,7 +205,10 @@ function hideTargetElement(): void {
|
||||
// Wait for animation to complete, then hide
|
||||
setTimeout(() => {
|
||||
elementToHide.classList.add("element-hider-hidden");
|
||||
elementToHide.classList.remove("element-hider-hiding", "element-hider-target");
|
||||
elementToHide.classList.remove(
|
||||
"element-hider-hiding",
|
||||
"element-hider-target",
|
||||
);
|
||||
hiddenElements.add(elementToHide);
|
||||
hiddenElementsArray.push(elementToHide);
|
||||
}, 300);
|
||||
@@ -186,10 +219,12 @@ function hideTargetElement(): void {
|
||||
|
||||
// Unhide all elements permanently (remove from storage)
|
||||
function unhideAllElements(): void {
|
||||
trace.log(`Permanently unhiding ${settings.hiddenElements.length} saved elements`);
|
||||
trace.log(
|
||||
`Permanently unhiding ${settings.hiddenElements.length} saved elements`,
|
||||
);
|
||||
|
||||
// Show all currently hidden elements
|
||||
hiddenElementsArray.forEach(element => {
|
||||
hiddenElementsArray.forEach((element) => {
|
||||
if (document.body.contains(element)) {
|
||||
element.classList.remove("element-hider-hidden", "element-hider-hiding");
|
||||
}
|
||||
@@ -205,7 +240,9 @@ function unhideAllElements(): void {
|
||||
function processAllElements(): void {
|
||||
if (settings.hiddenElements.length === 0) return;
|
||||
|
||||
trace.log(`Scanning document for ${settings.hiddenElements.length} stored selectors`);
|
||||
trace.log(
|
||||
`Scanning document for ${settings.hiddenElements.length} stored selectors`,
|
||||
);
|
||||
let hiddenCount = 0;
|
||||
|
||||
// Use querySelectorAll for each stored selector with validation
|
||||
@@ -217,7 +254,9 @@ function processAllElements(): void {
|
||||
|
||||
// Limit to prevent over-hiding (safety check)
|
||||
if (elements.length > 10) {
|
||||
trace.warn(`Selector too broad (${elements.length} matches), skipping: ${storedElement.selector}`);
|
||||
trace.warn(
|
||||
`Selector too broad (${elements.length} matches), skipping: ${storedElement.selector}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -226,7 +265,9 @@ function processAllElements(): void {
|
||||
if (!hiddenElements.has(htmlElement)) {
|
||||
hideElementDirectly(htmlElement);
|
||||
hiddenCount++;
|
||||
trace.log(`Hid element ${elemIndex + 1}/${elements.length} for selector ${index + 1}`);
|
||||
trace.log(
|
||||
`Hid element ${elemIndex + 1}/${elements.length} for selector ${index + 1}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -241,7 +282,7 @@ function processAllElements(): void {
|
||||
|
||||
// Process new elements that are added to the DOM
|
||||
function processNewElements(addedNodes: NodeList): void {
|
||||
addedNodes.forEach(node => {
|
||||
addedNodes.forEach((node) => {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
||||
|
||||
const element = node as HTMLElement;
|
||||
@@ -252,8 +293,8 @@ function processNewElements(addedNodes: NodeList): void {
|
||||
}
|
||||
|
||||
// Check all descendant elements
|
||||
const descendants = element.querySelectorAll('*');
|
||||
descendants.forEach(descendant => {
|
||||
const descendants = element.querySelectorAll("*");
|
||||
descendants.forEach((descendant) => {
|
||||
if (matchesStoredSelector(descendant as HTMLElement)) {
|
||||
hideElementDirectly(descendant as HTMLElement);
|
||||
}
|
||||
@@ -267,7 +308,7 @@ function setupElementObserver(): void {
|
||||
|
||||
elementObserver = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
||||
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
|
||||
processNewElements(mutation.addedNodes);
|
||||
}
|
||||
});
|
||||
@@ -275,15 +316,22 @@ function setupElementObserver(): void {
|
||||
|
||||
elementObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
trace.log(`Set up reactive element observer`);
|
||||
}
|
||||
|
||||
// Global functions
|
||||
(window as any).showAllElementsFromSettings = unhideAllElements;
|
||||
(window as any).debugElementHider = () => {
|
||||
declare global {
|
||||
interface Window {
|
||||
showAllElementsFromSettings?: () => void;
|
||||
debugElementHider?: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
window.showAllElementsFromSettings = unhideAllElements;
|
||||
window.debugElementHider = () => {
|
||||
trace.log(`=== Element Hider Debug Info ===`);
|
||||
trace.log(`Stored elements: ${settings.hiddenElements.length}`);
|
||||
trace.log(`Currently hidden elements: ${hiddenElementsArray.length}`);
|
||||
@@ -297,19 +345,19 @@ function setupElementObserver(): void {
|
||||
// Handle highlighting target element
|
||||
function highlightElement(element: HTMLElement): void {
|
||||
// Remove previous highlights
|
||||
document.querySelectorAll('.element-hider-target').forEach(el => {
|
||||
el.classList.remove('element-hider-target');
|
||||
document.querySelectorAll(".element-hider-target").forEach((el) => {
|
||||
el.classList.remove("element-hider-target");
|
||||
});
|
||||
|
||||
// Highlight current element
|
||||
element.classList.add('element-hider-target');
|
||||
element.classList.add("element-hider-target");
|
||||
targetElement = element;
|
||||
}
|
||||
|
||||
// Remove highlight
|
||||
function removeHighlight(): void {
|
||||
if (targetElement) {
|
||||
targetElement.classList.remove('element-hider-target');
|
||||
targetElement.classList.remove("element-hider-target");
|
||||
targetElement = null;
|
||||
}
|
||||
}
|
||||
@@ -321,11 +369,17 @@ let contextMenuTimeout: number | null = null;
|
||||
let waitingForBuiltInMenu = false;
|
||||
|
||||
// Listen for right-click events to capture the target for context menu
|
||||
document.addEventListener('contextmenu', (event: MouseEvent) => {
|
||||
document.addEventListener(
|
||||
"contextmenu",
|
||||
(event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// Don't interfere with native context menus on inputs, textareas, etc.
|
||||
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
|
||||
if (
|
||||
target.tagName === "INPUT" ||
|
||||
target.tagName === "TEXTAREA" ||
|
||||
target.isContentEditable
|
||||
) {
|
||||
currentContextElement = null;
|
||||
return;
|
||||
}
|
||||
@@ -346,8 +400,7 @@ document.addEventListener('contextmenu', (event: MouseEvent) => {
|
||||
const eventX = event.clientX;
|
||||
const eventY = event.clientY;
|
||||
|
||||
// Prevent default immediately if we plan to handle it
|
||||
event.preventDefault();
|
||||
// Allow native context menu by default; we'll show our custom menu only if needed
|
||||
|
||||
// Wait to see if the built-in context menu appears
|
||||
contextMenuTimeout = window.setTimeout(() => {
|
||||
@@ -359,10 +412,14 @@ document.addEventListener('contextmenu', (event: MouseEvent) => {
|
||||
}, 150); // Wait 150ms for built-in menu
|
||||
|
||||
// Don't prevent default initially - let Luna try to handle the context menu
|
||||
}, true);
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
// Listen for clicks to close custom menu
|
||||
document.addEventListener('click', (event: MouseEvent) => {
|
||||
document.addEventListener(
|
||||
"click",
|
||||
(event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// If clicking outside our custom menu, close it
|
||||
@@ -370,10 +427,12 @@ document.addEventListener('click', (event: MouseEvent) => {
|
||||
closeCustomMenu();
|
||||
removeHighlight();
|
||||
}
|
||||
}, true);
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
// Handle escape key to close custom menu and remove highlights
|
||||
document.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
document.addEventListener("keydown", (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
if (customMenu) {
|
||||
closeCustomMenu();
|
||||
@@ -464,8 +523,15 @@ const contextMenuObserver = new MutationObserver((mutations) => {
|
||||
const element = node as HTMLElement;
|
||||
|
||||
// Look for Tidal's context menu
|
||||
if (element.matches('[data-test="contextmenu"]') || element.querySelector('[data-test="contextmenu"]')) {
|
||||
const contextMenu = element.matches('[data-test="contextmenu"]') ? element : element.querySelector('[data-test="contextmenu"]') as HTMLElement;
|
||||
if (
|
||||
element.matches('[data-test="contextmenu"]') ||
|
||||
element.querySelector('[data-test="contextmenu"]')
|
||||
) {
|
||||
const contextMenu = element.matches('[data-test="contextmenu"]')
|
||||
? element
|
||||
: (element.querySelector(
|
||||
'[data-test="contextmenu"]',
|
||||
) as HTMLElement);
|
||||
|
||||
if (contextMenu && currentContextElement && waitingForBuiltInMenu) {
|
||||
// Built-in menu appeared, cancel custom menu timeout
|
||||
@@ -485,8 +551,8 @@ const contextMenuObserver = new MutationObserver((mutations) => {
|
||||
// Add our options to the existing context menu
|
||||
function addElementHiderOptions(contextMenu: HTMLElement): void {
|
||||
// Create hide element button
|
||||
const hideButton = document.createElement('button');
|
||||
hideButton.className = 'element-hider-menu-item';
|
||||
const hideButton = document.createElement("button");
|
||||
hideButton.className = "element-hider-menu-item";
|
||||
hideButton.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -503,7 +569,7 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
|
||||
`;
|
||||
hideButton.innerHTML = `Hide This Element`;
|
||||
|
||||
hideButton.addEventListener('click', () => {
|
||||
hideButton.addEventListener("click", () => {
|
||||
if (currentContextElement) {
|
||||
targetElement = currentContextElement;
|
||||
hideTargetElement();
|
||||
@@ -511,37 +577,38 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
|
||||
});
|
||||
|
||||
// Add hover effects for highlighting
|
||||
hideButton.addEventListener('mouseenter', () => {
|
||||
hideButton.style.background = 'var(--wave-color-background-hover, #3a3a3a)';
|
||||
hideButton.addEventListener("mouseenter", () => {
|
||||
hideButton.style.background = "var(--wave-color-background-hover, #3a3a3a)";
|
||||
if (currentContextElement) {
|
||||
highlightElement(currentContextElement);
|
||||
}
|
||||
});
|
||||
|
||||
hideButton.addEventListener('mouseleave', () => {
|
||||
hideButton.style.background = 'transparent';
|
||||
hideButton.addEventListener("mouseleave", () => {
|
||||
hideButton.style.background = "transparent";
|
||||
removeHighlight();
|
||||
});
|
||||
|
||||
// Create unhide all button
|
||||
const unhideAllButton = document.createElement('button');
|
||||
unhideAllButton.className = 'element-hider-menu-item';
|
||||
const unhideAllButton = document.createElement("button");
|
||||
unhideAllButton.className = "element-hider-menu-item";
|
||||
unhideAllButton.style.cssText = hideButton.style.cssText;
|
||||
unhideAllButton.innerHTML = `Unhide All Elements (${hiddenElementsArray.length})`;
|
||||
|
||||
unhideAllButton.addEventListener('click', unhideAllElements);
|
||||
unhideAllButton.addEventListener("click", unhideAllElements);
|
||||
|
||||
// Add hover effects for unhide all button
|
||||
unhideAllButton.addEventListener('mouseenter', () => {
|
||||
unhideAllButton.style.background = 'var(--wave-color-background-hover, #3a3a3a)';
|
||||
unhideAllButton.addEventListener("mouseenter", () => {
|
||||
unhideAllButton.style.background =
|
||||
"var(--wave-color-background-hover, #3a3a3a)";
|
||||
});
|
||||
unhideAllButton.addEventListener('mouseleave', () => {
|
||||
unhideAllButton.style.background = 'transparent';
|
||||
unhideAllButton.addEventListener("mouseleave", () => {
|
||||
unhideAllButton.style.background = "transparent";
|
||||
});
|
||||
|
||||
// Add a separator if the menu has other items
|
||||
if (contextMenu.children.length > 0) {
|
||||
const separator = document.createElement('div');
|
||||
const separator = document.createElement("div");
|
||||
separator.style.cssText = `
|
||||
height: 1px;
|
||||
background: var(--wave-color-border, #444);
|
||||
@@ -558,7 +625,7 @@ function addElementHiderOptions(contextMenu: HTMLElement): void {
|
||||
// Start observing for context menus
|
||||
contextMenuObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
// Initialize plugin
|
||||
@@ -578,8 +645,8 @@ function initializePlugin() {
|
||||
}
|
||||
|
||||
// Run initialization when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePlugin);
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initializePlugin);
|
||||
} else {
|
||||
initializePlugin();
|
||||
}
|
||||
@@ -600,8 +667,8 @@ unloads.add(() => {
|
||||
removeHighlight();
|
||||
|
||||
// Clean up global functions
|
||||
(window as any).showAllElementsFromSettings = undefined;
|
||||
(window as any).debugElementHider = undefined;
|
||||
window.showAllElementsFromSettings = undefined;
|
||||
window.debugElementHider = undefined;
|
||||
|
||||
trace.log("Plugin unloaded");
|
||||
});
|
||||
|
||||
@@ -57,7 +57,9 @@
|
||||
|
||||
/* Animation for hiding */
|
||||
.element-hider-hiding {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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,23 +43,6 @@
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* Performance mode optimizations - keep spinning but optimize other aspects */
|
||||
.global-spinning-image.performance-mode-static {
|
||||
/* Keep animation enabled in performance mode */
|
||||
/* Lighter blur for performance */
|
||||
filter: blur(20px) brightness(0.4) contrast(1.2) saturate(1) !important;
|
||||
/* Smaller size for performance */
|
||||
width: 120vw !important;
|
||||
height: 120vh !important;
|
||||
}
|
||||
|
||||
.now-playing-background-image.performance-mode-static {
|
||||
/* Keep animation enabled in performance mode */
|
||||
/* Optimized size and effects for performance */
|
||||
width: 80vw !important;
|
||||
height: 80vh !important;
|
||||
}
|
||||
|
||||
/* Now Playing Background Container Optimization */
|
||||
.now-playing-background-container {
|
||||
position: absolute;
|
||||
@@ -89,8 +72,11 @@
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.global-spinning-image,
|
||||
.now-playing-background-image {
|
||||
/* biome-ignore lint: Accessibility override needs priority */
|
||||
animation: none !important;
|
||||
/* biome-ignore lint: Accessibility override needs priority */
|
||||
transform: translate(-50%, -50%) !important;
|
||||
/* biome-ignore lint: Accessibility override needs priority */
|
||||
will-change: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -99,6 +85,7 @@
|
||||
.performance-mode .global-spinning-image,
|
||||
.performance-mode .now-playing-background-image {
|
||||
/* Keep animations but optimize filter effects */
|
||||
/* biome-ignore lint: Intentional override of runtime styles */
|
||||
filter: blur(10px) brightness(0.4) contrast(1.1) !important;
|
||||
}
|
||||
|
||||
@@ -121,6 +108,7 @@ main,
|
||||
[class^="_feedSidebarItemDiv"],
|
||||
[class^="_cellContainer"],
|
||||
[class^="_cellTextContainer"] {
|
||||
/* biome-ignore lint: Ensure background is fully cleared under theme CSS */
|
||||
background: unset !important;
|
||||
}
|
||||
|
||||
@@ -129,8 +117,11 @@ main,
|
||||
[data-test="main-layout-sidebar-wrapper"],
|
||||
[class^="_bar"],
|
||||
[class^="_sidebarItem"]:hover {
|
||||
/* biome-ignore lint: Must beat app inline styles for translucency */
|
||||
background-color: rgba(0, 0, 0, 0.3) !important;
|
||||
/* biome-ignore lint: Must beat app inline styles for translucency */
|
||||
backdrop-filter: blur(10px) !important;
|
||||
/* biome-ignore lint: Must beat app inline styles for translucency */
|
||||
-webkit-backdrop-filter: blur(10px) !important;
|
||||
}
|
||||
|
||||
@@ -139,20 +130,27 @@ main,
|
||||
.performance-mode [data-test="main-layout-sidebar-wrapper"],
|
||||
.performance-mode [class^="_bar"],
|
||||
.performance-mode [class^="_sidebarItem"]:hover {
|
||||
/* biome-ignore lint: Performance mode style requires priority */
|
||||
backdrop-filter: blur(5px) !important;
|
||||
/* biome-ignore lint: Performance mode style requires priority */
|
||||
-webkit-backdrop-filter: blur(5px) !important;
|
||||
}
|
||||
|
||||
/* Feed sidebar panel - black tint background for readability */
|
||||
[data-test="feed-sidebar"] {
|
||||
/* biome-ignore lint: Ensure readability over media */
|
||||
background-color: rgba(0, 0, 0, 0.5) !important;
|
||||
/* biome-ignore lint: Ensure readability over media */
|
||||
backdrop-filter: blur(10px) !important;
|
||||
/* biome-ignore lint: Ensure readability over media */
|
||||
-webkit-backdrop-filter: blur(10px) !important;
|
||||
}
|
||||
|
||||
/* Performance mode: reduce sidebar backdrop blur */
|
||||
.performance-mode [data-test="feed-sidebar"] {
|
||||
/* biome-ignore lint: Performance mode style requires priority */
|
||||
backdrop-filter: blur(5px) !important;
|
||||
/* biome-ignore lint: Performance mode style requires priority */
|
||||
-webkit-backdrop-filter: blur(5px) !important;
|
||||
}
|
||||
|
||||
@@ -162,10 +160,12 @@ main,
|
||||
[class*="_cellContainer"],
|
||||
[data-test="feed-interval"],
|
||||
[data-test="feed-item"] {
|
||||
/* biome-ignore lint: Match theme transparency */
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Remove bottom gradient */
|
||||
[class^="_bottomGradient"] {
|
||||
/* biome-ignore lint: Explicitly remove conflicting gradient */
|
||||
display: none !important;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/* Floating Rounded Player Bar from Obsidian <3 */
|
||||
|
||||
/* MARKER: Floating Player Bar CSS */
|
||||
|
||||
[data-test="footer-player"] {
|
||||
position: absolute !important;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--wave-color-opacity-contrast-fill-ultra-thin, rgba(255, 255, 255, 0.1)) !important;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,49 +2,67 @@
|
||||
@font-face {
|
||||
font-family: "AbyssFont";
|
||||
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-family: "AbyssFont";
|
||||
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-family: "AbyssFont";
|
||||
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-family: "AbyssFont";
|
||||
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 */
|
||||
[class*="_lyricsText"] > div > span[data-current="true"] {
|
||||
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;
|
||||
transition-duration: 0.7s;
|
||||
font-size: 55px;
|
||||
color: white !important;
|
||||
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-size: calc(55px * var(--rl-font-scale, 1));
|
||||
/* biome-ignore lint: Active lyric uses Colorama color */
|
||||
color: var(--cl-glow1, #fff) !important;
|
||||
font-family:
|
||||
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
[class*="_lyricsText"] > div > span {
|
||||
text-shadow: 0 0 0px transparent, 0 0 0px transparent;
|
||||
text-shadow:
|
||||
0 0 0px transparent,
|
||||
0 0 0px transparent;
|
||||
transition-duration: 0.25s;
|
||||
color: rgba(128, 128, 128, 0.4);
|
||||
font-size: 40px;
|
||||
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-size: calc(40px * var(--rl-font-scale, 1));
|
||||
font-family:
|
||||
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
[class*="_lyricsText"] > div > span:hover {
|
||||
text-shadow: 0 0 2px lightgray, 0 0 20px lightgray !important;
|
||||
text-shadow:
|
||||
0 0 var(--rl-glow-inner, 2px) lightgray,
|
||||
/* biome-ignore lint: Hover glow should override defaults */
|
||||
0 0 var(--rl-glow-outer, 20px) lightgray !important;
|
||||
/* biome-ignore lint: Hover color override */
|
||||
color: lightgray !important;
|
||||
padding-left: 20px;
|
||||
transition-duration: 0.7s;
|
||||
@@ -52,36 +70,396 @@
|
||||
|
||||
/* Track title glow */
|
||||
[data-test="now-playing-track-title"] {
|
||||
text-shadow: 0 0 1px #fff, 0 0 30px #fff !important;
|
||||
/* Title text color/gradient is left to default app styling; only glow is customized. */
|
||||
text-shadow:
|
||||
0 0 var(--rl-glow-inner, 1px) var(--cl-glow1, #fff),
|
||||
/* biome-ignore lint: Title glow needs priority */
|
||||
0 0 var(--rl-glow-outer, 30px) #fff !important;
|
||||
/* biome-ignore lint: Reset vendor background clip */
|
||||
-webkit-background-clip: initial !important;
|
||||
/* biome-ignore lint: Reset background clip */
|
||||
background-clip: initial !important;
|
||||
/* biome-ignore lint: Reset vendor text fill */
|
||||
-webkit-text-fill-color: initial !important;
|
||||
/* biome-ignore lint: Ensure inherited color takes precedence */
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* When track title glow setting is disabled, remove glow regardless of Colorama */
|
||||
.rl-title-glow-disabled[data-test="now-playing-track-title"] {
|
||||
/* biome-ignore lint: Full reset required */
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Current line transitions */
|
||||
[class*="_lyricsText"] > div > span {
|
||||
transition: text-shadow 0.7s ease-in-out, color 0.7s ease-in-out, padding 0.7s ease-in-out !important;
|
||||
transition:
|
||||
text-shadow 0.7s ease-in-out,
|
||||
color 0.7s ease-in-out,
|
||||
/* biome-ignore lint: Transition priority needed */
|
||||
padding 0.7s ease-in-out !important;
|
||||
}
|
||||
|
||||
/* Glow-aware left padding so the glow doesn't clip | Thanks Aya <3*/
|
||||
.rl-wbw-active {
|
||||
padding-left: var(--rl-glow-outer) !important;
|
||||
}
|
||||
|
||||
[data-rl-injected][role="tabpanel"] {
|
||||
transform: translateX(calc(var(--rl-glow-outer) * -1)) !important;
|
||||
}
|
||||
|
||||
/* Lyrics container styling */
|
||||
[class^="_lyricsContainer"] > div > div > span {
|
||||
margin-bottom: 2rem;
|
||||
opacity: 1;
|
||||
font-family: "AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-family:
|
||||
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 38px !important;
|
||||
/* biome-ignore lint: Typography override for readability */
|
||||
font-size: calc(38px * var(--rl-font-scale, 1)) !important;
|
||||
}
|
||||
|
||||
/* Reset all lyrics styling when disabled */
|
||||
.lyrics-glow-disabled [class*="_lyricsText"] > div > span[data-current="true"],
|
||||
.lyrics-glow-disabled [class*="_lyricsText"] > div > span,
|
||||
.lyrics-glow-disabled [class*="_lyricsText"] > div > span:hover,
|
||||
.lyrics-glow-disabled [data-test="now-playing-track-title"],
|
||||
.lyrics-glow-disabled [class^="_lyricsContainer"] > div > div > span {
|
||||
text-shadow: none !important;
|
||||
padding-left: 0 !important;
|
||||
transition: none !important;
|
||||
font-size: inherit !important;
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
margin-bottom: inherit !important;
|
||||
opacity: inherit !important;
|
||||
/* MARKER: WBW lyrics CSS */
|
||||
|
||||
/* hide tidal spans for wbw */
|
||||
.rl-wbw-active span[data-test="lyrics-line"] {
|
||||
/* biome-ignore lint: Must hide original lines when word-by-word is on */
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Active line slide */
|
||||
.rl-wbw-line {
|
||||
text-align: left;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
filter: none;
|
||||
transform: translateZ(0);
|
||||
transform-origin: left;
|
||||
transition:
|
||||
filter 0.4s ease,
|
||||
padding-left 0.7s ease-in-out,
|
||||
padding-right 0.7s ease-in-out;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.rl-wbw-line.rl-wbw-spacer {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
/* Blur Inactive (opt-in via .rl-blur-active on container) */
|
||||
.rl-blur-active .rl-wbw-line {
|
||||
filter: blur(0.07em);
|
||||
}
|
||||
|
||||
.rl-blur-active .rl-wbw-line.rl-pos-1 {
|
||||
filter: blur(0.035em);
|
||||
}
|
||||
|
||||
.rl-blur-active .rl-wbw-line.rl-pos-2 {
|
||||
filter: blur(0.05em);
|
||||
}
|
||||
|
||||
.rl-blur-active .rl-wbw-line.rl-pos-3 {
|
||||
filter: blur(0.06em);
|
||||
}
|
||||
|
||||
/* Active line overrides (MUST come after blur rules to win on equal specificity) */
|
||||
.rl-wbw-line.rl-wbw-line-active,
|
||||
.rl-blur-active .rl-wbw-line.rl-wbw-line-active {
|
||||
padding-left: 20px;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
/* Keep last-active line unblurred during instrumental gaps */
|
||||
.rl-blur-active .rl-wbw-line.rl-gap-hold {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
/* Bubbled Lyrics scale (opt-in via .rl-bubbled on container) */
|
||||
.rl-bubbled .rl-wbw-line {
|
||||
scale: 0.93 0.93 0.95;
|
||||
transition:
|
||||
scale 0.7s ease,
|
||||
filter 0.4s ease,
|
||||
padding-left 0.7s ease-in-out,
|
||||
padding-right 0.7s ease-in-out;
|
||||
will-change: scale, translate, filter;
|
||||
}
|
||||
|
||||
.rl-bubbled .rl-wbw-line.rl-wbw-spacer {
|
||||
scale: none;
|
||||
}
|
||||
|
||||
.rl-bubbled .rl-wbw-line.rl-wbw-line-active {
|
||||
scale: 1;
|
||||
transition:
|
||||
scale 0.5s ease,
|
||||
filter 0.4s ease,
|
||||
padding-left 0.7s ease-in-out,
|
||||
padding-right 0.7s ease-in-out;
|
||||
}
|
||||
|
||||
/* Staggered scroll bounce animation (part of Bubbled Lyrics) */
|
||||
@keyframes rl-scroll-bounce {
|
||||
from {
|
||||
translate: 0 var(--rl-scroll-delta);
|
||||
}
|
||||
to {
|
||||
translate: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.rl-wbw-line:not(.rl-scroll-animate) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.rl-scroll-animate {
|
||||
animation: rl-scroll-bounce 400ms cubic-bezier(0.41, 0, 0.12, 0.99) both;
|
||||
animation-delay: var(--rl-line-delay, 0ms);
|
||||
}
|
||||
|
||||
/* Word span */
|
||||
.rl-wbw-word {
|
||||
text-shadow:
|
||||
0 0 0px transparent,
|
||||
0 0 0px transparent;
|
||||
color: rgba(128, 128, 128, 0.4);
|
||||
transition:
|
||||
text-shadow 0.15s ease-out,
|
||||
color 0.15s ease-out;
|
||||
}
|
||||
|
||||
/* Hover word (Grouped Syllables) */
|
||||
.rl-wbw-line:not(.rl-wbw-line-active) > .rl-wbw-word:hover,
|
||||
.rl-wbw-line:not(.rl-wbw-line-active) > .rl-wbw-word.rl-wbw-word-hover,
|
||||
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-main .rl-wbw-word:hover,
|
||||
.rl-wbw-line:not(.rl-wbw-line-active)
|
||||
.rl-wbw-main
|
||||
.rl-wbw-word.rl-wbw-word-hover {
|
||||
text-shadow:
|
||||
0 0 var(--rl-glow-inner, 2px) lightgray,
|
||||
/* biome-ignore lint: Hover glow should override defaults */
|
||||
0 0 var(--rl-glow-outer, 20px) lightgray !important;
|
||||
/* biome-ignore lint: Hover color override */
|
||||
color: lightgray !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Active word */
|
||||
.rl-wbw-word.rl-wbw-active {
|
||||
text-shadow:
|
||||
0 0 var(--rl-glow-inner, 2px) var(--cl-glow1, #fff),
|
||||
/* biome-ignore lint: Glow priority for active word */
|
||||
0 0 var(--rl-glow-outer, 20px) var(--cl-glow2, #fff) !important;
|
||||
/* biome-ignore lint: Active word uses Colorama color */
|
||||
color: var(--cl-glow1, #fff) !important;
|
||||
}
|
||||
|
||||
|
||||
/* MARKER: Syllable sweep animation CSS */
|
||||
|
||||
@keyframes rl-wipe {
|
||||
from {
|
||||
background-size:
|
||||
0.75em 100%,
|
||||
0% 100%,
|
||||
100% 100%;
|
||||
background-position:
|
||||
-0.375em 0%,
|
||||
left,
|
||||
left;
|
||||
}
|
||||
to {
|
||||
background-size:
|
||||
0.75em 100%,
|
||||
100% 100%,
|
||||
100% 100%;
|
||||
background-position:
|
||||
calc(100% + 0.375em) 0%,
|
||||
left,
|
||||
left;
|
||||
}
|
||||
}
|
||||
|
||||
/* Syllable active: gradient sweep L-to-R via background-clip */
|
||||
.rl-wbw-word.rl-syl-active {
|
||||
/* biome-ignore lint: Kill base transitions so class swaps are instant */
|
||||
transition: none !important;
|
||||
/* biome-ignore lint: Transparent fill so gradient paints the text */
|
||||
color: transparent !important;
|
||||
/* biome-ignore lint: Clip gradient to text glyphs */
|
||||
-webkit-background-clip: text !important;
|
||||
/* biome-ignore lint: Clip gradient to text glyphs */
|
||||
background-clip: text !important;
|
||||
background-repeat: no-repeat;
|
||||
background-image:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--cl-glow1, #fff) 50%,
|
||||
transparent 100%
|
||||
),
|
||||
linear-gradient(90deg, var(--cl-glow1, #fff) 100%, transparent 100%),
|
||||
linear-gradient(90deg, rgba(128, 128, 128, 0.4), rgba(128, 128, 128, 0.4));
|
||||
background-size:
|
||||
0.75em 100%,
|
||||
0% 100%,
|
||||
100% 100%;
|
||||
background-position:
|
||||
-0.375em 0%,
|
||||
left,
|
||||
left;
|
||||
/* biome-ignore lint: No glow for syllable mode */
|
||||
text-shadow: none !important;
|
||||
/* biome-ignore lint: No glow for syllable mode */
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
/* Syllable finished: word stays Colorama color, no glow */
|
||||
.rl-wbw-word.rl-syl-finished {
|
||||
/* biome-ignore lint: Kill base transitions so class swaps are instant */
|
||||
transition: none !important;
|
||||
/* biome-ignore lint: Finished syllable uses Colorama color */
|
||||
color: var(--cl-glow1, #fff) !important;
|
||||
/* biome-ignore lint: No glow for syllable mode */
|
||||
text-shadow: none !important;
|
||||
/* biome-ignore lint: No glow for syllable mode */
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
/* MARKER: Syllable animations CSS (WIP coming soon) */
|
||||
/* syllableStyle: 0 = none, 1 = Pop!, 2 = Jump */
|
||||
|
||||
@keyframes rl-pop {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
25%,
|
||||
35% {
|
||||
transform: scale(1.03) translateY(-0.5%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rl-jump {
|
||||
0% {
|
||||
transform: translateY(8px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Pop! for word mode */
|
||||
.rl-syl-pop .rl-wbw-word.rl-wbw-active {
|
||||
transform-origin: center bottom;
|
||||
animation: rl-pop 0.6s ease-out;
|
||||
}
|
||||
|
||||
/* Pop! for syllable mode */
|
||||
.rl-syl-pop .rl-wbw-word.rl-syl-active {
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
/* Jump for word mode */
|
||||
.rl-syl-jump .rl-wbw-word.rl-wbw-active {
|
||||
animation: rl-jump 0.35s ease-out;
|
||||
}
|
||||
|
||||
/* Tidals "..." at the top of the container */
|
||||
.rl-wbw-active > span:not([data-test="lyrics-line"]) {
|
||||
display: block;
|
||||
font-size: calc(40px * var(--rl-font-scale, 1));
|
||||
font-family:
|
||||
"AbyssFont", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
font-weight: 700;
|
||||
color: rgba(128, 128, 128, 0.4);
|
||||
text-shadow: 0 0 0px transparent;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* MARKER: Context Aware Lyrics CSS */
|
||||
|
||||
/* Background vocal sub-container */
|
||||
.rl-wbw-bg-container {
|
||||
max-height: 0;
|
||||
overflow: visible;
|
||||
opacity: 0;
|
||||
font-size: 0.55em;
|
||||
padding-top: 0.15em;
|
||||
transition:
|
||||
max-height 0.3s ease,
|
||||
opacity 0.5s ease;
|
||||
color: rgba(128, 128, 128, 0.4);
|
||||
}
|
||||
|
||||
.rl-wbw-line.rl-wbw-line-active .rl-wbw-bg-container {
|
||||
max-height: 3em;
|
||||
opacity: 1;
|
||||
transition:
|
||||
max-height 0.5s ease,
|
||||
opacity 0.5s ease;
|
||||
}
|
||||
|
||||
/* Singer duet positioning */
|
||||
.rl-wbw-line.rl-singer-right {
|
||||
text-align: end;
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
.rl-dual-side .rl-wbw-line.rl-singer-left {
|
||||
padding-right: 20%;
|
||||
}
|
||||
|
||||
.rl-dual-side .rl-wbw-line.rl-singer-right {
|
||||
padding-left: 20%;
|
||||
}
|
||||
|
||||
.rl-dual-side .rl-wbw-line.rl-singer-right .rl-wbw-bg-container {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.rl-dual-side .rl-wbw-line.rl-singer-right.rl-wbw-line-active {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.rl-dual-side .rl-wbw-line.rl-singer-left.rl-wbw-line-active {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
/* Reset glow when disabled */
|
||||
.lyrics-glow-disabled [class*="_lyricsText"] > div > span[data-current="true"],
|
||||
.lyrics-glow-disabled [class*="_lyricsText"] > div > span: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 */
|
||||
.radiant-lyrics-ui-hidden:has([data-test="footer-player"]:hover) [data-test="footer-player"],
|
||||
.radiant-lyrics-ui-hidden body.rl-footer-hover [data-test="footer-player"],
|
||||
.radiant-lyrics-ui-hidden [data-test="footer-player"]:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
@@ -1,218 +1,66 @@
|
||||
/* Sidebar */
|
||||
[class*="_sidebar_"] {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Section header */
|
||||
[class*="_sectionHeader_"] {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Rounded corners */
|
||||
[class*="_thumbnail_"],
|
||||
[class*="_imageWrapper_"],
|
||||
[class*="_coverImage_"],
|
||||
[class*="_overlayIconWrapperAlbum_"],
|
||||
[class*="_playButton_"] {
|
||||
border-radius: 5px !important;
|
||||
}
|
||||
|
||||
|
||||
/* MARKER: HideUI CSS*/
|
||||
|
||||
/* Only apply styles when UI is hidden */
|
||||
.radiant-lyrics-ui-hidden [class*="tabItems"] {
|
||||
opacity: 0 !important;
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
/* Default state - visible */
|
||||
[class*="tabItems"] {
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
.radiant-lyrics-ui-hidden [class*="tabItems"]: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) */
|
||||
|
||||
.radiant-lyrics-ui-hidden [data-test="header-container"]:not(:has(.hide-ui-button)) {
|
||||
/* Hide header container (search, minimize, fullscreen) when UI is hidden */
|
||||
.radiant-lyrics-ui-hidden [data-test="header-container"] {
|
||||
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 */
|
||||
.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) */
|
||||
/* Immediate hide class for unhide button */
|
||||
.hide-immediately {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
[class^="_bar"] {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.radiant-lyrics-ui-hidden [class^="_bar"]>*:not(.hide-ui-button) {
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
/* Default state for bar elements */
|
||||
[class^="_bar"]>* {
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
/* Hide search box and make it non-interactive */
|
||||
.radiant-lyrics-ui-hidden [data-test="search-input"],
|
||||
.radiant-lyrics-ui-hidden [class*="_searchInput"],
|
||||
.radiant-lyrics-ui-hidden [class*="searchInput"],
|
||||
.radiant-lyrics-ui-hidden [class*="_search"],
|
||||
.radiant-lyrics-ui-hidden [class*="search"],
|
||||
.radiant-lyrics-ui-hidden input[type="search"],
|
||||
.radiant-lyrics-ui-hidden input[type="text"],
|
||||
.radiant-lyrics-ui-hidden input[placeholder*="Search"],
|
||||
.radiant-lyrics-ui-hidden input[placeholder*="search"],
|
||||
.radiant-lyrics-ui-hidden [placeholder*="Search"],
|
||||
.radiant-lyrics-ui-hidden [data-test="main-layout-header"] input,
|
||||
.radiant-lyrics-ui-hidden [data-test="main-layout-header"] [class*="input"],
|
||||
.radiant-lyrics-ui-hidden header input,
|
||||
.radiant-lyrics-ui-hidden nav input {
|
||||
pointer-events: none !important;
|
||||
cursor: default !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
/* Hide bottom left controls completely - no hover functionality */
|
||||
/* Exclude heart button in player bar and make sure hidden buttons can't be clicked */
|
||||
.radiant-lyrics-ui-hidden [data-test="add-to-playlist"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [data-test="remove-from-playlist"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [data-test="like-toggle"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [data-test="dislike-toggle"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [data-test="favorite-toggle"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [data-test="heart-button"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [data-test="playlist-add"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [class*="_trackActions"],
|
||||
.radiant-lyrics-ui-hidden [class*="_bottomLeftControls"],
|
||||
.radiant-lyrics-ui-hidden [class*="_actionButtons"],
|
||||
.radiant-lyrics-ui-hidden [class*="_favoriteButton"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden [class*="_addToPlaylist"],
|
||||
.radiant-lyrics-ui-hidden [class*="_lowerLeft"],
|
||||
.radiant-lyrics-ui-hidden [class*="_bottomActions"],
|
||||
.radiant-lyrics-ui-hidden [class*="_mediaControls"] > div:first-child,
|
||||
.radiant-lyrics-ui-hidden button[title*="Add to"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[title*="Remove from"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[title*="Like"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[title*="Favorite"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[title*="Heart"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[aria-label*="Add to"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[aria-label*="Remove from"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[aria-label*="Like"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[aria-label*="Favorite"]:not([data-test="footer-player"] *),
|
||||
.radiant-lyrics-ui-hidden button[aria-label*="Heart"]:not([data-test="footer-player"] *),
|
||||
/* Target buttons in bottom left area specifically - (idk if this is needed.. but it's here) */
|
||||
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] button[class*="_button"]:not(.unhide-ui-button),
|
||||
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] [class*="_iconButton"]:not(.unhide-ui-button),
|
||||
/* Additional catch-all for bottom left area buttons - (idk if this is needed.. but it's here) */
|
||||
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] > div > div:first-child button:not(.unhide-ui-button),
|
||||
.radiant-lyrics-ui-hidden [class*="_nowPlayingContainer"] > div:first-child button:not(.unhide-ui-button) {
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
transition: opacity 0.5s ease-in-out !important;
|
||||
}
|
||||
|
||||
/* No hover functionality in Hide UI Mode - buttons stay hidden.. yea thats right, you heard me */
|
||||
|
||||
/* Default state for control buttons */
|
||||
[data-test="add-to-playlist"],
|
||||
[data-test="remove-from-playlist"],
|
||||
[data-test="like-toggle"],
|
||||
[data-test="dislike-toggle"],
|
||||
[data-test="favorite-toggle"],
|
||||
[data-test="heart-button"],
|
||||
[data-test="playlist-add"],
|
||||
[class*="_trackActions"],
|
||||
[class*="_bottomLeftControls"],
|
||||
[class*="_actionButtons"],
|
||||
[class*="_favoriteButton"],
|
||||
[class*="_addToPlaylist"],
|
||||
[class*="_lowerLeft"],
|
||||
[class*="_bottomActions"],
|
||||
[class*="_mediaControls"] > div:first-child,
|
||||
button[title*="Add to"],
|
||||
button[title*="Remove from"],
|
||||
button[title*="Like"],
|
||||
button[title*="Favorite"],
|
||||
button[title*="Heart"],
|
||||
button[aria-label*="Add to"],
|
||||
button[aria-label*="Remove from"],
|
||||
button[aria-label*="Like"],
|
||||
button[aria-label*="Favorite"],
|
||||
button[aria-label*="Heart"],
|
||||
[class*="_nowPlayingContainer"] button[class*="_button"]:not(.unhide-ui-button),
|
||||
[class*="_nowPlayingContainer"] [class*="_iconButton"]:not(.unhide-ui-button),
|
||||
[class*="_nowPlayingContainer"] > div > div:first-child button:not(.unhide-ui-button),
|
||||
[class*="_nowPlayingContainer"] > div:first-child button:not(.unhide-ui-button) {
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* Smooth cover art movement when UI is hidden */
|
||||
[class*="_albumImage"],
|
||||
[class*="_coverArt"],
|
||||
figure[class*="_albumImage"] {
|
||||
transition: transform 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
.radiant-lyrics-ui-hidden [class*="_albumImage"],
|
||||
.radiant-lyrics-ui-hidden [class*="_coverArt"],
|
||||
.radiant-lyrics-ui-hidden figure[class*="_albumImage"] {
|
||||
transform: translateX(80px) !important;
|
||||
}
|
||||
|
||||
/* Smooth track info wrapper movement when UI is hidden (Arists & Track Title) */
|
||||
[class*="_infoWrapper"],
|
||||
[class*="_textContainer"] {
|
||||
transition: transform 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
.radiant-lyrics-ui-hidden [class*="_infoWrapper"],
|
||||
.radiant-lyrics-ui-hidden [class*="_textContainer"] {
|
||||
transform: translateX(40px) !important;
|
||||
}
|
||||
|
||||
/* Move parent containers instead of lyrics container directly to preserve gradient fade */
|
||||
[data-test="stream-metadata"],
|
||||
[class*="_rightColumn"],
|
||||
[class*="_rightSide"],
|
||||
[class*="_contentRight"],
|
||||
[class*="_sidePanel"],
|
||||
[class*="_lyricsSection"],
|
||||
[class*="_lyricsWrapper"] {
|
||||
transition: transform 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
.radiant-lyrics-ui-hidden [data-test="stream-metadata"],
|
||||
.radiant-lyrics-ui-hidden [class*="_rightColumn"],
|
||||
.radiant-lyrics-ui-hidden [class*="_rightSide"],
|
||||
.radiant-lyrics-ui-hidden [class*="_contentRight"],
|
||||
.radiant-lyrics-ui-hidden [class*="_sidePanel"],
|
||||
.radiant-lyrics-ui-hidden [class*="_lyricsSection"],
|
||||
.radiant-lyrics-ui-hidden [class*="_lyricsWrapper"] {
|
||||
transform: translateX(60px) translateY(-70px) !important;
|
||||
}
|
||||
|
||||
/* Hide UI button base styling - just the transition */
|
||||
.hide-ui-button {
|
||||
transition: opacity 0.5s ease-in-out, visibility 0.5s ease-in-out, background-color 0.2s ease-in-out, transform 0.2s ease-in-out !important;
|
||||
}
|
||||
|
||||
/* Auto-fade styling for unhide button - (Keeps Text Visible, just not full opacity) | Cheers @Zyhn for the idea*/
|
||||
/* Auto-fade styling for unhide button */
|
||||
.unhide-ui-button.auto-faded {
|
||||
background-color: transparent !important;
|
||||
border-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
transition: background-color 0.8s ease-in-out, border-color 0.8s ease-in-out, box-shadow 0.8s ease-in-out, backdrop-filter 0.8s ease-in-out, color 0.8s ease-in-out;
|
||||
color: rgba(255, 255, 255, 0.4) !important;
|
||||
transition:
|
||||
background-color 0.8s ease-in-out,
|
||||
border-color 0.8s ease-in-out,
|
||||
box-shadow 0.8s ease-in-out,
|
||||
backdrop-filter 0.8s ease-in-out,
|
||||
color 0.8s ease-in-out !important;
|
||||
}
|
||||
|
||||
/* Restore button styling on hover */
|
||||
.unhide-ui-button.auto-faded:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
border-color: rgba(255, 255, 255, 0.3) !important;
|
||||
@@ -220,5 +68,284 @@ figure[class*="_albumImage"] {
|
||||
backdrop-filter: blur(10px) !important;
|
||||
-webkit-backdrop-filter: blur(10px) !important;
|
||||
color: white !important;
|
||||
transition: background-color 0.3s ease-in-out, border-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out, backdrop-filter 0.3s ease-in-out, color 0.3s ease-in-out;
|
||||
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 tab */
|
||||
[data-test="tabs-lyrics"]:has(.sticky-lyrics-trigger) {
|
||||
position: relative !important;
|
||||
padding-right: 38px !important;
|
||||
}
|
||||
|
||||
/* Trigger */
|
||||
.sticky-lyrics-trigger {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-left: 5px;
|
||||
padding-right: 0px;
|
||||
box-sizing: border-box;
|
||||
cursor: default;
|
||||
color: #CCCCD1;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Divider line */
|
||||
.sticky-lyrics-trigger::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
width: 1px;
|
||||
background: transparent;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
/* When Lyrics tab is active — show divider & make icon black*/
|
||||
[data-test="tabs-lyrics"][aria-selected="true"] .sticky-lyrics-trigger {
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-test="tabs-lyrics"][aria-selected="true"] .sticky-lyrics-trigger::before {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
[data-test="tabs-lyrics"][aria-selected="true"] .sticky-lyrics-trigger:hover {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* Square the Lyrics button bottom corners when dropdown is open */
|
||||
[data-test="tabs-lyrics"].sticky-lyrics-open {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Dropdown */
|
||||
.sticky-lyrics-dropdown {
|
||||
position: fixed;
|
||||
background: white;
|
||||
border-radius: 0 0 16px 16px;
|
||||
padding: 8px 12px 10px;
|
||||
box-sizing: border-box;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
|
||||
clip-path: inset(0 -20px -20px -20px);
|
||||
animation: stickyLyricsDropdownIn 0.12s ease-out;
|
||||
}
|
||||
|
||||
@keyframes stickyLyricsDropdownIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
clip-path: inset(0 0 100% 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Row containing label + toggle */
|
||||
.sticky-lyrics-dropdown-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sticky-lyrics-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Toggle switch */
|
||||
.sticky-lyrics-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 34px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sticky-lyrics-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.sticky-lyrics-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
transition: 0.3s;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.sticky-lyrics-slider::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: white;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.sticky-lyrics-switch input:checked + .sticky-lyrics-slider {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.sticky-lyrics-switch input:checked + .sticky-lyrics-slider::before {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
/* Segmented control (Line | Word | Syllable) */
|
||||
.rl-style-row {
|
||||
justify-content: center;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.rl-seg-control {
|
||||
display: flex;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
border-radius: 10px;
|
||||
padding: 2px;
|
||||
gap: 2px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rl-seg-btn {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 5px 0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rl-seg-btn:hover {
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.rl-seg-btn.rl-seg-active {
|
||||
background: white;
|
||||
color: black;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* MARKER: PATCHES (Random Fixes for Tidals Changes) */
|
||||
/* These change allot so i gave them their own section */
|
||||
|
||||
/* Fixes the new Sticky Header Tidal added.. in the shittest jankiest way possible */
|
||||
/* [class*="_stickyHeader"] {
|
||||
background: transparent !important;
|
||||
backdrop-filter: blur(50px);
|
||||
background-color: transparent !important;
|
||||
width: fit-content !important;
|
||||
padding-right: 3.5% !important;
|
||||
-webkit-mask-image:
|
||||
linear-gradient(to bottom, black 60%, transparent),
|
||||
linear-gradient(to right, black 85%, transparent) !important;
|
||||
mask-image:
|
||||
linear-gradient(to bottom, black 60%, transparent),
|
||||
linear-gradient(to right, black 85%, transparent) !important;
|
||||
-webkit-mask-composite: source-in !important;
|
||||
mask-composite: intersect !important;
|
||||
padding-bottom: 5px !important;
|
||||
}
|
||||
|
||||
[class*="_playQueueItems"]{
|
||||
border-radius: 2.5px 0 0 0 !important;
|
||||
}
|
||||
|
||||
[data-test="playqueue-sticky-clear-active-items"] {
|
||||
visibility: collapse !important;
|
||||
width: 0px !important;
|
||||
}
|
||||
|
||||
[data-test="playqueue-sticky-clear-source-items"] {
|
||||
visibility: collapse !important;
|
||||
width: 0px !important;
|
||||
} */
|
||||
|
||||
|
||||
/* Remove the background color from the small header */
|
||||
[class*="_smallHeader"]::before {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* fixes Tidals broken mini cover art padding | Cheers Aya <3*/
|
||||
._imageBorder_110890a {
|
||||
filter: opacity(0);
|
||||
}
|
||||
._container_14bcbd4._playingFrom_79b167e {
|
||||
transform: scale(1.01) translatex(.1em);
|
||||
}
|
||||
._leftColumn_aaf28de {
|
||||
min-height: 110%;
|
||||
transform: translatey(-.23em);
|
||||
}
|
||||
._imageryContainer_f99fc07.image {
|
||||
transform: scale(1.03) translatey(.2em) translatex(.1em);
|
||||
background-color: #00000000;
|
||||
padding: 0em !important;
|
||||
}
|
||||
._image_145331a._cellImage_0ef8dd3 {
|
||||
border-radius: .7em !important;
|
||||
}
|
||||
|
||||
[data-test="footer-player"] {
|
||||
._container_14bcbd4._playingFrom_79b167e > ._text_15008b2._medium20_1lyag_192._marketText_1lyag_1 {
|
||||
transform: translatey(-.2em);
|
||||
}
|
||||
[class="image _imageryContainer_f99fc07"] {
|
||||
transform: translatey(.3em) !important;
|
||||
}
|
||||
._image_145331a._cellImage_0ef8dd3 {
|
||||
border-radius: .25em !important;
|
||||
}
|
||||
._toggleButton_809eee8 {
|
||||
transform: translateY(-.22em);
|
||||
}
|
||||
[class="image _imageryContainer_f99fc07"]:hover {
|
||||
[class="_cellImage_0ef8dd3 _image_145331a"] {
|
||||
filter: brightness(.3);
|
||||
}
|
||||
}
|
||||
._notFullscreenOverlay_1442d60 {
|
||||
background: none !important;
|
||||
transition: 0ms;
|
||||
}
|
||||
._notFullscreenOverlay_1442d60 ._nowPlayingButton_c1a86fa {
|
||||
background-color: rgba(245, 245, 220, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user