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 😭

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
a 2026-04-05 23:46:50 -04:00 committed by GitHub
parent 7df10b0f5e
commit f2135cc455
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 752 additions and 361 deletions

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

@ -205,13 +205,17 @@
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 +261,7 @@
</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">
@ -280,12 +285,7 @@
<button id="fs-repeat-btn" title="Repeat">
<use svg="!lucide/repeat.svg" size="24" />
</button>
<button
id="fs-quality-btn"
class="fs-quality-btn"
title="Quality"
style="display: none"
>
<button id="fs-quality-btn" class="fs-quality-btn" title="Quality" style="display: none">
<use svg="!lucide/pencil-line.svg" size="20" />
<span class="fs-quality-label">Auto</span>
</button>

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;

298
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', () => {
@ -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 =
'<div class="fullscreen-lyrics-empty">Lyrics are not available for this track.</div>';
lyricsContent.innerHTML = '<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 +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 = '<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');
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);
};
}

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;
}
}

File diff suppressed because it is too large Load diff