From f2135cc455008c9be5b3cacf15628ff7bcfe35e3 Mon Sep 17 00:00:00 2001
From: a <252674497+ap5z@users.noreply.github.com>
Date: Sun, 5 Apr 2026 23:46:50 -0400
Subject: [PATCH 1/6] fix: visualizer bugs and better mobile support (#509)
* Refine fullscreen player to look more like apple music
* fix: buttons when in visualizer only mode
* fix: mobile sizing
* Update styles.css
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update js/ui.js
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* feat: refine fullscreen apple player
* fix: add lyrics toggle for mobile
* add lyrics toggle for mobile
* wrong branch oops :sob:
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
fix-gen.py | 1 -
index.html | 18 +-
js/lyrics.js | 8 +
js/ui.js | 298 ++++++++++++++++--
js/visualizer.js | 12 +
styles.css | 776 +++++++++++++++++++++++++++--------------------
6 files changed, 752 insertions(+), 361 deletions(-)
diff --git a/fix-gen.py b/fix-gen.py
index 7ef2989..938291a 100644
--- a/fix-gen.py
+++ b/fix-gen.py
@@ -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)
-
diff --git a/index.html b/index.html
index 9f1a371..605a632 100644
--- a/index.html
+++ b/index.html
@@ -205,13 +205,17 @@
z-index: 0;
"
>
+
+
+
+
-
-
-
+
+
+
@@ -257,6 +261,7 @@
+
0:00
@@ -280,12 +285,7 @@
-
+
Auto
diff --git a/js/lyrics.js b/js/lyrics.js
index 8ee4dcf..96a7dee 100644
--- a/js/lyrics.js
+++ b/js/lyrics.js
@@ -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;
diff --git a/js/ui.js b/js/ui.js
index 763f4f6..da78079 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -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', () => {
@@ -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');
}
@@ -1308,23 +1336,23 @@ export class UIRenderer {
nextTrackEl.classList.remove('animate-in');
}
- const canRenderLyrics = Boolean(
- lyricsManager && activeElement && lyricsPane && lyricsContent && track.type !== 'video'
- );
+ const canRenderLyrics = Boolean(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);
- lyricsContent.innerHTML =
- 'Lyrics are not available for this track.
';
+ lyricsContent.innerHTML = 'Lyrics are not available for this track.
';
}
}
+ this.updateFullscreenLyricsVisibility(overlay);
const playerBar = document.querySelector('.now-playing-bar');
if (playerBar) playerBar.style.display = 'none';
@@ -1355,9 +1383,84 @@ 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,16 +1473,22 @@ export class UIRenderer {
lyricsContent.innerHTML = 'Lyrics appear here.
';
}
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');
const mainContent = document.querySelector('.main-content');
if (mainContent instanceof HTMLElement) {
- if (
- typeof this.fullscreenMainContentOverflow === 'string' &&
- this.fullscreenMainContentOverflow.length > 0
- ) {
+ if (typeof this.fullscreenMainContentOverflow === 'string' && this.fullscreenMainContentOverflow.length > 0) {
mainContent.style.overflowY = this.fullscreenMainContentOverflow;
} else {
mainContent.style.removeProperty('overflow-y');
@@ -1434,6 +1543,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 +1567,7 @@ export class UIRenderer {
}
if (this.visualizer) {
+ this.visualizer.applyPresetOverride('kawarp');
await this.visualizer.start();
overlay.classList.add('visualizer-active');
}
@@ -1493,7 +1613,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 +1698,6 @@ export class UIRenderer {
}
this.fullscreenVisualizerSuppressed = false;
- visualizerSettings.setEnabled(true);
await this.refreshFullscreenVisualizerState(this.player?.activeElement);
if (!overlay.classList.contains('visualizer-active')) {
@@ -1659,6 +1778,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 +1979,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);
};
}
diff --git a/js/visualizer.js b/js/visualizer.js
index d1b637f..b8c802f 100644
--- a/js/visualizer.js
+++ b/js/visualizer.js
@@ -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;
+ }
}
diff --git a/styles.css b/styles.css
index 835d834..04e9b75 100644
--- a/styles.css
+++ b/styles.css
@@ -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;
+ 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, 0px));
+ 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;
@@ -4187,7 +4263,6 @@ input:checked + .slider::before {
#fullscreen-cover-overlay.controls-idle {
cursor: none;
}
-
#fullscreen-cover-image {
max-width: 55vw;
max-height: 45vh;
@@ -7817,148 +7892,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 +8512,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 +9231,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 +10086,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 +10115,33 @@ 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 +10155,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 {
@@ -10320,7 +10167,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
aspect-ratio: 1 / 1;
border-radius: 18px;
overflow: hidden;
- box-shadow: 0 28px 80px rgb(0, 0, 0, 0.26);
+ box-shadow: 0 28px 80px rgba(0, 0, 0, 0.26);
}
#fullscreen-cover-overlay #fullscreen-cover-image {
@@ -10365,7 +10212,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 +10290,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 +10305,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 +10334,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 +10379,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 +10420,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 +10466,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 +10483,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 +10505,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;
@@ -10664,16 +10528,16 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
--lyrics-scroll-padding-top: 18%;
--lyplus-blur-amount: 0.16em;
--lyplus-blur-amount-near: 0.085em;
-
height: 100%;
width: 100%;
- font-family: 'SF Pro Display', Inter, sans-serif;
-
+ font-family:
+ 'SF Pro Display',
+ Inter,
+ sans-serif;
--lyplus-font-size-base: clamp(34px, 3vw, 52px);
--lyplus-padding-line: 8px;
- --lyplus-text-color: rgb(246, 244, 239, 0.08);
+ --lyplus-text-color: rgba(246, 244, 239, 0.08);
--lyplus-active-color: #f6f4ef;
-
line-height: 1.32;
letter-spacing: -0.04em;
font-weight: 600;
@@ -10696,6 +10560,34 @@ 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 +10604,339 @@ 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)
- calc(1.5rem + env(safe-area-inset-bottom));
+ 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))
+ clamp(1rem, 4vw, 1.75rem);
}
#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))
+ 0;
}
- #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: rgba(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))
+ clamp(1rem, 4vw, 1.4rem);
+ 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 {
From d031c44a4060484b4fa3c47f15b3d1b292a77b41 Mon Sep 17 00:00:00 2001
From: ap5z <252674497+ap5z@users.noreply.github.com>
Date: Mon, 6 Apr 2026 03:47:25 +0000
Subject: [PATCH 2/6] style: auto-fix linting issues
---
index.html | 19 ++++++++++++++++---
js/ui.js | 15 +++++++++++----
styles.css | 44 +++++++++++++++++++-------------------------
3 files changed, 46 insertions(+), 32 deletions(-)
diff --git a/index.html b/index.html
index 605a632..6d91859 100644
--- a/index.html
+++ b/index.html
@@ -206,7 +206,11 @@
"
>
-
+
@@ -261,7 +265,11 @@
-
+
0:00
@@ -285,7 +293,12 @@
-
+
Auto
diff --git a/js/ui.js b/js/ui.js
index da78079..b5ac06c 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -1336,7 +1336,9 @@ export class UIRenderer {
nextTrackEl.classList.remove('animate-in');
}
- const canRenderLyrics = Boolean(lyricsManager && activeElement && lyricsPane && lyricsContent && track.type !== 'video');
+ const canRenderLyrics = Boolean(
+ lyricsManager && activeElement && lyricsPane && lyricsContent && track.type !== 'video'
+ );
if (canRenderLyrics) {
this.fullscreenLyricsVisible = true;
if (lyricsToggleBtn) lyricsToggleBtn.style.removeProperty('display');
@@ -1349,7 +1351,8 @@ export class UIRenderer {
overlay.classList.add('lyrics-unavailable');
if (lyricsContent) {
clearFullscreenLyricsSync(lyricsContent);
- lyricsContent.innerHTML = 'Lyrics are not available for this track.
';
+ lyricsContent.innerHTML =
+ 'Lyrics are not available for this track.
';
}
}
this.updateFullscreenLyricsVisibility(overlay);
@@ -1421,7 +1424,8 @@ export class UIRenderer {
if (!title) return;
const qualityBadge = this.getFullscreenQualityBadgeHTML(track);
- const useMobileBadgeOnly = window.matchMedia('(max-width: 768px)').matches && overlay.classList.contains('lyrics-hidden');
+ 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) {
@@ -1488,7 +1492,10 @@ export class UIRenderer {
if (playerBar) playerBar.style.removeProperty('display');
const mainContent = document.querySelector('.main-content');
if (mainContent instanceof HTMLElement) {
- if (typeof this.fullscreenMainContentOverflow === 'string' && this.fullscreenMainContentOverflow.length > 0) {
+ if (
+ typeof this.fullscreenMainContentOverflow === 'string' &&
+ this.fullscreenMainContentOverflow.length > 0
+ ) {
mainContent.style.overflowY = this.fullscreenMainContentOverflow;
} else {
mainContent.style.removeProperty('overflow-y');
diff --git a/styles.css b/styles.css
index 04e9b75..5f78852 100644
--- a/styles.css
+++ b/styles.css
@@ -3924,6 +3924,7 @@ input:checked + .slider::before {
/* 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);
}
@@ -3936,8 +3937,7 @@ input:checked + .slider::before {
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;
- transition:
- opacity 0.65s ease;
+ transition: opacity 0.65s ease;
opacity: calc(1 - (var(--fullscreen-drag-progress, 0) * 0.32));
}
@@ -3993,7 +3993,7 @@ input:checked + .slider::before {
position: relative;
padding: 1rem;
overflow: hidden;
- transform: translateY(var(--fullscreen-drag-offset, 0px));
+ 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),
@@ -4263,6 +4263,7 @@ input:checked + .slider::before {
#fullscreen-cover-overlay.controls-idle {
cursor: none;
}
+
#fullscreen-cover-image {
max-width: 55vw;
max-height: 45vh;
@@ -10125,6 +10126,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
#fullscreen-cover-overlay .fullscreen-main-view {
--fs-media-column-size: minmax(340px, 430px);
--fs-lyrics-column-size: minmax(520px, 760px);
+
width: min(1480px, 100%);
height: 100%;
flex: 1;
@@ -10167,7 +10169,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
aspect-ratio: 1 / 1;
border-radius: 18px;
overflow: hidden;
- box-shadow: 0 28px 80px rgba(0, 0, 0, 0.26);
+ box-shadow: 0 28px 80px rgb(0, 0, 0, 0.26);
}
#fullscreen-cover-overlay #fullscreen-cover-image {
@@ -10528,16 +10530,16 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
--lyrics-scroll-padding-top: 18%;
--lyplus-blur-amount: 0.16em;
--lyplus-blur-amount-near: 0.085em;
+
height: 100%;
width: 100%;
- font-family:
- 'SF Pro Display',
- Inter,
- sans-serif;
+ font-family: 'SF Pro Display', Inter, sans-serif;
+
--lyplus-font-size-base: clamp(34px, 3vw, 52px);
--lyplus-padding-line: 8px;
- --lyplus-text-color: rgba(246, 244, 239, 0.08);
+ --lyplus-text-color: rgb(246, 244, 239, 0.08);
--lyplus-active-color: #f6f4ef;
+
line-height: 1.32;
letter-spacing: -0.04em;
font-weight: 600;
@@ -10563,6 +10565,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
#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;
}
@@ -10609,11 +10612,8 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
width: min(760px, 100%);
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))
- clamp(1rem, 4vw, 1.75rem);
+ 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 {
@@ -10682,11 +10682,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
'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))
- 0;
+ padding: calc(4.45rem + env(safe-area-inset-top)) 0 calc(0.8rem + env(safe-area-inset-bottom));
}
#fullscreen-cover-overlay .fullscreen-media-column {
@@ -10775,9 +10771,10 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
--lyrics-scroll-padding-top: 18%;
--lyplus-font-size-base: clamp(1.75rem, 7vw, 2.35rem);
--lyplus-padding-line: 6px;
- --lyplus-text-color: rgba(246, 244, 239, 0.16);
+ --lyplus-text-color: rgb(246, 244, 239, 0.16);
--lyplus-blur-amount: 0.16em;
--lyplus-blur-amount-near: 0.08em;
+
line-height: 1.2;
}
@@ -10834,11 +10831,8 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
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))
- clamp(1rem, 4vw, 1.4rem);
+ 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%;
From e59e14fdb67d4e07d3f0c22903865ea0677e1129 Mon Sep 17 00:00:00 2001
From: Samidy
Date: Mon, 6 Apr 2026 14:49:53 +0300
Subject: [PATCH 3/6] Gracefully handle no Critic Reviews yet
---
js/ui.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/js/ui.js b/js/ui.js
index b5ac06c..4c38690 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -3854,6 +3854,10 @@ export class UIRenderer {
const data = await response.json();
rateCriticsEl.innerHTML = `Critic Score: ${data.critic.score} , Based on ${data.critic.count} reviews `;
+
+ if (data.critic.score == "NR") {
+ rateCriticsEl.innerHTML = `Critic Score Not Available Yet `;
+ }
rateUsersEl.innerHTML = `User Score: ${data.user.score} , Based on ${data.user.count} reviews `;
} catch (e) {
rateCriticsEl.innerHTML = `Unable to Fetch Critic Score `;
From b3145c5c3ec29a9774a1735c41e76507689615a9 Mon Sep 17 00:00:00 2001
From: SamidyFR <168582143+SamidyFR@users.noreply.github.com>
Date: Mon, 6 Apr 2026 11:50:34 +0000
Subject: [PATCH 4/6] style: auto-fix linting issues
---
js/ui.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/js/ui.js b/js/ui.js
index 4c38690..bd9db3f 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -3855,7 +3855,7 @@ export class UIRenderer {
rateCriticsEl.innerHTML = `Critic Score: ${data.critic.score} , Based on ${data.critic.count} reviews `;
- if (data.critic.score == "NR") {
+ if (data.critic.score == 'NR') {
rateCriticsEl.innerHTML = `Critic Score Not Available Yet `;
}
rateUsersEl.innerHTML = `User Score: ${data.user.score} , Based on ${data.user.count} reviews `;
From d7642ff78e82ea22904b5ce07db49382db311d40 Mon Sep 17 00:00:00 2001
From: Samidy
Date: Mon, 6 Apr 2026 15:55:30 +0300
Subject: [PATCH 5/6] test: Add first row of tests for the site
---
js/db.js | 39 +++++---
js/player.js | 3 +-
js/storage.js | 3 +-
js/tests/db.test.js | 107 ++++++++++++++++++++
js/tests/player.test.js | 195 ++++++++++++++++++++++++++++++++++++
js/tests/storage.test.js | 125 +++++++++++++++++++++++
js/tests/utils.test.js | 211 +++++++++++++++++++++++++++++++++++++++
7 files changed, 665 insertions(+), 18 deletions(-)
create mode 100644 js/tests/db.test.js
create mode 100644 js/tests/player.test.js
create mode 100644 js/tests/storage.test.js
create mode 100644 js/tests/utils.test.js
diff --git a/js/db.js b/js/db.js
index 95c306b..acd548e 100644
--- a/js/db.js
+++ b/js/db.js
@@ -102,8 +102,6 @@ export class MusicDatabase {
async addToHistory(track) {
const storeName = 'history_tracks';
const minified = this._minifyItem(track.type || 'track', track);
- const timestamp = Date.now();
- const entry = { ...minified, timestamp };
const db = await this.open();
@@ -112,25 +110,34 @@ export class MusicDatabase {
const store = transaction.objectStore(storeName);
const index = store.index('timestamp');
- const cursorReq = index.openCursor(null, 'prev');
+ const lastReq = index.openCursor(null, 'prev');
+ let lastTimestamp = 0;
- cursorReq.onsuccess = (e) => {
+ lastReq.onsuccess = (e) => {
const cursor = e.target.result;
- if (cursor) {
- const lastTrack = cursor.value;
- if (lastTrack.id === track.id) {
- store.delete(cursor.primaryKey);
- }
+ if (cursor && lastTimestamp === 0) {
+ lastTimestamp = cursor.value.timestamp;
}
- store.put(entry);
+
+ const timestamp = Math.max(Date.now(), lastTimestamp + 1);
+ const entry = { ...minified, timestamp };
+
+ const dedupeReq = index.openCursor(null, 'prev');
+ dedupeReq.onsuccess = (e2) => {
+ const dedupeCursor = e2.target.result;
+ if (dedupeCursor) {
+ const trackInHistory = dedupeCursor.value;
+ if (trackInHistory.id === track.id) {
+ store.delete(dedupeCursor.primaryKey);
+ }
+ dedupeCursor.continue();
+ } else {
+ store.put(entry);
+ resolve(entry);
+ }
+ };
};
- cursorReq.onerror = (_e) => {
- // If cursor fails, just try to put (fallback)
- store.put(entry);
- };
-
- transaction.oncomplete = () => resolve(entry);
transaction.onerror = (e) => reject(e.target.error);
});
}
diff --git a/js/player.js b/js/player.js
index 81f57fe..508e9d5 100644
--- a/js/player.js
+++ b/js/player.js
@@ -291,7 +291,8 @@ export class Player {
}
setPlaybackSpeed(speed) {
- const validSpeed = Math.max(0.01, Math.min(100, parseFloat(speed) || 1.0));
+ const parsed = parseFloat(speed);
+ const validSpeed = Math.max(0.01, Math.min(100, isNaN(parsed) ? 1.0 : parsed));
audioEffectsSettings.setSpeed(validSpeed);
this.applyAudioEffects();
}
diff --git a/js/storage.js b/js/storage.js
index 222fa10..f702eac 100644
--- a/js/storage.js
+++ b/js/storage.js
@@ -1830,7 +1830,8 @@ export const audioEffectsSettings = {
},
setSpeed(speed) {
- const validSpeed = Math.max(0.01, Math.min(100, parseFloat(speed) || 1.0));
+ const parsed = parseFloat(speed);
+ const validSpeed = Math.max(0.01, Math.min(100, isNaN(parsed) ? 1.0 : parsed));
localStorage.setItem(this.SPEED_KEY, validSpeed.toString());
},
diff --git a/js/tests/db.test.js b/js/tests/db.test.js
new file mode 100644
index 0000000..8f24d57
--- /dev/null
+++ b/js/tests/db.test.js
@@ -0,0 +1,107 @@
+import { expect, test, describe, beforeEach, afterEach, vi } from 'vitest';
+import { MusicDatabase } from '../db.js';
+
+describe('MusicDatabase', () => {
+ let db;
+ const TEST_DB_NAME = 'TestMonochromeDB';
+
+ beforeEach(async () => {
+ db = new MusicDatabase();
+ db.dbName = TEST_DB_NAME;
+ const req = indexedDB.deleteDatabase(TEST_DB_NAME);
+ await new Promise((resolve) => {
+ req.onsuccess = resolve;
+ req.onerror = resolve;
+ });
+ });
+
+ afterEach(async () => {
+ if (db.db) {
+ db.db.close();
+ }
+ const req = indexedDB.deleteDatabase(TEST_DB_NAME);
+ await new Promise((resolve) => {
+ req.onsuccess = resolve;
+ req.onerror = resolve;
+ });
+ });
+
+ test('opens database and creates stores', async () => {
+ const openedDb = await db.open();
+ expect(openedDb.name).toBe(TEST_DB_NAME);
+ expect(openedDb.objectStoreNames.contains('favorites_tracks')).toBe(true);
+ expect(openedDb.objectStoreNames.contains('history_tracks')).toBe(true);
+ expect(openedDb.objectStoreNames.contains('user_playlists')).toBe(true);
+ });
+
+ test('toggleFavorite adds and removes items', async () => {
+ const track = { id: 'track1', title: 'Test Track', artist: { name: 'Artist' } };
+
+ const added = await db.toggleFavorite('track', track);
+ expect(added).toBe(true);
+ const favorites = await db.getFavorites('track');
+ expect(favorites.length).toBe(1);
+ expect(favorites[0].id).toBe('track1');
+
+ const removed = await db.toggleFavorite('track', track);
+ expect(removed).toBe(false);
+ const favoritesAfter = await db.getFavorites('track');
+ expect(favoritesAfter.length).toBe(0);
+ });
+
+ test('addToHistory manages recent tracks and avoids duplicates', async () => {
+ const track1 = { id: 't1', title: 'Track 1' };
+ const track2 = { id: 't2', title: 'Track 2' };
+
+ await db.addToHistory(track1);
+ await db.addToHistory(track2);
+ await db.addToHistory(track1);
+
+ const history = await db.getHistory();
+ expect(history.length).toBe(2);
+ expect(history[0].id).toBe('t1');
+ expect(history[1].id).toBe('t2');
+ });
+
+ test('playlist operations: create, add, remove, delete', async () => {
+ const track = { id: 'track1', title: 'Test Track' };
+
+ const playlist = await db.createPlaylist('My Playlist', [track]);
+ expect(playlist.name).toBe('My Playlist');
+ expect(playlist.tracks.length).toBe(1);
+
+ const track2 = { id: 'track2', title: 'Track 2' };
+ await db.addTrackToPlaylist(playlist.id, track2);
+
+ const updated = await db.getPlaylist(playlist.id);
+ expect(updated.tracks.length).toBe(2);
+ expect(updated.tracks[1].id).toBe('track2');
+
+ await db.removeTrackFromPlaylist(playlist.id, 'track1');
+ const afterRemove = await db.getPlaylist(playlist.id);
+ expect(afterRemove.tracks.length).toBe(1);
+ expect(afterRemove.tracks[0].id).toBe('track2');
+
+ await db.deletePlaylist(playlist.id);
+ const deleted = await db.getPlaylist(playlist.id);
+ expect(deleted).toBeUndefined();
+ });
+
+ test('pinned items management', async () => {
+ const album = { id: 'album1', title: 'Album 1', type: 'album' };
+
+ await db.togglePinned(album, 'album');
+ let pinned = await db.getPinned();
+ expect(pinned.length).toBe(1);
+ expect(pinned[0].id).toBe('album1');
+
+ await db.togglePinned({ id: 'a2', title: 'A2' }, 'album');
+ await db.togglePinned({ id: 'a3', title: 'A3' }, 'album');
+ await db.togglePinned({ id: 'a4', title: 'A4' }, 'album');
+
+ pinned = await db.getPinned();
+ expect(pinned.length).toBe(3);
+ expect(pinned.some((p) => p.id === 'a4')).toBe(true);
+ expect(pinned.some((p) => p.id === 'album1')).toBe(false);
+ });
+});
diff --git a/js/tests/player.test.js b/js/tests/player.test.js
new file mode 100644
index 0000000..07821a9
--- /dev/null
+++ b/js/tests/player.test.js
@@ -0,0 +1,195 @@
+import { expect, test, describe, beforeEach, vi, afterEach } from 'vitest';
+import { Player } from '../player.js';
+import { REPEAT_MODE } from '../utils.js';
+import { audioEffectsSettings } from '../storage.js';
+
+vi.mock('../audio-context.js', () => ({
+ audioContextManager: {
+ init: vi.fn(),
+ resume: vi.fn(() => Promise.resolve()),
+ isReady: vi.fn(() => false),
+ setVolume: vi.fn(),
+ changeSource: vi.fn(),
+ },
+}));
+
+vi.mock('../storage.js', () => ({
+ queueManager: {
+ getQueue: vi.fn(() => null),
+ saveQueue: vi.fn(),
+ },
+ replayGainSettings: { getMode: vi.fn(() => 'off'), getPreamp: vi.fn(() => 0) },
+ trackDateSettings: { useAlbumYear: vi.fn(() => true) },
+ exponentialVolumeSettings: { applyCurve: vi.fn((v) => v) },
+ audioEffectsSettings: {
+ getSpeed: vi.fn(() => 1.0),
+ setSpeed: vi.fn(),
+ isPreservePitchEnabled: vi.fn(() => true),
+ setPreservePitch: vi.fn(),
+ },
+ radioSettings: { isEnabled: vi.fn(() => false) },
+ contentBlockingSettings: {
+ shouldHideTrack: vi.fn(() => false),
+ shouldHideAlbum: vi.fn(() => false),
+ shouldHideArtist: vi.fn(() => false),
+ },
+ qualityBadgeSettings: { isEnabled: vi.fn(() => true) },
+ coverArtSizeSettings: { getSize: vi.fn(() => '1280') },
+ apiSettings: {
+ loadInstancesFromGitHub: vi.fn(() => Promise.resolve([])),
+ getInstances: vi.fn(() => Promise.resolve([])),
+ },
+ recentActivityManager: { addArtist: vi.fn(), addAlbum: vi.fn() },
+ themeManager: { getTheme: vi.fn(() => 'dark'), setTheme: vi.fn() },
+ lastFMStorage: { isEnabled: vi.fn(() => false) },
+ nowPlayingSettings: { getMode: vi.fn(() => 'cover') },
+ gaplessPlaybackSettings: { isEnabled: vi.fn(() => true) },
+}));
+
+vi.mock('../db.js', () => ({
+ db: {
+ get: vi.fn(),
+ put: vi.fn(),
+ },
+}));
+
+vi.mock('../ui.js', () => ({
+ UIRenderer: {
+ renderQueue: vi.fn(),
+ },
+}));
+
+vi.mock('shaka-player', () => ({
+ default: {
+ polyfill: { installAll: vi.fn() },
+ Player: {
+ isBrowserSupported: vi.fn(() => true),
+ prototype: {
+ configure: vi.fn(),
+ addEventListener: vi.fn(),
+ load: vi.fn(),
+ unload: vi.fn(),
+ },
+ },
+ },
+ polyfill: { installAll: vi.fn() },
+ Player: class {
+ static isBrowserSupported() {
+ return true;
+ }
+ configure() {}
+ addEventListener() {}
+ load() {
+ return Promise.resolve();
+ }
+ unload() {
+ return Promise.resolve();
+ }
+ destroy() {
+ return Promise.resolve();
+ }
+ },
+}));
+
+describe('Player', () => {
+ let audioElement;
+ let api;
+ let player;
+
+ beforeEach(async () => {
+ document.body.innerHTML = `
+
+
+
+
+
+
+
+
+
+ `;
+
+ audioElement = document.getElementById('audio-player');
+ api = {
+ getCoverUrl: vi.fn((id) => `url-${id}`),
+ getCoverSrcset: vi.fn(),
+ getStreamUrl: vi.fn(),
+ };
+
+ Player._instance = null;
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ test('initialization sets up initial state', async () => {
+ player = new Player(audioElement, api);
+ expect(player.audio).toBe(audioElement);
+ expect(player.api).toBe(api);
+ expect(player.queue).toEqual([]);
+ expect(player.shuffleActive).toBe(false);
+ });
+
+ test('setVolume updates userVolume and localStorage', () => {
+ player = new Player(audioElement, api);
+ player.setVolume(0.5);
+ expect(player.userVolume).toBe(0.5);
+ expect(localStorage.getItem('volume')).toBe('0.5');
+ });
+
+ test('shuffle toggles correctly', () => {
+ player = new Player(audioElement, api);
+ player.queue = [{ id: 1 }, { id: 2 }, { id: 3 }];
+
+ player.toggleShuffle();
+ expect(player.shuffleActive).toBe(true);
+ expect(player.shuffledQueue.length).toBe(3);
+
+ player.toggleShuffle();
+ expect(player.shuffleActive).toBe(false);
+ });
+
+ test('repeat mode cycles correctly', () => {
+ player = new Player(audioElement, api);
+ expect(player.repeatMode).toBe(REPEAT_MODE.OFF);
+
+ player.toggleRepeat();
+ expect(player.repeatMode).toBe(REPEAT_MODE.ALL);
+
+ player.toggleRepeat();
+ expect(player.repeatMode).toBe(REPEAT_MODE.ONE);
+
+ player.toggleRepeat();
+ expect(player.repeatMode).toBe(REPEAT_MODE.OFF);
+ });
+
+ test('addToQueue adds tracks to the end', async () => {
+ player = new Player(audioElement, api);
+ player.queue = [{ id: 1 }];
+
+ await player.addToQueue([{ id: 2 }, { id: 3 }]);
+ expect(player.queue.length).toBe(3);
+ expect(player.queue[2].id).toBe(3);
+ });
+
+ test('clearQueue resets queue state', async () => {
+ player = new Player(audioElement, api);
+ player.queue = [{ id: 1 }];
+ player.currentQueueIndex = 0;
+
+ await player.clearQueue();
+ expect(player.queue).toEqual([]);
+ expect(player.currentQueueIndex).toBe(-1);
+ });
+
+ test('setPlaybackSpeed clamps values', () => {
+ player = new Player(audioElement, api);
+
+ player.setPlaybackSpeed(2.0);
+ expect(audioEffectsSettings.setSpeed).toHaveBeenCalledWith(2.0);
+
+ player.setPlaybackSpeed(0);
+ expect(audioEffectsSettings.setSpeed).toHaveBeenCalledWith(0.01);
+ });
+});
diff --git a/js/tests/storage.test.js b/js/tests/storage.test.js
new file mode 100644
index 0000000..3162623
--- /dev/null
+++ b/js/tests/storage.test.js
@@ -0,0 +1,125 @@
+import { expect, test, describe, beforeEach, vi } from 'vitest';
+import {
+ recentActivityManager,
+ themeManager,
+ lastFMStorage,
+ nowPlayingSettings,
+ gaplessPlaybackSettings,
+ exponentialVolumeSettings,
+ audioEffectsSettings,
+} from '../storage.js';
+
+describe('storage.js', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ vi.clearAllMocks();
+ });
+
+ describe('recentActivityManager', () => {
+ test('initializes with empty arrays', () => {
+ const recents = recentActivityManager.getRecents();
+ expect(recents.artists).toEqual([]);
+ expect(recents.albums).toEqual([]);
+ });
+
+ test('adds artist and maintains limit', () => {
+ for (let i = 0; i < 15; i++) {
+ recentActivityManager.addArtist({ id: i, name: `Artist ${i}` });
+ }
+ const recents = recentActivityManager.getRecents();
+ expect(recents.artists.length).toBe(10);
+ expect(recents.artists[0].id).toBe(14);
+ });
+
+ test('clears recents', () => {
+ recentActivityManager.addArtist({ id: 1, name: 'Artist' });
+ recentActivityManager.clear();
+ const recents = recentActivityManager.getRecents();
+ expect(recents.artists).toEqual([]);
+ });
+ });
+
+ describe('themeManager', () => {
+ test('gets and sets theme', () => {
+ themeManager.setTheme('dark');
+ expect(themeManager.getTheme()).toBe('dark');
+ expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
+ });
+
+ test('handles custom theme', () => {
+ const colors = { primary: '#ff0000', background: '#000000' };
+ themeManager.setCustomTheme(colors);
+ expect(themeManager.getTheme()).toBe('custom');
+ expect(themeManager.getCustomTheme()).toEqual(colors);
+ expect(document.documentElement.style.getPropertyValue('--primary')).toBe('#ff0000');
+ });
+ });
+
+ describe('lastFMStorage', () => {
+ test('handles enabled state', () => {
+ lastFMStorage.setEnabled(true);
+ expect(lastFMStorage.isEnabled()).toBe(true);
+ lastFMStorage.setEnabled(false);
+ expect(lastFMStorage.isEnabled()).toBe(false);
+ });
+
+ test('obfuscates sensitive data', () => {
+ const key = 'test-api-key';
+ lastFMStorage.setCustomApiKey(key);
+ expect(localStorage.getItem(lastFMStorage.CUSTOM_API_KEY)).not.toBe(key);
+ expect(lastFMStorage.getCustomApiKey()).toBe(key);
+ });
+ });
+
+ describe('nowPlayingSettings', () => {
+ test('gets and sets mode', () => {
+ expect(nowPlayingSettings.getMode()).toBe('cover');
+ nowPlayingSettings.setMode('visualizer');
+ expect(nowPlayingSettings.getMode()).toBe('visualizer');
+ });
+ });
+
+ describe('gaplessPlaybackSettings', () => {
+ test('defaults to true', () => {
+ expect(gaplessPlaybackSettings.isEnabled()).toBe(true);
+ });
+
+ test('sets enabled state', () => {
+ gaplessPlaybackSettings.setEnabled(false);
+ expect(gaplessPlaybackSettings.isEnabled()).toBe(false);
+ });
+ });
+
+ describe('exponentialVolumeSettings', () => {
+ test('applies curve when enabled', () => {
+ exponentialVolumeSettings.setEnabled(true);
+ expect(exponentialVolumeSettings.applyCurve(0.5)).toBeCloseTo(0.125);
+ expect(exponentialVolumeSettings.inverseCurve(0.125)).toBeCloseTo(0.5);
+ });
+
+ test('does not apply curve when disabled', () => {
+ exponentialVolumeSettings.setEnabled(false);
+ expect(exponentialVolumeSettings.applyCurve(0.5)).toBe(0.5);
+ expect(exponentialVolumeSettings.inverseCurve(0.5)).toBe(0.5);
+ });
+ });
+
+ describe('audioEffectsSettings', () => {
+ test('gets and sets speed within bounds', () => {
+ audioEffectsSettings.setSpeed(2.0);
+ expect(audioEffectsSettings.getSpeed()).toBe(2.0);
+
+ audioEffectsSettings.setSpeed(200);
+ expect(audioEffectsSettings.getSpeed()).toBe(100);
+
+ audioEffectsSettings.setSpeed(0);
+ expect(audioEffectsSettings.getSpeed()).toBe(0.01);
+ });
+
+ test('resets speed', () => {
+ audioEffectsSettings.setSpeed(2.0);
+ audioEffectsSettings.resetSpeed();
+ expect(audioEffectsSettings.getSpeed()).toBe(1.0);
+ });
+ });
+});
diff --git a/js/tests/utils.test.js b/js/tests/utils.test.js
new file mode 100644
index 0000000..edacfbc
--- /dev/null
+++ b/js/tests/utils.test.js
@@ -0,0 +1,211 @@
+import { expect, test, describe, vi } from 'vitest';
+import * as utils from '../utils.js';
+
+vi.mock('../ModernSettings.js', () => ({
+ modernSettings: {
+ filenameTemplate: '{artist} - {album} - {trackNumber} - {title}',
+ },
+}));
+
+vi.mock('../icons.js', () => ({
+ SVG_ATMOS: () => 'atmos ',
+}));
+
+vi.mock('../storage.js', () => ({
+ qualityBadgeSettings: { isEnabled: vi.fn(() => true) },
+ coverArtSizeSettings: { getSize: vi.fn(() => '1280') },
+ trackDateSettings: { useAlbumYear: vi.fn(() => false) },
+}));
+
+describe('utils.js', () => {
+ describe('formatTime', () => {
+ test('formats seconds into M:SS', () => {
+ expect(utils.formatTime(0)).toBe('0:00');
+ expect(utils.formatTime(5)).toBe('0:05');
+ expect(utils.formatTime(60)).toBe('1:00');
+ expect(utils.formatTime(65)).toBe('1:05');
+ });
+
+ test('formats seconds into H:MM:SS', () => {
+ expect(utils.formatTime(3600)).toBe('1:00:00');
+ expect(utils.formatTime(3665)).toBe('1:01:05');
+ });
+
+ test('handles NaN', () => {
+ expect(utils.formatTime(NaN)).toBe('0:00');
+ });
+ });
+
+ describe('sanitizeForFilename', () => {
+ test('replaces invalid characters with underscores', () => {
+ expect(utils.sanitizeForFilename('a/b:c*d?e"fh|i')).toBe('a_b_c_d_e_f_g_h_i');
+ });
+
+ test('collapses multiple spaces and trims', () => {
+ expect(utils.sanitizeForFilename(' hello world ')).toBe('hello world');
+ });
+
+ test('returns "Unknown" for empty input', () => {
+ expect(utils.sanitizeForFilename('')).toBe('Unknown');
+ expect(utils.sanitizeForFilename(null)).toBe('Unknown');
+ });
+ });
+
+ describe('replaceTokens', () => {
+ test('replaces tokens in template', () => {
+ const template = '{artist} - {title}';
+ const tokens = { artist: 'Artist', title: 'Title' };
+ expect(utils.replaceTokens(template, tokens)).toBe('Artist - Title');
+ });
+
+ test('leaves unknown tokens as is', () => {
+ const template = '{artist} - {unknown}';
+ const tokens = { artist: 'Artist' };
+ expect(utils.replaceTokens(template, tokens)).toBe('Artist - {unknown}');
+ });
+ });
+
+ describe('formatPathTemplate', () => {
+ test('formats path correctly', () => {
+ const data = {
+ artist: 'Artist',
+ album: 'Album',
+ trackNumber: 1,
+ title: 'Title',
+ discNumber: 1,
+ };
+ const template = '{artist}/{album}/{trackNumber} - {title}';
+ expect(utils.formatPathTemplate(template, data)).toBe('Artist/Album/01 - Title');
+ });
+
+ test('strips . and .. segments', () => {
+ const data = { artist: '..', title: '.' };
+ const template = '{artist}/{title}/song';
+ expect(utils.formatPathTemplate(template, data)).toBe('song');
+ });
+ });
+
+ describe('detectAudioFormat', () => {
+ test('detects flac', () => {
+ const view = new DataView(new Uint8Array([0x66, 0x4c, 0x61, 0x43]).buffer);
+ expect(utils.detectAudioFormat(view)).toBe('flac');
+ });
+
+ test('detects mp4', () => {
+ const view = new DataView(new Uint8Array([0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70]).buffer);
+ expect(utils.detectAudioFormat(view)).toBe('mp4');
+ });
+
+ test('detects mp3 (ID3)', () => {
+ const view = new DataView(new Uint8Array([0x49, 0x44, 0x33]).buffer);
+ expect(utils.detectAudioFormat(view)).toBe('mp3');
+ });
+
+ test('detects ogg', () => {
+ const view = new DataView(new Uint8Array([0x4f, 0x67, 0x67, 0x53]).buffer);
+ expect(utils.detectAudioFormat(view)).toBe('ogg');
+ });
+
+ test('returns null for unknown format', () => {
+ const view = new DataView(new Uint8Array([0, 0, 0, 0]).buffer);
+ expect(utils.detectAudioFormat(view)).toBeNull();
+ });
+ });
+
+ describe('normalizeQualityToken', () => {
+ test('normalizes various quality strings', () => {
+ expect(utils.normalizeQualityToken('HI_RES_LOSSLESS')).toBe('HI_RES_LOSSLESS');
+ expect(utils.normalizeQualityToken('MASTER')).toBe('HI_RES_LOSSLESS');
+ expect(utils.normalizeQualityToken('HIFI')).toBe('LOSSLESS');
+ expect(utils.normalizeQualityToken('ATMOS')).toBe('DOLBY_ATMOS');
+ });
+
+ test('returns null for unknown quality', () => {
+ expect(utils.normalizeQualityToken('UNKNOWN')).toBeNull();
+ });
+ });
+
+ describe('pickBestQuality', () => {
+ test('picks the highest quality from list', () => {
+ expect(utils.pickBestQuality(['LOSSLESS', 'HI_RES_LOSSLESS', 'HIGH'])).toBe('HI_RES_LOSSLESS');
+ expect(utils.pickBestQuality(['LOW', 'HIGH'])).toBe('HIGH');
+ expect(utils.pickBestQuality(['DOLBY_ATMOS', 'HI_RES_LOSSLESS'])).toBe('DOLBY_ATMOS');
+ });
+ });
+
+ describe('getTrackTitle', () => {
+ test('returns title with version if present', () => {
+ expect(utils.getTrackTitle({ title: 'Song', version: 'Remix' })).toBe('Song (Remix)');
+ });
+
+ test('returns just title if no version', () => {
+ expect(utils.getTrackTitle({ title: 'Song' })).toBe('Song');
+ });
+
+ test('returns fallback if no title', () => {
+ expect(utils.getTrackTitle({}, { fallback: 'No Title' })).toBe('No Title');
+ });
+ });
+
+ describe('getTrackArtists', () => {
+ test('joins multiple artists', () => {
+ const track = { artists: [{ name: 'A' }, { name: 'B' }] };
+ expect(utils.getTrackArtists(track)).toBe('A, B');
+ });
+
+ test('returns fallback if no artists', () => {
+ expect(utils.getTrackArtists({})).toBe('Unknown Artist');
+ });
+ });
+
+ describe('getTrackDiscNumber', () => {
+ test('extracts disc number from various properties', () => {
+ expect(utils.getTrackDiscNumber({ discNumber: 2 })).toBe(2);
+ expect(utils.getTrackDiscNumber({ volumeNumber: 3 })).toBe(3);
+ expect(utils.getTrackDiscNumber({ mediaNumber: 4 })).toBe(4);
+ });
+
+ test('returns null for invalid values', () => {
+ expect(utils.getTrackDiscNumber({ discNumber: 0 })).toBeNull();
+ expect(utils.getTrackDiscNumber({ discNumber: 'abc' })).toBeNull();
+ });
+ });
+
+ describe('tryCatch', () => {
+ test('executes sync function', () => {
+ const fn = vi.fn(() => 'success');
+ const onError = vi.fn();
+ expect(utils.tryCatch(fn, onError)).toBe('success');
+ expect(onError).not.toHaveBeenCalled();
+ });
+
+ test('handles sync error', () => {
+ const error = new Error('fail');
+ const fn = vi.fn(() => {
+ throw error;
+ });
+ const onError = vi.fn((err) => err.message);
+ expect(utils.tryCatch(fn, onError)).toBe('fail');
+ expect(onError).toHaveBeenCalledWith(error);
+ });
+
+ test('executes async function', async () => {
+ const fn = vi.fn(async () => 'success');
+ const onError = vi.fn();
+ const result = await utils.tryCatch(fn, onError);
+ expect(result).toBe('success');
+ expect(onError).not.toHaveBeenCalled();
+ });
+
+ test('handles async error', async () => {
+ const error = new Error('fail');
+ const fn = vi.fn(async () => {
+ throw error;
+ });
+ const onError = vi.fn(async (err) => err.message);
+ const result = await utils.tryCatch(fn, onError);
+ expect(result).toBe('fail');
+ expect(onError).toHaveBeenCalledWith(error);
+ });
+ });
+});
From 11f66c1a28d9d13b54545b5224344c621d3fa8c9 Mon Sep 17 00:00:00 2001
From: Samidy
Date: Mon, 6 Apr 2026 17:35:43 +0300
Subject: [PATCH 6/6] le "debloat root" thingy
---
fix-gen.py => .github/scripts/fix-gen.py | 0
.../scripts/gen-editors-picks.py | 0
.github/workflows/editors-picks.yml | 2 +-
Dockerfile => docker/Dockerfile | 0
Dockerfile.dev => docker/Dockerfile.dev | 0
docker-compose.yml => docker/docker-compose.yml | 0
legacy.html | 16 ----------------
.../editors-picks-input.txt | 0
8 files changed, 1 insertion(+), 17 deletions(-)
rename fix-gen.py => .github/scripts/fix-gen.py (100%)
rename gen-editors-picks.py => .github/scripts/gen-editors-picks.py (100%)
rename Dockerfile => docker/Dockerfile (100%)
rename Dockerfile.dev => docker/Dockerfile.dev (100%)
rename docker-compose.yml => docker/docker-compose.yml (100%)
delete mode 100644 legacy.html
rename editors-picks-input.txt => public/editors-picks-input.txt (100%)
diff --git a/fix-gen.py b/.github/scripts/fix-gen.py
similarity index 100%
rename from fix-gen.py
rename to .github/scripts/fix-gen.py
diff --git a/gen-editors-picks.py b/.github/scripts/gen-editors-picks.py
similarity index 100%
rename from gen-editors-picks.py
rename to .github/scripts/gen-editors-picks.py
diff --git a/.github/workflows/editors-picks.yml b/.github/workflows/editors-picks.yml
index 680ce1a..933fee4 100644
--- a/.github/workflows/editors-picks.yml
+++ b/.github/workflows/editors-picks.yml
@@ -97,7 +97,7 @@ jobs:
- name: Generate new editors picks
if: steps.backoff.outputs.skip == 'false'
- run: python3 gen-editors-picks.py
+ run: python3 .github/scripts/gen-editors-picks.py
- name: Commit and push
if: steps.backoff.outputs.skip == 'false'
diff --git a/Dockerfile b/docker/Dockerfile
similarity index 100%
rename from Dockerfile
rename to docker/Dockerfile
diff --git a/Dockerfile.dev b/docker/Dockerfile.dev
similarity index 100%
rename from Dockerfile.dev
rename to docker/Dockerfile.dev
diff --git a/docker-compose.yml b/docker/docker-compose.yml
similarity index 100%
rename from docker-compose.yml
rename to docker/docker-compose.yml
diff --git a/legacy.html b/legacy.html
deleted file mode 100644
index d2d75f9..0000000
--- a/legacy.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
- Redirecting...
-
-
-
-
- If you are not redirected, click here .
-
-
diff --git a/editors-picks-input.txt b/public/editors-picks-input.txt
similarity index 100%
rename from editors-picks-input.txt
rename to public/editors-picks-input.txt