Merge branch 'main' of github.com:monochrome-music/monochrome

This commit is contained in:
Samidy 2026-04-06 15:55:34 +03:00
commit 704074d275
18 changed files with 1472 additions and 656 deletions

View file

@ -20,7 +20,6 @@ jobs:
# Copilot will be given its own token for its operations.
permissions:
contents: read
workflows: write
steps:
- name: Checkout code

View file

@ -104,6 +104,7 @@ jobs:
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git pull --rebase origin main
git add public/editors-picks.json public/editors-picks-old/
git diff --staged --quiet && echo "No changes to commit." && exit 0
git commit -m "chore: update editors picks"

43
.github/workflows/lighthouse.yml vendored Normal file
View file

@ -0,0 +1,43 @@
name: Lighthouse
on:
workflow_dispatch:
push:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Preview build
run: npm run preview &
continue-on-error: true
- name: Wait for preview server
run: sleep 10
- name: Run Lighthouse
run: |
npx lhci autorun --config=.lhci.yml || true
- name: Upload results
if: always()
uses: actions/upload-artifact@v4
with:
name: lighthouse-results
path: .lighthouseci/

506
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -21,4 +21,7 @@ album:250986538
album:509761344
album:15621057
album:103897783
album:151728406
album:151728406
album:199412873
album:3280432
album:37927851

View file

@ -19,4 +19,3 @@ new_func = """def download_and_process_cover(cover_uuid):
content = re.sub(r"def download_and_process_cover\(cover_uuid\):[\s\S]*?(?=def process_cover)", new_func + "\n\n", content)
with open("gen-editors-picks.py", "w") as f: f.write(content)

View file

@ -9,32 +9,58 @@ import hashlib
import time
import os
import tempfile
import base64
INPUT_FILE = "editors-picks-input.txt"
COUNTRY = "US"
# Tidal internal token replace when expired
TIDAL_TOKEN = "eyJraWQiOiJ2OU1GbFhqWSIsImFsZyI6IkVTMjU2In0.eyJ0eXBlIjoibzJfYWNjZXNzIiwic2NvcGUiOiIiLCJnVmVyIjowLCJzVmVyIjowLCJjaWQiOjEzNTU3LCJhdCI6IklOVEVSTkFMIiwiZXhwIjoxNzc1MzY0MTQwLCJpc3MiOiJodHRwczovL2F1dGgudGlkYWwuY29tL3YxIn0.6ui6itHVQ-OXPF0F9mbf5KcKz1fKYJNsa1vBAj60upXpcN-DQG8JPKBlqJN6RuBEH8yhwYj2wh4YJ-TOOuO8DA"
TIDAL_CLIENT_ID = "txNoH4kkV41MfH25"
TIDAL_CLIENT_SECRET = "dQjy0MinCEvxi1O4UmxvxWnDjt4cgHBPw8ll6nYBk98="
TIDAL_HEADERS = {
"accept": "*/*",
"authorization": f"Bearer {TIDAL_TOKEN}",
}
# PodcastIndex credentials
PODCAST_API_KEY = "YU5HMSDYBQQVYDF6QN4P"
PODCAST_API_SECRET = "8hCvpjSL7T$S7^5ftnf5MhqQwYUYVjM^fmUL3Ld$"
PODCASTINDEX_BASE = "https://api.podcastindex.org/api/1.0"
_tidal_token = None
# ── Tidal helpers ─────────────────────────────────────────────────────────────
def get_tidal_token():
global _tidal_token
if _tidal_token:
return _tidal_token
credentials = base64.b64encode(f"{TIDAL_CLIENT_ID}:{TIDAL_CLIENT_SECRET}".encode()).decode()
params = urllib.parse.urlencode({
"client_id": TIDAL_CLIENT_ID,
"client_secret": TIDAL_CLIENT_SECRET,
"grant_type": "client_credentials",
})
req = urllib.request.Request(
"https://auth.tidal.com/v1/oauth2/token",
data=params.encode(),
headers={
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": f"Basic {credentials}",
},
method="POST"
)
try:
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read().decode())
_tidal_token = data["access_token"]
return _tidal_token
except Exception as e:
print(f"Error getting Tidal token: {e}", file=sys.stderr)
return None
def tidal_get(path, params=None):
if params is None:
params = {}
params.setdefault("countryCode", COUNTRY)
token = get_tidal_token()
if not token:
return None
url = f"https://api.tidal.com/v1/{path}?{urllib.parse.urlencode(params)}"
req = urllib.request.Request(url, headers=TIDAL_HEADERS)
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
try:
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode())

View file

@ -205,13 +205,21 @@
z-index: 0;
"
></div>
<button id="fullscreen-dismiss-handle" type="button" aria-label="Dismiss fullscreen"></button>
<button
id="toggle-fullscreen-lyrics-mobile-btn"
class="fullscreen-mobile-lyrics-toggle"
title="Hide Lyrics"
>
<use svg="!lucide/mic-vocal.svg" size="18" />
</button>
<button id="toggle-ui-btn" class="fullscreen-ui-toggle" title="Toggle UI">
<use svg="!lucide/eye-off.svg" size="24" />
</button>
<button id="toggle-fullscreen-lyrics-btn" class="fullscreen-lyrics-toggle" title="Toggle Lyrics">
<use svg="!lucide/mic-vocal.svg" size="24" />
</button>
<div class="fullscreen-top-actions">
<button id="toggle-fullscreen-lyrics-btn" class="fullscreen-lyrics-toggle" title="Hide Lyrics">
<use svg="!lucide/mic-vocal.svg" size="20" />
</button>
<button id="fs-visualizer-btn" class="fs-visualizer-btn" title="Disable Visualizer">
<use svg="!lucide/audio-lines.svg" size="20" />
</button>
@ -257,6 +265,11 @@
</div>
<div class="fullscreen-controls">
<div
id="fullscreen-mobile-quality"
class="fullscreen-mobile-quality"
aria-hidden="true"
></div>
<div class="fullscreen-progress-container">
<span id="fs-current-time">0:00</span>
<div id="fs-progress-bar" class="progress-bar">

View file

@ -1013,12 +1013,16 @@ function applyFullscreenLyricsShadowTweaks(amLyrics, container) {
}
.lyrics-line {
transform-origin: left center;
transition:
opacity 0.42s ease,
transform 0.55s cubic-bezier(0.22, 1, 0.36, 1) var(--lyrics-line-delay, 0ms),
filter 0.48s cubic-bezier(0.22, 1, 0.36, 1) !important;
}
.lyrics-line:not(.active):not(.pre-active) {
opacity: 0.44;
}
.lyrics-line-container {
transition:
transform 0.72s cubic-bezier(0.22, 1, 0.36, 1),
@ -1033,6 +1037,10 @@ function applyFullscreenLyricsShadowTweaks(amLyrics, container) {
background-color 0.22s ease,
color 0.22s ease !important;
}
.lyrics-line.active .lyrics-line-container {
transform: scale(1.015);
}
`;
return true;

293
js/ui.js
View file

@ -93,6 +93,7 @@ const setFullscreenUIToggleIcon = (button, visualizerOnlyMode) => {
button.innerHTML = visualizerOnlyMode ? SVG_EYE(24) : SVG_EYE_OFF(24);
};
const isMobileFullscreenViewport = () => window.matchMedia('(max-width: 768px)').matches;
function sortTracks(tracks, sortType) {
if (sortType === 'custom') return [...tracks];
const sorted = [...tracks];
@ -155,6 +156,10 @@ export class UIRenderer {
this.renderLock = false;
this.lastRecommendedTracks = [];
this.currentArtistId = null;
this.fullscreenLyricsVisible = true;
this.fullscreenPlaybackStateCleanup = null;
this.fullscreenDismissHandleCleanup = null;
this.fullscreenLyricsToggleCleanup = null;
// Listen for dynamic color reset events
window.addEventListener('reset-dynamic-color', () => {
@ -545,7 +550,7 @@ export class UIRenderer {
type = 'album'
) {
let size = '320';
if (this.currentPage === 'search' || className === 'track-item-cover') {
if (className === 'track-item-cover') {
size = '80';
} else if (type === 'artist') {
size = '160';
@ -1095,9 +1100,13 @@ export class UIRenderer {
let r = parseInt(hex.substr(0, 2), 16);
let g = parseInt(hex.substr(2, 2), 16);
let b = parseInt(hex.substr(4, 2), 16);
let fullscreenR = r;
let fullscreenG = g;
let fullscreenB = b;
// Calculate perceived brightness
let brightness = (r * 299 + g * 587 + b * 114) / 1000;
let fullscreenBrightness = brightness;
if (isLightMode) {
// In light mode, the background is white.
@ -1124,6 +1133,23 @@ export class UIRenderer {
}
const adjustedColor = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
while (fullscreenBrightness < 105) {
fullscreenR = Math.min(255, Math.max(fullscreenR + 1, Math.floor(fullscreenR * 1.08)));
fullscreenG = Math.min(255, Math.max(fullscreenG + 1, Math.floor(fullscreenG * 1.08)));
fullscreenB = Math.min(255, Math.max(fullscreenB + 1, Math.floor(fullscreenB * 1.08)));
fullscreenBrightness = (fullscreenR * 299 + fullscreenG * 587 + fullscreenB * 114) / 1000;
if (fullscreenR >= 255 && fullscreenG >= 255 && fullscreenB >= 255) break;
}
while (fullscreenBrightness > 185) {
fullscreenR = Math.floor(fullscreenR * 0.92);
fullscreenG = Math.floor(fullscreenG * 0.92);
fullscreenB = Math.floor(fullscreenB * 0.92);
fullscreenBrightness = (fullscreenR * 299 + fullscreenG * 587 + fullscreenB * 114) / 1000;
}
const fullscreenAdjustedColor = `#${fullscreenR.toString(16).padStart(2, '0')}${fullscreenG
.toString(16)
.padStart(2, '0')}${fullscreenB.toString(16).padStart(2, '0')}`;
// Calculate contrast text color for buttons (text on top of the vibrant color)
const foreground = brightness > 128 ? '#000000' : '#ffffff';
@ -1135,6 +1161,8 @@ export class UIRenderer {
root.style.setProperty('--highlight-rgb', `${r}, ${g}, ${b}`);
root.style.setProperty('--active-highlight', adjustedColor);
root.style.setProperty('--ring', adjustedColor);
root.style.setProperty('--fs-accent', fullscreenAdjustedColor);
root.style.setProperty('--fs-accent-rgb', `${fullscreenR}, ${fullscreenG}, ${fullscreenB}`);
// Calculate a safe hover color
let hoverColor;
@ -1157,6 +1185,8 @@ export class UIRenderer {
root.style.removeProperty('--highlight-rgb');
root.style.removeProperty('--active-highlight');
root.style.removeProperty('--ring');
root.style.removeProperty('--fs-accent');
root.style.removeProperty('--fs-accent-rgb');
root.style.removeProperty('--track-hover-bg');
}
@ -1270,12 +1300,10 @@ export class UIRenderer {
currentImage.src = coverUrl;
}
}
overlay.style.setProperty('--bg-image', `url('${this.api.getCoverUrl(track.album?.cover, '1280')}')`);
await this.extractAndApplyColor(this.api.getCoverUrl(track.album?.cover, '80'));
}
const qualityBadge = this.getFullscreenQualityBadgeHTML(track);
title.innerHTML = `${escapeHtml(track.title)} ${qualityBadge}`;
this.updateFullscreenQualityBadgePlacement(track, overlay);
artist.textContent = getTrackArtists(track);
if (nextTrack) {
@ -1288,7 +1316,7 @@ export class UIRenderer {
async showFullscreenCover(track, nextTrack, lyricsManager, activeElement) {
if (!track) return;
this.fullscreenVisualizerSuppressed = true;
this.fullscreenVisualizerSuppressed = isMobileFullscreenViewport();
if (window.location.hash !== '#fullscreen') {
window.history.pushState({ fullscreen: true }, '', '#fullscreen');
}
@ -1312,12 +1340,14 @@ export class UIRenderer {
lyricsManager && activeElement && lyricsPane && lyricsContent && track.type !== 'video'
);
if (canRenderLyrics) {
lyricsToggleBtn.style.display = 'none';
this.fullscreenLyricsVisible = true;
if (lyricsToggleBtn) lyricsToggleBtn.style.removeProperty('display');
overlay.classList.remove('lyrics-unavailable');
clearFullscreenLyricsSync(lyricsContent);
await renderLyricsInFullscreen(track, activeElement, lyricsManager, lyricsContent);
} else {
lyricsToggleBtn.style.display = 'none';
this.fullscreenLyricsVisible = false;
if (lyricsToggleBtn) lyricsToggleBtn.style.display = 'none';
overlay.classList.add('lyrics-unavailable');
if (lyricsContent) {
clearFullscreenLyricsSync(lyricsContent);
@ -1325,6 +1355,7 @@ export class UIRenderer {
'<div class="fullscreen-lyrics-empty">Lyrics are not available for this track.</div>';
}
}
this.updateFullscreenLyricsVisibility(overlay);
const playerBar = document.querySelector('.now-playing-bar');
if (playerBar) playerBar.style.display = 'none';
@ -1355,9 +1386,85 @@ export class UIRenderer {
this.setupUIToggleButton(overlay);
this.setupControlsAutoHide(overlay);
this.setupFullscreenSidePanelSync(overlay);
this.setupFullscreenDismissHandle(overlay);
this.setupFullscreenLyricsToggle(overlay);
await this.refreshFullscreenVisualizerState(activeElement);
}
updateFullscreenLyricsVisibility(overlay = document.getElementById('fullscreen-cover-overlay')) {
if (!overlay) return;
const lyricsToggleButtons = [
document.getElementById('toggle-fullscreen-lyrics-btn'),
document.getElementById('toggle-fullscreen-lyrics-mobile-btn'),
].filter(Boolean);
const lyricsUnavailable = overlay.classList.contains('lyrics-unavailable');
const shouldShowLyrics = this.fullscreenLyricsVisible && !lyricsUnavailable;
overlay.classList.toggle('lyrics-hidden', !shouldShowLyrics);
this.updateFullscreenQualityBadgePlacement(this.player?.currentTrack, overlay);
lyricsToggleButtons.forEach((lyricsToggleBtn) => {
lyricsToggleBtn.classList.toggle('active', shouldShowLyrics);
lyricsToggleBtn.title = shouldShowLyrics ? 'Hide Lyrics' : 'Show Lyrics';
lyricsToggleBtn.setAttribute('aria-pressed', shouldShowLyrics ? 'true' : 'false');
if (lyricsUnavailable) {
lyricsToggleBtn.style.display = 'none';
} else {
lyricsToggleBtn.style.removeProperty('display');
}
});
}
updateFullscreenQualityBadgePlacement(track, overlay = document.getElementById('fullscreen-cover-overlay')) {
if (!track || !overlay) return;
const title = document.getElementById('fullscreen-track-title');
const mobileQuality = document.getElementById('fullscreen-mobile-quality');
if (!title) return;
const qualityBadge = this.getFullscreenQualityBadgeHTML(track);
const useMobileBadgeOnly =
window.matchMedia('(max-width: 768px)').matches && overlay.classList.contains('lyrics-hidden');
title.innerHTML = useMobileBadgeOnly ? escapeHtml(track.title) : `${escapeHtml(track.title)} ${qualityBadge}`;
if (mobileQuality) {
mobileQuality.innerHTML = useMobileBadgeOnly ? qualityBadge : '';
}
}
async dismissFullscreenCover({ animate = true } = {}) {
const overlay = document.getElementById('fullscreen-cover-overlay');
if (!overlay || overlay.style.display === 'none') return;
if (animate) {
await new Promise((resolve) => {
const finish = () => {
overlay.removeEventListener('transitionend', handleTransitionEnd);
overlay.classList.remove('fullscreen-dragging', 'fullscreen-dismissing');
overlay.style.removeProperty('--fullscreen-drag-offset');
overlay.style.removeProperty('--fullscreen-drag-progress');
resolve();
};
const handleTransitionEnd = (event) => {
if (event.target !== overlay.querySelector('.fullscreen-cover-content')) return;
finish();
};
overlay.addEventListener('transitionend', handleTransitionEnd);
overlay.classList.add('fullscreen-dismissing');
window.setTimeout(finish, 280);
});
}
this.closeFullscreenCover();
if (window.location.hash === '#fullscreen') {
window.history.back();
}
}
closeFullscreenCover() {
const overlay = document.getElementById('fullscreen-cover-overlay');
const coverImage = document.getElementById('fullscreen-cover-image');
@ -1370,7 +1477,16 @@ export class UIRenderer {
lyricsContent.innerHTML = '<div class="fullscreen-lyrics-empty">Lyrics appear here.</div>';
}
overlay.style.display = 'none';
overlay.classList.remove('visualizer-active', 'ui-hidden', 'fullscreen-cover-no-round', 'fullscreen-paused');
overlay.classList.remove(
'visualizer-active',
'ui-hidden',
'fullscreen-cover-no-round',
'fullscreen-paused',
'fullscreen-dragging',
'fullscreen-dismissing'
);
overlay.style.removeProperty('--fullscreen-drag-offset');
overlay.style.removeProperty('--fullscreen-drag-progress');
const playerBar = document.querySelector('.now-playing-bar');
if (playerBar) playerBar.style.removeProperty('display');
@ -1434,6 +1550,16 @@ export class UIRenderer {
this.fullscreenSidePanelSyncCleanup();
this.fullscreenSidePanelSyncCleanup = null;
}
if (this.fullscreenDismissHandleCleanup) {
this.fullscreenDismissHandleCleanup();
this.fullscreenDismissHandleCleanup = null;
}
if (this.fullscreenLyricsToggleCleanup) {
this.fullscreenLyricsToggleCleanup();
this.fullscreenLyricsToggleCleanup = null;
}
}
async startFullscreenVisualizer(activeElement, overlay) {
@ -1448,6 +1574,7 @@ export class UIRenderer {
}
if (this.visualizer) {
this.visualizer.applyPresetOverride('kawarp');
await this.visualizer.start();
overlay.classList.add('visualizer-active');
}
@ -1493,7 +1620,7 @@ export class UIRenderer {
const visualizerBtn = document.getElementById('fs-visualizer-btn');
const toggleBtn = document.getElementById('toggle-ui-btn');
const isVideoTrack = this.player?.currentTrack?.type === 'video';
const enabled = visualizerSettings.isEnabled() && !isVideoTrack && !this.fullscreenVisualizerSuppressed;
const enabled = !isVideoTrack && !this.fullscreenVisualizerSuppressed && !isMobileFullscreenViewport();
if (!overlay) return;
@ -1578,7 +1705,6 @@ export class UIRenderer {
}
this.fullscreenVisualizerSuppressed = false;
visualizerSettings.setEnabled(true);
await this.refreshFullscreenVisualizerState(this.player?.activeElement);
if (!overlay.classList.contains('visualizer-active')) {
@ -1659,6 +1785,138 @@ export class UIRenderer {
};
}
setupFullscreenDismissHandle(overlay) {
if (this.fullscreenDismissHandleCleanup) {
this.fullscreenDismissHandleCleanup();
this.fullscreenDismissHandleCleanup = null;
}
const handle = document.getElementById('fullscreen-dismiss-handle');
if (!handle) return;
let activePointerId = null;
let startY = 0;
let startX = 0;
let lastY = 0;
let lastTimestamp = 0;
let velocityY = 0;
let hasDragged = false;
const resetDragState = () => {
activePointerId = null;
hasDragged = false;
overlay.classList.remove('fullscreen-dragging');
overlay.style.removeProperty('--fullscreen-drag-offset');
overlay.style.removeProperty('--fullscreen-drag-progress');
};
const onPointerDown = (event) => {
if (!isMobileFullscreenViewport()) return;
activePointerId = event.pointerId;
startY = event.clientY;
startX = event.clientX;
lastY = event.clientY;
lastTimestamp = event.timeStamp;
velocityY = 0;
hasDragged = false;
overlay.classList.add('fullscreen-dragging');
handle.setPointerCapture(event.pointerId);
};
const onPointerMove = (event) => {
if (event.pointerId !== activePointerId) return;
const deltaY = Math.max(0, event.clientY - startY);
const deltaX = Math.abs(event.clientX - startX);
if (!hasDragged && deltaX > deltaY) {
resetDragState();
return;
}
hasDragged = true;
event.preventDefault();
const elapsed = Math.max(1, event.timeStamp - lastTimestamp);
velocityY = (event.clientY - lastY) / elapsed;
lastY = event.clientY;
lastTimestamp = event.timeStamp;
const progress = Math.min(deltaY / Math.max(window.innerHeight * 0.32, 1), 1);
overlay.style.setProperty('--fullscreen-drag-offset', `${deltaY}px`);
overlay.style.setProperty('--fullscreen-drag-progress', progress.toFixed(3));
};
const onPointerEnd = async (event) => {
if (event.pointerId !== activePointerId) return;
const deltaY = Math.max(0, event.clientY - startY);
const shouldDismiss = hasDragged && (deltaY > 96 || velocityY > 0.55);
if (handle.hasPointerCapture(event.pointerId)) {
handle.releasePointerCapture(event.pointerId);
}
if (shouldDismiss) {
await this.dismissFullscreenCover();
return;
}
resetDragState();
};
const onClick = async (event) => {
if (!isMobileFullscreenViewport() || hasDragged) return;
event.preventDefault();
await this.dismissFullscreenCover();
};
handle.addEventListener('pointerdown', onPointerDown);
handle.addEventListener('pointermove', onPointerMove);
handle.addEventListener('pointerup', onPointerEnd);
handle.addEventListener('pointercancel', onPointerEnd);
handle.addEventListener('click', onClick);
this.fullscreenDismissHandleCleanup = () => {
handle.removeEventListener('pointerdown', onPointerDown);
handle.removeEventListener('pointermove', onPointerMove);
handle.removeEventListener('pointerup', onPointerEnd);
handle.removeEventListener('pointercancel', onPointerEnd);
handle.removeEventListener('click', onClick);
overlay.classList.remove('fullscreen-dragging');
overlay.style.removeProperty('--fullscreen-drag-offset');
overlay.style.removeProperty('--fullscreen-drag-progress');
};
}
setupFullscreenLyricsToggle(overlay) {
if (this.fullscreenLyricsToggleCleanup) {
this.fullscreenLyricsToggleCleanup();
this.fullscreenLyricsToggleCleanup = null;
}
const toggleButtons = [
document.getElementById('toggle-fullscreen-lyrics-btn'),
document.getElementById('toggle-fullscreen-lyrics-mobile-btn'),
].filter(Boolean);
if (toggleButtons.length === 0) return;
const handleToggle = (event) => {
event.preventDefault();
event.stopPropagation();
if (overlay.classList.contains('lyrics-unavailable')) return;
this.fullscreenLyricsVisible = !this.fullscreenLyricsVisible;
this.updateFullscreenLyricsVisibility(overlay);
};
toggleButtons.forEach((toggleBtn) => toggleBtn.addEventListener('click', handleToggle));
this.updateFullscreenLyricsVisibility(overlay);
this.fullscreenLyricsToggleCleanup = () => {
toggleButtons.forEach((toggleBtn) => toggleBtn.removeEventListener('click', handleToggle));
};
}
setupFullscreenControls() {
const playBtn = document.getElementById('fs-play-pause-btn');
const prevBtn = document.getElementById('fs-prev-btn');
@ -1728,16 +1986,7 @@ export class UIRenderer {
if (visualizerBtn) {
visualizerBtn.onclick = async () => {
if (this.fullscreenVisualizerSuppressed) {
this.fullscreenVisualizerSuppressed = false;
visualizerSettings.setEnabled(true);
} else if (visualizerSettings.isEnabled()) {
visualizerSettings.setEnabled(false);
this.fullscreenVisualizerSuppressed = false;
} else {
this.fullscreenVisualizerSuppressed = false;
visualizerSettings.setEnabled(true);
}
this.fullscreenVisualizerSuppressed = !this.fullscreenVisualizerSuppressed;
await this.refreshFullscreenVisualizerState(this.player.activeElement);
};
}
@ -3605,6 +3854,10 @@ export class UIRenderer {
const data = await response.json();
rateCriticsEl.innerHTML = `<a href="${data.url}" target="_blank" style="color: var(--muted-foreground);">Critic Score: <span style="text-decoration: underline;">${data.critic.score}</span>, Based on ${data.critic.count} reviews</a>`;
if (data.critic.score == 'NR') {
rateCriticsEl.innerHTML = `<a style="color: var(--muted-foreground);">Critic Score Not Available Yet</a>`;
}
rateUsersEl.innerHTML = `<a href="${data.url}" target="_blank" style="color: var(--muted-foreground);">User Score: <span style="text-decoration: underline;">${data.user.score}</span>, Based on ${data.user.count} reviews</a>`;
} catch (e) {
rateCriticsEl.innerHTML = `<a style="color: var(--muted-foreground);">Unable to Fetch Critic Score</a>`;

View file

@ -337,4 +337,16 @@ export class Visualizer {
});
}
}
applyPresetOverride(key) {
if (!this.presets?.[key] || this.activePresetKey === key) return;
if (this.activePreset?.destroy) {
this.activePreset.destroy();
}
this._currentContextType = undefined;
this.ctx = null;
this.activePresetKey = key;
}
}

27
lhci.yml Normal file
View file

@ -0,0 +1,27 @@
target: static
assert:
assertions:
# Performance
- first-contentful-paint:warn < 3000
- interactive:warn < 7000
- lcp-lazy-loaded:off
- speed-index:warn < 5000
# Accessibility (warn if below 85)
- accessibility:warn < 85
# Best Practices
- best-practices:warn < 85
# SEO
- seo:warn < 85
upload:
target: temporary-public-storage
collect:
numberOfRuns: 3
settings:
preset: desktop
# Headless Chrome for CI
chromeFlags: '--no-sandbox --disable-gpu'

View file

@ -17,7 +17,9 @@
"lint:css": "stylelint \"**/*.css\"",
"lint:html": "htmlhint \"**/*.html\" --ignore=\"dist/**,legacy/**,node_modules/**\"",
"lint": "bun run lint:js && bun run lint:css && bun run lint:html",
"format": "prettier --write ."
"format": "prettier --write .",
"lhci": "lhci",
"lhci:autorun": "npm run build && lhci autorun"
},
"repository": {
"type": "git",
@ -33,6 +35,7 @@
"devDependencies": {
"@capacitor/assets": "^3.0.5",
"@capacitor/cli": "^8.2.0",
"@lhci/cli": "^0.14.0",
"@testing-library/dom": "^10.4.1",
"@types/node": "^25.3.5",
"@vitest/browser-playwright": "^4.1.2",
@ -47,6 +50,7 @@
"stylelint": "^17.6.0",
"stylelint-config-standard": "^39.0.1",
"stylelint-config-standard-scss": "^16.0.0",
"terser": "^5.46.1",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-bundle-visualizer": "^1.2.1",

View file

@ -1,274 +1 @@
[
{
"type": "album",
"id": 324660713,
"title": "JOECHILLWORLD",
"artist": {
"id": 40978758,
"name": "Devon Hendryx"
},
"releaseDate": "2010-07-10",
"cover": "25d45544-3e82-4184-b8c2-2c2c6f0f152a",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS", "HIRES_LOSSLESS"]
}
},
{
"type": "album",
"id": 15427733,
"title": "Mysterious Phonk: The Chronicles of SpaceGhostPurrp",
"artist": {
"id": 4611745,
"name": "Spaceghostpurrp"
},
"releaseDate": "2012-06-12",
"cover": "c78b7543-1cd8-4921-9155-e81d421353a0",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS"]
}
},
{
"type": "album",
"id": 464178301,
"title": "Never Forget",
"artist": {
"id": 5516508,
"name": "Chris Travis"
},
"releaseDate": "2014-05-14",
"cover": "4ab11f0d-0768-4cce-8de5-1894134d5994",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS"]
}
},
{
"type": "album",
"id": 75115890,
"title": "Blood Shore Season 2",
"artist": {
"id": 6332342,
"name": "Xavier Wulf"
},
"releaseDate": "2014-10-30",
"cover": "517303e5-d541-4704-b552-026427e05fcb",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS"]
}
},
{
"type": "album",
"id": 410197513,
"title": "THE PEAK",
"artist": {
"id": 33481052,
"name": "smokedope2016"
},
"releaseDate": "2025-01-17",
"cover": "ea18084d-36ec-4cea-98a7-fe4684246986",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS"]
}
},
{
"type": "album",
"id": 418729278,
"title": "I LAY DOWN MY LIFE FOR YOU: DIRECTOR'S CUT",
"artist": {
"id": 7958797,
"name": "JPEGMAFIA"
},
"releaseDate": "2025-02-03",
"cover": "9c84302b-2584-4c0a-9db7-e648542f459f",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS", "HIRES_LOSSLESS"]
}
},
{
"type": "album",
"id": 504004321,
"title": "Half Blood (BloodLuxe)",
"artist": {
"id": 50799233,
"name": "slayr"
},
"releaseDate": "2025-11-05",
"cover": "2767cc63-7e92-4a48-aa4b-806a3ea7ec1c",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS", "HIRES_LOSSLESS"]
}
},
{
"type": "album",
"id": 510893864,
"title": "BULLY",
"artist": {
"id": 25022,
"name": "Kanye West"
},
"releaseDate": "2026-03-28",
"cover": "cf2f2c9c-ff67-44f6-83aa-a7622f8c6b64",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS", "HIRES_LOSSLESS"]
}
},
{
"type": "album",
"id": 325723583,
"title": "Replica",
"artist": {
"id": 3715530,
"name": "Oneohtrix Point Never"
},
"releaseDate": "2011-11-05",
"cover": "95ceeae9-cac7-42dc-ae37-7c93c223f809",
"explicit": false,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS"]
}
},
{
"type": "album",
"id": 336178142,
"title": "Pirate This Album",
"artist": {
"id": 8622751,
"name": "Shamana"
},
"releaseDate": "2023-12-25",
"cover": "a8a647be-0331-4779-9a6e-31645a9abdab",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS", "HIRES_LOSSLESS"]
}
},
{
"type": "album",
"id": 106369871,
"title": "Organic Thoughts from the Synthetic Mind",
"artist": {
"id": 6436013,
"name": "Shinjuku Mad"
},
"releaseDate": "2009-07-01",
"cover": "3acc888e-35da-40a8-a4b7-7ffd00576cc9",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS"]
}
},
{
"type": "album",
"id": 423471869,
"title": "pain",
"artist": {
"id": 44257324,
"name": "bleood"
},
"releaseDate": "2025-03-11",
"cover": "711b23ba-c473-44e6-a2f0-010fefa9c5b8",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS"]
}
},
{
"type": "album",
"id": 250986538,
"title": "Revolutionary, Vol. 1 (Bonus Edition)",
"artist": {
"id": 3604583,
"name": "Immortal Technique"
},
"releaseDate": "2001-09-14",
"cover": "e510dd6d-dcdf-4272-9c68-f4580f2fbd14",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS"]
}
},
{
"type": "album",
"id": 509761344,
"title": "EMOTIONS",
"artist": {
"id": 49124576,
"name": "Nine Vicious"
},
"releaseDate": "2026-04-03",
"cover": "f29b18d3-b19f-45b1-968a-0ad360647130",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS"]
}
},
{
"type": "album",
"id": 15621057,
"title": "Triple F Life: Friends, Fans & Family (Deluxe Version)",
"artist": {
"id": 3654061,
"name": "Waka Flocka Flame"
},
"releaseDate": "2012-06-12",
"cover": "3199b7de-5e3d-486c-acf1-870ff4c60572",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS"]
}
},
{
"type": "album",
"id": 103897783,
"title": "Freewave 3",
"artist": {
"id": 7923685,
"name": "Lucki"
},
"releaseDate": "2019-02-15",
"cover": "1d481a33-8b20-4ee3-b04b-5ac6e0fc5e78",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS"]
}
},
{
"type": "album",
"id": 151728406,
"title": "Niagara",
"artist": {
"id": 7607680,
"name": "redveil"
},
"releaseDate": "2020-08-25",
"cover": "14690142-7fc8-4557-8a61-0721b7884822",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS"]
}
}
]
[]

View file

@ -1,4 +1,19 @@
[
{
"file": "2026-4-5.json",
"label": "Spring 2026",
"date": "2026-04-05"
},
{
"file": "2026-4-5.json",
"label": "Spring 2026",
"date": "2026-04-05"
},
{
"file": "2026-4-5.json",
"label": "Spring 2026",
"date": "2026-04-05"
},
{
"file": "2026-4-5.json",
"label": "Spring 2026",

View file

@ -8,7 +8,7 @@
"name": "Devon Hendryx"
},
"releaseDate": "2010-07-10",
"cover": "https://monochrome.tf/editors-picks-images/25d45544-3e82-4184-b8c2-2c2c6f0f152a.webp",
"cover": "25d45544-3e82-4184-b8c2-2c2c6f0f152a",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -24,7 +24,7 @@
"name": "Spaceghostpurrp"
},
"releaseDate": "2012-06-12",
"cover": "https://monochrome.tf/editors-picks-images/c78b7543-1cd8-4921-9155-e81d421353a0.webp",
"cover": "c78b7543-1cd8-4921-9155-e81d421353a0",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -40,7 +40,7 @@
"name": "Chris Travis"
},
"releaseDate": "2014-05-14",
"cover": "https://monochrome.tf/editors-picks-images/4ab11f0d-0768-4cce-8de5-1894134d5994.webp",
"cover": "4ab11f0d-0768-4cce-8de5-1894134d5994",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -56,7 +56,7 @@
"name": "Xavier Wulf"
},
"releaseDate": "2014-10-30",
"cover": "https://monochrome.tf/editors-picks-images/517303e5-d541-4704-b552-026427e05fcb.webp",
"cover": "517303e5-d541-4704-b552-026427e05fcb",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -72,7 +72,7 @@
"name": "smokedope2016"
},
"releaseDate": "2025-01-17",
"cover": "https://monochrome.tf/editors-picks-images/ea18084d-36ec-4cea-98a7-fe4684246986.webp",
"cover": "ea18084d-36ec-4cea-98a7-fe4684246986",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -88,7 +88,7 @@
"name": "JPEGMAFIA"
},
"releaseDate": "2025-02-03",
"cover": "https://monochrome.tf/editors-picks-images/9c84302b-2584-4c0a-9db7-e648542f459f.webp",
"cover": "9c84302b-2584-4c0a-9db7-e648542f459f",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -104,7 +104,7 @@
"name": "slayr"
},
"releaseDate": "2025-11-05",
"cover": "https://monochrome.tf/editors-picks-images/2767cc63-7e92-4a48-aa4b-806a3ea7ec1c.webp",
"cover": "2767cc63-7e92-4a48-aa4b-806a3ea7ec1c",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -120,7 +120,7 @@
"name": "Kanye West"
},
"releaseDate": "2026-03-28",
"cover": "https://monochrome.tf/editors-picks-images/cf2f2c9c-ff67-44f6-83aa-a7622f8c6b64.webp",
"cover": "cf2f2c9c-ff67-44f6-83aa-a7622f8c6b64",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -136,7 +136,7 @@
"name": "Oneohtrix Point Never"
},
"releaseDate": "2011-11-05",
"cover": "https://monochrome.tf/editors-picks-images/95ceeae9-cac7-42dc-ae37-7c93c223f809.webp",
"cover": "95ceeae9-cac7-42dc-ae37-7c93c223f809",
"explicit": false,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -152,7 +152,7 @@
"name": "Shamana"
},
"releaseDate": "2023-12-25",
"cover": "https://monochrome.tf/editors-picks-images/a8a647be-0331-4779-9a6e-31645a9abdab.webp",
"cover": "a8a647be-0331-4779-9a6e-31645a9abdab",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -168,7 +168,7 @@
"name": "Shinjuku Mad"
},
"releaseDate": "2009-07-01",
"cover": "https://monochrome.tf/editors-picks-images/3acc888e-35da-40a8-a4b7-7ffd00576cc9.webp",
"cover": "3acc888e-35da-40a8-a4b7-7ffd00576cc9",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -184,7 +184,7 @@
"name": "bleood"
},
"releaseDate": "2025-03-11",
"cover": "https://monochrome.tf/editors-picks-images/711b23ba-c473-44e6-a2f0-010fefa9c5b8.webp",
"cover": "711b23ba-c473-44e6-a2f0-010fefa9c5b8",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -200,7 +200,7 @@
"name": "Immortal Technique"
},
"releaseDate": "2001-09-14",
"cover": "https://monochrome.tf/editors-picks-images/e510dd6d-dcdf-4272-9c68-f4580f2fbd14.webp",
"cover": "e510dd6d-dcdf-4272-9c68-f4580f2fbd14",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -216,7 +216,7 @@
"name": "Nine Vicious"
},
"releaseDate": "2026-04-03",
"cover": "https://monochrome.tf/editors-picks-images/f29b18d3-b19f-45b1-968a-0ad360647130.webp",
"cover": "f29b18d3-b19f-45b1-968a-0ad360647130",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -232,7 +232,7 @@
"name": "Waka Flocka Flame"
},
"releaseDate": "2012-06-12",
"cover": "https://monochrome.tf/editors-picks-images/3199b7de-5e3d-486c-acf1-870ff4c60572.webp",
"cover": "3199b7de-5e3d-486c-acf1-870ff4c60572",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -248,7 +248,7 @@
"name": "Lucki"
},
"releaseDate": "2019-02-15",
"cover": "https://monochrome.tf/editors-picks-images/1d481a33-8b20-4ee3-b04b-5ac6e0fc5e78.webp",
"cover": "1d481a33-8b20-4ee3-b04b-5ac6e0fc5e78",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
@ -264,11 +264,59 @@
"name": "redveil"
},
"releaseDate": "2020-08-25",
"cover": "https://monochrome.tf/editors-picks-images/14690142-7fc8-4557-8a61-0721b7884822.webp",
"cover": "14690142-7fc8-4557-8a61-0721b7884822",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS"]
}
},
{
"type": "album",
"id": 199412873,
"title": "Tha Carter III",
"artist": {
"id": 27518,
"name": "Lil Wayne"
},
"releaseDate": "2008-06-10",
"cover": "797a90ea-3860-4d02-ac85-39b34ca8ee25",
"explicit": true,
"audioQuality": "LOW",
"mediaMetadata": {
"tags": ["DOLBY_ATMOS"]
}
},
{
"type": "album",
"id": 3280432,
"title": "We Are Young Money",
"artist": {
"id": 3654487,
"name": "Young Money"
},
"releaseDate": "2009-12-21",
"cover": "5b1456e5-1bba-415b-8276-8bc9cd211687",
"explicit": true,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS"]
}
},
{
"type": "album",
"id": 37927851,
"title": "The DeAndre Way (Deluxe)",
"artist": {
"id": 3820209,
"name": "Soulja Boy"
},
"releaseDate": "2010-11-30",
"cover": "6ca0217d-4f74-47d2-b449-30144d91e41f",
"explicit": false,
"audioQuality": "LOSSLESS",
"mediaMetadata": {
"tags": ["LOSSLESS"]
}
}
]

View file

@ -3575,7 +3575,7 @@ input:checked + .slider::before {
}
.player-controls .progress-container span {
min-width: 45px;
min-width: 40px;
font-variant-numeric: tabular-nums;
flex-shrink: 0;
}
@ -3920,29 +3920,25 @@ input:checked + .slider::before {
justify-content: center;
animation: fade-in 0.3s ease;
overflow: hidden;
background-color: var(--background);
/* Use a CSS variable for the image so we can set it in JS */
--bg-image: none;
background-color: rgb(11 13 17);
/* Reserve space above taskbar / system UI so volume controls stay visible (fixes #322) */
padding-bottom: max(env(safe-area-inset-bottom), 1.5rem);
--fullscreen-drag-progress: 0;
--fs-accent-rgb: var(--highlight-rgb);
}
#fullscreen-cover-overlay::before {
content: '';
position: absolute;
inset: -20px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
filter: var(--cover-filter);
inset: 0;
background:
radial-gradient(circle at 50% 50%, rgb(255 255 255 / 0.035), transparent 58%),
linear-gradient(180deg, rgb(6 8 12 / 0.12), rgb(6 8 12 / 0.34));
z-index: -1;
background-image: var(--bg-image);
transition:
background-image var(--transition),
filter 0.65s ease,
opacity 0.65s ease;
transition: opacity 0.65s ease;
opacity: calc(1 - (var(--fullscreen-drag-progress, 0) * 0.32));
}
#fullscreen-cover-overlay::after {
@ -3950,10 +3946,10 @@ input:checked + .slider::before {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 20% 22%, rgb(var(--highlight-rgb) / 0.28), transparent 36%),
radial-gradient(circle at 20% 22%, rgb(var(--fs-accent-rgb) / 0.28), transparent 36%),
radial-gradient(circle at 82% 18%, rgb(255 255 255 / 0.09), transparent 28%),
linear-gradient(135deg, rgb(10 13 18 / 0.48), rgb(10 13 18 / 0.2) 38%, rgb(var(--highlight-rgb) / 0.12) 100%);
opacity: 0.36;
linear-gradient(135deg, rgb(10 13 18 / 0.48), rgb(10 13 18 / 0.2) 38%, rgb(var(--fs-accent-rgb) / 0.12) 100%);
opacity: calc(0.36 - (var(--fullscreen-drag-progress, 0) * 0.26));
pointer-events: none;
z-index: 0;
transition:
@ -3968,9 +3964,9 @@ input:checked + .slider::before {
height: 100%;
z-index: 0;
pointer-events: none;
filter: blur(14px) saturate(0.84) brightness(0.8);
transform: scale(1.04);
opacity: 0.82;
filter: blur(8px) saturate(0.9) brightness(0.8);
transform: scale(1.03);
opacity: 0.8;
transition:
opacity 0.65s ease,
filter 0.65s ease,
@ -3991,9 +3987,57 @@ input:checked + .slider::before {
justify-content: center;
width: 100%;
height: 100%;
max-width: 100%;
min-width: 0;
box-sizing: border-box;
position: relative;
padding: 1rem;
overflow: hidden;
transform: translateY(var(--fullscreen-drag-offset, 0));
opacity: calc(1 - (var(--fullscreen-drag-progress, 0) * 0.16));
transition:
transform 0.26s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.22s ease;
will-change: transform, opacity;
}
#fullscreen-dismiss-handle {
position: absolute;
top: calc(0.75rem + env(safe-area-inset-top));
left: 50%;
width: 3.25rem;
height: 1rem;
border: 0;
padding: 0;
margin: 0;
background: transparent;
transform: translateX(-50%);
z-index: 14;
display: none;
cursor: grab;
touch-action: none;
}
#fullscreen-dismiss-handle::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 3rem;
height: 0.3rem;
border-radius: 999px;
transform: translate(-50%, -50%);
background: rgb(255 255 255 / 0.28);
box-shadow: 0 2px 12px rgb(0 0 0 / 0.25);
}
#fullscreen-cover-overlay.fullscreen-dragging .fullscreen-cover-content {
transition: none;
}
#fullscreen-cover-overlay.fullscreen-dismissing .fullscreen-cover-content {
transform: translateY(calc(100% + 3rem));
opacity: 0;
}
/* UI Toggle Button for Visualizer Mode - Rightmost position */
@ -4071,6 +4115,38 @@ input:checked + .slider::before {
background: var(--primary);
}
#toggle-fullscreen-lyrics-mobile-btn {
display: none;
position: absolute;
top: calc(0.85rem + env(safe-area-inset-top));
right: calc(0.9rem + env(safe-area-inset-right));
width: 38px;
height: 38px;
border: none;
border-radius: 999px;
padding: 0;
align-items: center;
justify-content: center;
background: rgb(9 12 18 / 0.32);
color: rgb(255 255 255 / 0.76);
backdrop-filter: blur(10px);
z-index: 14;
transition:
background-color 0.2s ease,
color 0.2s ease,
opacity 0.2s ease,
transform 0.2s ease;
}
#toggle-fullscreen-lyrics-mobile-btn.active {
background: rgb(255 255 255 / 0.12);
color: rgb(255 255 255 / 0.96);
}
#toggle-fullscreen-lyrics-mobile-btn:hover {
transform: scale(1.04);
}
/* Close Button - Leftmost position */
#close-fullscreen-cover-btn {
position: absolute;
@ -7817,148 +7893,6 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
transform: scale(0.97);
}
/* ========================================
16-Band Graphic Equalizer (Legacy EQ)
======================================== */
.graphic-eq-section {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.graphic-eq-preset-row {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.graphic-eq-preset-label {
font-size: 0.8rem;
font-weight: 600;
color: var(--foreground);
white-space: nowrap;
}
.graphic-eq-preset-select {
flex: 1;
padding: 8px 12px;
background: var(--input);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--foreground);
font-size: 0.85rem;
}
.graphic-eq-bands {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 2px;
padding: var(--spacing-md) var(--spacing-sm);
background: rgb(0, 0, 0, 0.15);
border-radius: var(--radius);
min-height: 240px;
}
.graphic-eq-band {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
flex: 1;
min-width: 0;
}
.graphic-eq-band-value {
font-size: 0.65rem;
color: var(--foreground);
font-variant-numeric: tabular-nums;
white-space: nowrap;
min-height: 14px;
opacity: 0.7;
}
.graphic-eq-band-slider-wrap {
position: relative;
height: 160px;
width: 28px;
display: flex;
align-items: center;
justify-content: center;
}
.graphic-eq-band-slider-wrap input[type='range'] {
writing-mode: vertical-lr;
direction: rtl;
width: 28px;
height: 100%;
accent-color: var(--foreground);
cursor: pointer;
margin: 0;
padding: 0;
}
.graphic-eq-band-label {
font-size: 0.6rem;
color: var(--muted-foreground);
white-space: nowrap;
text-align: center;
letter-spacing: -0.02em;
}
.graphic-eq-bottom-row {
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.graphic-eq-preamp {
display: flex;
align-items: center;
gap: var(--spacing-sm);
flex: 1;
}
.graphic-eq-preamp-label {
font-size: 0.75rem;
font-weight: 600;
color: var(--muted-foreground);
white-space: nowrap;
}
.graphic-eq-preamp-slider {
flex: 1;
height: 4px;
accent-color: var(--highlight);
}
.graphic-eq-preamp-value {
font-size: 0.75rem;
color: var(--muted-foreground);
min-width: 45px;
text-align: right;
font-variant-numeric: tabular-nums;
}
@media (max-width: 600px) {
.graphic-eq-bands {
min-height: 180px;
}
.graphic-eq-band-slider-wrap {
height: 130px;
width: 22px;
}
.graphic-eq-band-label {
font-size: 0.5rem;
}
.graphic-eq-band-value {
font-size: 0.5rem;
}
}
/* ========================================
Precision AutoEQ - Redesigned Equalizer
======================================== */
@ -8579,19 +8513,6 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
align-items: center;
justify-content: space-between;
padding: var(--spacing-md);
cursor: pointer;
user-select: none;
transition: background var(--transition-fast);
}
.autoeq-database-header:hover {
background: rgb(var(--highlight-rgb), 0.08);
}
.autoeq-database-header-right {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.autoeq-database-title {
@ -9311,69 +9232,6 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
flex: 1;
width: auto;
}
/* Mobile parametric EQ band layout */
.autoeq-band-control {
padding: 0.5rem;
}
.autoeq-band-header {
flex-wrap: wrap;
gap: 0.3rem;
}
.autoeq-band-number {
min-width: 1.2rem;
}
.autoeq-band-param {
min-width: 0;
flex: 0 0 auto;
}
.autoeq-band-sliders {
flex-direction: column;
gap: 0.5rem;
}
.autoeq-band-slider {
width: 100%;
height: 6px;
}
.autoeq-band-slider::-webkit-slider-thumb {
width: 18px;
height: 18px;
}
.autoeq-band-slider::-moz-range-thumb {
width: 18px;
height: 18px;
}
.autoeq-filters-actions {
flex-wrap: wrap;
gap: 0.3rem;
}
.autoeq-filters-actions button {
font-size: 0.7rem;
padding: 0.3rem 0.5rem;
}
}
@media (max-width: 600px) {
/* Rearrange band header into 2 rows on small screens */
.autoeq-band-header {
display: grid;
grid-template-columns: auto auto 1fr 1fr 1fr;
gap: 0.25rem 0.4rem;
align-items: center;
}
.autoeq-band-param {
justify-content: flex-start;
}
}
/* Track List Search */
@ -10229,35 +10087,12 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
}
.about-contributors div {
flex: 1 1 calc(25% - 10px);
min-width: 150px;
max-width: calc(50% - 10px);
width: calc(20% - 8px);
border: 1px solid var(--border);
border-radius: 14px;
padding: 20px;
padding: 30px;
text-align: center;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
@media (max-width: 768px) {
.about-contributors div {
flex: 1 1 calc(50% - 10px);
max-width: 100%;
padding: 15px;
}
}
.about-contributors a {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
text-decoration: none;
color: inherit;
}
.about-contributors img {
@ -10281,24 +10116,34 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
display: flex;
align-items: stretch;
justify-content: center;
max-width: 100%;
min-width: 0;
box-sizing: border-box;
min-height: 0;
overflow: hidden;
}
#fullscreen-cover-overlay .fullscreen-main-view {
width: min(1240px, 100%);
--fs-media-column-size: minmax(340px, 430px);
--fs-lyrics-column-size: minmax(520px, 760px);
width: min(1480px, 100%);
height: 100%;
flex: 1;
display: grid;
grid-template-columns: minmax(360px, 430px) minmax(420px, 1fr);
gap: clamp(1.5rem, 3vw, 3rem);
grid-template-columns: var(--fs-media-column-size) var(--fs-lyrics-column-size);
gap: clamp(2rem, 4vw, 4.5rem);
align-items: center;
justify-content: center;
padding: clamp(4rem, 7vh, 5rem) clamp(2rem, 4vw, 3rem) clamp(3rem, 6vh, 4rem) clamp(4rem, 7vw, 6.25rem);
padding: clamp(4rem, 7vh, 5rem) clamp(3rem, 6vw, 5rem) clamp(3rem, 6vh, 4rem);
position: relative;
z-index: 1;
min-height: 0;
overflow: hidden;
transition:
grid-template-columns 0.34s cubic-bezier(0.22, 1, 0.36, 1),
width 0.34s cubic-bezier(0.22, 1, 0.36, 1),
gap 0.34s cubic-bezier(0.22, 1, 0.36, 1);
}
#fullscreen-cover-overlay .fullscreen-media-column,
@ -10312,7 +10157,11 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
flex-direction: column;
gap: 0.95rem;
justify-self: center;
transform: translateX(clamp(0.75rem, 1.2vw, 1.4rem));
transform: none;
transition:
width 0.34s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.34s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.24s ease;
}
#fullscreen-cover-overlay .fullscreen-artwork-card {
@ -10365,7 +10214,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
#fullscreen-cover-overlay #toggle-fullscreen-lyrics-btn,
#fullscreen-cover-overlay .fullscreen-lyrics-toggle {
display: none !important;
display: flex;
}
#fullscreen-cover-overlay .fullscreen-actions {
@ -10443,9 +10292,10 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
}
#fullscreen-cover-overlay .fullscreen-top-actions #fs-visualizer-btn {
order: 2;
order: 3;
}
#fullscreen-cover-overlay .fullscreen-top-actions #toggle-fullscreen-lyrics-btn,
#fullscreen-cover-overlay .fullscreen-top-actions #fs-visualizer-btn,
#fullscreen-cover-overlay .fullscreen-top-actions #close-fullscreen-cover-btn {
position: static;
@ -10457,6 +10307,15 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
opacity: 1;
}
#fullscreen-cover-overlay .fullscreen-top-actions #toggle-fullscreen-lyrics-btn {
order: 2;
}
#fullscreen-cover-overlay .fullscreen-top-actions #toggle-fullscreen-lyrics-btn.active {
color: rgb(255 255 255 / 0.96);
background: rgb(255 255 255 / 0.12);
}
#fullscreen-cover-overlay .fullscreen-top-actions #close-fullscreen-cover-btn {
order: 1;
}
@ -10477,7 +10336,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
#fullscreen-cover-overlay #toggle-ui-btn {
top: 1.25rem;
left: calc(80px + 2.3rem + env(safe-area-inset-left));
left: calc(9.9rem + env(safe-area-inset-left));
right: auto;
width: 40px;
height: 40px;
@ -10522,7 +10381,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
}
#fullscreen-cover-overlay .fullscreen-buttons button.active {
color: rgb(var(--highlight-rgb) / 0.98);
color: rgb(var(--fs-accent-rgb) / 0.98);
}
#fullscreen-cover-overlay .fullscreen-buttons #fs-play-pause-btn {
@ -10563,7 +10422,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
}
#fullscreen-cover-overlay .fs-visualizer-btn.active {
color: rgb(var(--highlight-rgb) / 0.96);
color: rgb(var(--fs-accent-rgb) / 0.96);
}
#fullscreen-cover-overlay .fs-volume-btn {
@ -10609,7 +10468,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
#fullscreen-cover-overlay .fullscreen-progress-container .progress-bar:hover .progress-fill,
#fullscreen-cover-overlay .fs-volume-bar:hover .fs-volume-fill {
background: rgb(var(--highlight-rgb) / 0.94);
background: rgb(var(--fs-accent-rgb) / 0.94);
}
#fullscreen-cover-overlay .fullscreen-progress-container .progress-bar:hover .progress-fill::after,
@ -10626,6 +10485,13 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
align-items: stretch;
justify-content: flex-start;
overflow: hidden;
min-width: 0;
opacity: 1;
transform: translateX(0);
transition:
opacity 0.24s ease,
transform 0.34s cubic-bezier(0.22, 1, 0.36, 1),
visibility 0s linear 0s;
}
#fullscreen-cover-overlay .fullscreen-lyrics-shell,
@ -10641,14 +10507,14 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
#fullscreen-cover-overlay .fullscreen-lyrics-shell {
width: min(860px, 100%);
min-height: 0;
margin-left: clamp(4rem, 8vw, 8rem);
margin-left: 0;
}
#fullscreen-cover-overlay .fullscreen-lyrics-content {
min-height: 0;
height: 100%;
position: relative;
padding-left: clamp(2.5rem, 4vw, 4rem);
padding-left: clamp(0.5rem, 1.6vw, 1.5rem);
mask-image: none;
overflow: visible;
scrollbar-width: none;
@ -10696,6 +10562,35 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
opacity: 0.55;
}
#fullscreen-cover-overlay.lyrics-hidden .fullscreen-main-view {
--fs-media-column-size: minmax(420px, 760px);
--fs-lyrics-column-size: minmax(0, 0fr);
width: min(760px, 100%);
gap: 0;
}
#fullscreen-cover-overlay.lyrics-hidden .fullscreen-media-column {
justify-self: center;
width: min(520px, 100%);
transform: translateX(clamp(2rem, 4vw, 3.5rem));
}
#fullscreen-cover-overlay.lyrics-hidden .fullscreen-lyrics-pane {
opacity: 0;
transform: translateX(2rem);
visibility: hidden;
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
#fullscreen-cover-overlay .fullscreen-main-view,
#fullscreen-cover-overlay .fullscreen-media-column,
#fullscreen-cover-overlay .fullscreen-lyrics-pane {
transition: none !important;
}
}
#fullscreen-cover-overlay.queue-panel-active .fullscreen-main-view {
grid-template-columns: 1fr;
width: min(760px, 100%);
@ -10712,101 +10607,330 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
@media (max-width: 980px) {
#fullscreen-cover-overlay .fullscreen-main-view {
grid-template-columns: 1fr;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto minmax(0, 1fr) auto;
width: min(760px, 100%);
gap: 1rem;
align-items: start;
padding: calc(4.5rem + env(safe-area-inset-top)) clamp(1rem, 4vw, 1.5rem)
gap: 1.25rem;
align-items: stretch;
padding: calc(5rem + env(safe-area-inset-top)) clamp(1rem, 4vw, 1.75rem)
calc(1.5rem + env(safe-area-inset-bottom));
}
#fullscreen-cover-overlay .fullscreen-media-column {
justify-self: center;
transform: none;
width: min(100%, 620px);
}
#fullscreen-cover-overlay .fullscreen-lyrics-pane {
display: none;
display: flex;
width: min(100%, 620px);
justify-self: center;
min-height: min(48vh, 440px);
}
#fullscreen-cover-overlay .fullscreen-lyrics-shell {
width: 100%;
margin-left: 0;
}
#fullscreen-cover-overlay .fullscreen-lyrics-content {
padding-left: 0;
}
}
@media (max-width: 768px) {
#fullscreen-cover-overlay {
--fs-mobile-top-btn-size: 44px;
--fs-mobile-top-btn-size: 42px;
--fs-mobile-top-btn-gap: 0.6rem;
--fs-mobile-top-btn-left: calc(1rem + env(safe-area-inset-left));
}
#fullscreen-cover-overlay .fullscreen-cover-content {
padding: 0.75rem 0.75rem calc(0.75rem + env(safe-area-inset-bottom));
padding: 0 calc(0.9rem + env(safe-area-inset-right)) calc(0.9rem + env(safe-area-inset-bottom))
calc(0.9rem + env(safe-area-inset-left));
}
#fullscreen-dismiss-handle {
display: block;
}
#fullscreen-cover-overlay .fullscreen-top-actions {
top: calc(0.75rem + env(safe-area-inset-top));
left: var(--fs-mobile-top-btn-left);
gap: var(--fs-mobile-top-btn-gap);
display: none;
}
#fullscreen-cover-overlay .fullscreen-top-actions button,
#fullscreen-cover-overlay .fullscreen-lyrics-toggle,
#fullscreen-cover-overlay #toggle-ui-btn {
width: var(--fs-mobile-top-btn-size);
height: var(--fs-mobile-top-btn-size);
background: rgb(9 12 18 / 0.5);
display: none !important;
}
#fullscreen-cover-overlay #toggle-ui-btn {
top: calc(0.75rem + env(safe-area-inset-top));
left: calc(
var(--fs-mobile-top-btn-left) + (var(--fs-mobile-top-btn-size) * 2) + (var(--fs-mobile-top-btn-gap) * 2)
);
#toggle-fullscreen-lyrics-mobile-btn {
display: flex;
}
#fullscreen-cover-overlay .fullscreen-main-view {
width: 100%;
gap: 0.85rem;
padding: calc(7.25rem + env(safe-area-inset-top)) 0.75rem calc(1.5rem + env(safe-area-inset-bottom));
height: 100%;
max-width: 100%;
min-width: 0;
min-height: 0;
box-sizing: border-box;
grid-template-columns: minmax(78px, 92px) minmax(0, 1fr);
grid-template-rows: auto minmax(0, 1fr) auto;
grid-template-areas:
'art info'
'lyrics lyrics'
'controls controls';
gap: 1rem 0.9rem;
padding: calc(4.45rem + env(safe-area-inset-top)) 0 calc(0.8rem + env(safe-area-inset-bottom));
}
#fullscreen-cover-overlay .fullscreen-track-info,
#fullscreen-cover-overlay .fullscreen-controls,
#fullscreen-cover-overlay .fullscreen-media-column {
width: min(100%, 460px);
display: contents;
width: auto;
min-width: 0;
}
#fullscreen-cover-overlay .fullscreen-artwork-card {
grid-area: art;
width: 100%;
max-width: 92px;
box-sizing: border-box;
border-radius: 12px;
align-self: start;
margin-left: 0.95rem;
box-shadow: 0 20px 48px rgb(0 0 0 / 0.34);
}
#fullscreen-cover-overlay #fullscreen-cover-image {
border-radius: 12px;
}
#fullscreen-cover-overlay .fullscreen-track-info {
grid-area: info;
width: 100%;
min-width: 0;
align-self: center;
display: grid;
gap: 0.3rem;
padding-top: 0.2rem;
padding-left: 0.95rem;
}
#fullscreen-cover-overlay .fullscreen-track-text {
display: grid;
gap: 0.14rem;
}
#fullscreen-cover-overlay #fullscreen-track-title {
font-size: clamp(1.1rem, 4.7vw, 1.34rem);
line-height: 1.04;
}
#fullscreen-cover-overlay #fullscreen-track-artist {
margin-top: 0;
font-size: 0.92rem;
color: rgb(255 255 255 / 0.7);
}
#fullscreen-cover-overlay .fullscreen-actions {
display: none !important;
}
#fullscreen-cover-overlay #fullscreen-next-track {
display: none !important;
}
#fullscreen-cover-overlay .fullscreen-lyrics-pane {
grid-area: lyrics;
width: 100%;
flex-wrap: wrap;
gap: 0.45rem;
max-width: 100%;
min-height: 0;
min-width: 0;
box-sizing: border-box;
justify-self: stretch;
}
#fullscreen-cover-overlay .fullscreen-actions .btn-icon {
background: rgb(255 255 255 / 0.06);
#fullscreen-cover-overlay .fullscreen-lyrics-shell {
min-height: 0;
position: relative;
background: transparent !important;
box-shadow: none !important;
border-radius: 0;
overflow: visible;
}
#fullscreen-cover-overlay .fullscreen-progress-container {
gap: 0.65rem;
#fullscreen-cover-overlay .fullscreen-lyrics-content {
height: 100%;
padding: 0 0 0.2rem;
overflow: hidden;
mask-image: linear-gradient(180deg, transparent 0%, black 10%, black 88%, transparent 100%);
}
#fullscreen-cover-overlay .fullscreen-buttons {
gap: 0.2rem;
#fullscreen-cover-overlay .fullscreen-lyrics-content am-lyrics {
--lyrics-scroll-padding-top: 18%;
--lyplus-font-size-base: clamp(1.75rem, 7vw, 2.35rem);
--lyplus-padding-line: 6px;
--lyplus-text-color: rgb(246, 244, 239, 0.16);
--lyplus-blur-amount: 0.16em;
--lyplus-blur-amount-near: 0.08em;
line-height: 1.2;
}
#fullscreen-cover-overlay .fullscreen-buttons button {
width: 38px;
height: 38px;
#fullscreen-cover-overlay .fullscreen-lyrics-empty,
#fullscreen-cover-overlay .fullscreen-lyrics-content .lyrics-loading,
#fullscreen-cover-overlay .fullscreen-lyrics-content .lyrics-error {
padding: 2.5rem 1.2rem 0;
}
#fullscreen-cover-overlay .fullscreen-buttons #fs-play-pause-btn {
width: 52px;
height: 52px;
#fullscreen-cover-overlay .fullscreen-controls {
grid-area: controls;
width: 100%;
max-width: none;
min-width: 0;
box-sizing: border-box;
margin-top: 0;
padding: 0.15rem 0 0;
gap: 0.9rem;
}
#fullscreen-cover-overlay .fullscreen-mobile-quality {
display: none;
}
#fullscreen-cover-overlay .fullscreen-volume-container {
width: min(220px, calc(100% - 2.75rem));
display: none;
}
#fullscreen-cover-overlay .fullscreen-progress-container {
gap: 0.55rem;
font-size: 0.72rem;
}
#fullscreen-cover-overlay .fullscreen-buttons {
gap: 0.1rem;
justify-content: space-between;
}
#fullscreen-cover-overlay .fullscreen-buttons button {
width: 42px;
height: 42px;
}
#fullscreen-cover-overlay .fullscreen-buttons #fs-play-pause-btn {
width: 62px;
height: 62px;
box-shadow: 0 14px 28px rgb(0 0 0 / 0.3);
}
#fullscreen-cover-overlay.lyrics-hidden .fullscreen-main-view {
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr) auto minmax(0, 1fr) auto;
align-items: center;
justify-items: center;
gap: 0;
padding: calc(4.45rem + env(safe-area-inset-top)) clamp(1rem, 4vw, 1.4rem)
calc(0.8rem + env(safe-area-inset-bottom));
height: 100%;
width: 100%;
max-width: 100%;
min-width: 0;
overflow: hidden;
}
#fullscreen-cover-overlay.lyrics-hidden .fullscreen-lyrics-pane {
display: none;
}
#fullscreen-cover-overlay.lyrics-hidden .fullscreen-media-column {
display: flex;
flex-direction: column;
grid-row: 2;
justify-content: center;
align-items: center;
align-self: center;
width: min(100%, 320px);
max-width: min(88vw, 320px);
min-height: 0;
min-width: 0;
margin: 0 auto;
transform: none;
}
#fullscreen-cover-overlay.lyrics-hidden .fullscreen-artwork-card {
max-width: min(88vw, 320px);
width: min(100%, 320px);
margin: 0 auto;
align-self: center;
justify-self: center;
border-radius: 14px;
box-shadow: 0 24px 56px rgb(0 0 0 / 0.28);
}
#fullscreen-cover-overlay.lyrics-hidden #fullscreen-cover-image {
border-radius: 14px;
}
#fullscreen-cover-overlay.lyrics-hidden .fullscreen-track-info {
align-self: center;
width: min(100%, 320px);
padding: 0;
gap: 0.3rem;
margin-top: 0.9rem;
text-align: center;
}
#fullscreen-cover-overlay.lyrics-hidden .fullscreen-track-text {
gap: 0.2rem;
justify-items: center;
}
#fullscreen-cover-overlay.lyrics-hidden #fullscreen-track-title {
font-size: clamp(1.15rem, 5vw, 1.5rem);
line-height: 1.05;
}
#fullscreen-cover-overlay.lyrics-hidden #fullscreen-track-artist {
font-size: 0.95rem;
color: rgb(255 255 255 / 0.66);
}
#fullscreen-cover-overlay.lyrics-hidden .fullscreen-controls {
grid-row: 4;
width: min(100%, 320px);
max-width: min(88vw, 320px);
justify-self: center;
align-self: end;
margin-top: 0;
}
#fullscreen-cover-overlay.lyrics-hidden #fullscreen-track-title .quality-badge,
#fullscreen-cover-overlay.lyrics-hidden #fullscreen-track-title .shaka-quality-badge {
display: none !important;
}
#fullscreen-cover-overlay.lyrics-hidden .fullscreen-mobile-quality {
display: flex;
justify-content: center;
align-items: center;
min-height: 1.25rem;
margin: 0 auto -0.1rem;
}
#fullscreen-cover-overlay.lyrics-hidden .fullscreen-mobile-quality .quality-badge,
#fullscreen-cover-overlay.lyrics-hidden .fullscreen-mobile-quality .shaka-quality-badge {
display: inline-flex !important;
}
#fullscreen-cover-overlay .fullscreen-volume-container {
width: min(280px, calc(100% - 3rem));
margin-top: 0;
}
#fullscreen-cover-overlay .fs-volume-btn {
left: -2.25rem;
left: -2.5rem;
}
#fullscreen-cover-overlay .fs-volume-bar {

View file

@ -5,6 +5,7 @@ import path from 'path';
import uploadPlugin from './vite-plugin-upload.js';
import blobAssetPlugin from './vite-plugin-blob.js';
import svgUse from './vite-plugin-svg-use.js';
// import purgecss from 'vite-plugin-purgecss';
import purgecss from 'vite-plugin-purgecss';
import { execSync } from 'child_process';
import { playwright } from '@vitest/browser-playwright';
@ -67,13 +68,34 @@ export default defineConfig((_options) => {
outDir: 'dist',
emptyOutDir: true,
sourcemap: true,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
rollupOptions: {
treeshake: true,
},
},
plugins: [
purgecss({
variables: true,
variables: false, // DO NOT REMOVE UNUSED VARIABLES (breaks web components like am-lyrics)
safelist: {
standard: [
/^am-lyrics/,
/^lyplus-/,
'sidepanel',
'side-panel',
'active',
'show',
/^data-/,
/^modal-/,
],
deep: [/^am-lyrics/],
greedy: [/^lyplus-/, /sidepanel/, /side-panel/],
},
}),
authGatePlugin(),
uploadPlugin(),