Refine fullscreen player to look more like apple music

This commit is contained in:
Alan Brooks 2026-04-04 23:48:20 -04:00
parent a812198a07
commit 0b1bb3cd11
5 changed files with 1003 additions and 158 deletions

View file

@ -144,77 +144,99 @@
<button id="toggle-fullscreen-lyrics-btn" class="fullscreen-lyrics-toggle" title="Toggle Lyrics">
<use svg="!lucide/mic-vocal.svg" size="24" />
</button>
<button id="close-fullscreen-cover-btn" title="Close"><use svg="!lucide/x.svg" size="24" /></button>
<div class="fullscreen-main-view">
<img
id="fullscreen-cover-image"
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
alt="Album Cover"
/>
<div class="fullscreen-top-actions">
<button id="fs-visualizer-btn" class="fs-visualizer-btn" title="Disable Visualizer">
<use svg="!lucide/audio-lines.svg" size="20" />
</button>
<button id="close-fullscreen-cover-btn" title="Close"><use svg="!lucide/x.svg" size="24" /></button>
</div>
<div class="fullscreen-shell">
<div class="fullscreen-main-view">
<div class="fullscreen-media-column">
<div class="fullscreen-artwork-card">
<img
id="fullscreen-cover-image"
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
alt="Album Cover"
/>
</div>
<div class="fullscreen-track-info">
<h2 id="fullscreen-track-title"></h2>
<h3 id="fullscreen-track-artist"></h3>
<div class="fullscreen-actions">
<button id="fs-like-btn" class="btn-icon like-btn" title="Like">
<use svg="!lucide/heart.svg" class="heart-icon" size="24" />
</button>
<button id="fs-add-playlist-btn" class="btn-icon" title="Add to Playlist">
<use svg="!lucide/square-pen.svg" size="24" />
</button>
<button id="fs-download-btn" class="btn-icon" title="Download">
<use svg="!lucide/download.svg" size="24" />
</button>
<button id="fs-cast-btn" class="btn-icon" title="Cast">
<use svg="!lucide/cast.svg" size="24" />
</button>
<button id="fs-queue-btn" class="btn-icon" title="Queue">
<use svg="!lucide/list.svg" size="24" />
</button>
</div>
<div id="fullscreen-next-track" style="display: none">
<span class="label">Up Next: </span>
<span class="value"></span>
</div>
</div>
<div class="fullscreen-controls">
<div class="fullscreen-progress-container">
<span id="fs-current-time">0:00</span>
<div id="fs-progress-bar" class="progress-bar">
<div id="fs-progress-fill" class="progress-fill"></div>
<div class="fullscreen-track-info">
<div class="fullscreen-track-text">
<h2 id="fullscreen-track-title"></h2>
<h3 id="fullscreen-track-artist"></h3>
</div>
<div class="fullscreen-actions">
<button id="fs-like-btn" class="btn-icon like-btn" title="Like">
<use svg="!lucide/heart.svg" class="heart-icon" size="24" />
</button>
<button id="fs-add-playlist-btn" class="btn-icon" title="Add to Playlist">
<use svg="!lucide/square-pen.svg" size="24" />
</button>
<button id="fs-download-btn" class="btn-icon" title="Download">
<use svg="!lucide/download.svg" size="24" />
</button>
<button id="fs-cast-btn" class="btn-icon" title="Cast">
<use svg="!lucide/cast.svg" size="24" />
</button>
<button id="fs-queue-btn" class="btn-icon" title="Queue">
<use svg="!lucide/list.svg" size="24" />
</button>
</div>
<div id="fullscreen-next-track" style="display: none">
<span class="label">Up Next</span>
<span class="value"></span>
</div>
</div>
<span id="fs-total-duration">0:00</span>
</div>
<div class="fullscreen-buttons">
<button id="fs-shuffle-btn" title="Shuffle">
<use svg="!lucide/shuffle.svg" size="24" />
</button>
<button id="fs-prev-btn" title="Previous">
<use svg="!lucide/arrow-left-to-line.svg" size="24" />
</button>
<button id="fs-play-pause-btn" class="play-pause-btn" title="Play">
<use svg="./images/play-large.svg" size="32" />
</button>
<button id="fs-next-btn" title="Next">
<use svg="!lucide/arrow-right-to-line.svg" size="24" />
</button>
<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">
<use svg="!lucide/pencil-line.svg" size="20" />
<span class="fs-quality-label">Auto</span>
</button>
<div id="fs-quality-menu" class="fs-quality-menu" style="display: none"></div>
</div>
<div class="fullscreen-volume-container">
<button id="fs-volume-btn" class="fs-volume-btn" title="Mute">
<use svg="!lucide/volume-1.svg" size="24" />
</button>
<div id="fs-volume-bar" class="fs-volume-bar">
<div id="fs-volume-fill" class="fs-volume-fill"></div>
<div class="fullscreen-controls">
<div class="fullscreen-progress-container">
<span id="fs-current-time">0:00</span>
<div id="fs-progress-bar" class="progress-bar">
<div id="fs-progress-fill" class="progress-fill"></div>
</div>
<span id="fs-total-duration">0:00</span>
</div>
<div class="fullscreen-buttons">
<button id="fs-shuffle-btn" title="Shuffle">
<use svg="!lucide/shuffle.svg" size="24" />
</button>
<button id="fs-prev-btn" title="Previous">
<use svg="!lucide/arrow-left-to-line.svg" size="24" />
</button>
<button id="fs-play-pause-btn" class="play-pause-btn" title="Play">
<use svg="./images/play-large.svg" size="32" />
</button>
<button id="fs-next-btn" title="Next">
<use svg="!lucide/arrow-right-to-line.svg" size="24" />
</button>
<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">
<use svg="!lucide/pencil-line.svg" size="20" />
<span class="fs-quality-label">Auto</span>
</button>
<div id="fs-quality-menu" class="fs-quality-menu" style="display: none"></div>
</div>
<div class="fullscreen-volume-container">
<button id="fs-volume-btn" class="fs-volume-btn" title="Mute">
<use svg="!lucide/volume-1.svg" size="24" />
</button>
<div id="fs-volume-bar" class="fs-volume-bar">
<div id="fs-volume-fill" class="fs-volume-fill"></div>
</div>
</div>
</div>
</div>
<aside id="fullscreen-lyrics-pane" class="fullscreen-lyrics-pane">
<div class="fullscreen-lyrics-shell">
<div id="fullscreen-lyrics-content" class="fullscreen-lyrics-content">
<div class="fullscreen-lyrics-empty">Lyrics appear here.</div>
</div>
</div>
</aside>
</div>
</div>
</div>

View file

@ -11,6 +11,8 @@ export { default as SVG_CLOCK } from '!lucide/clock.svg?svg&icon';
export { default as SVG_CLOSE } from '!lucide/x.svg?svg&icon';
export { default as SVG_DISC } from '!lucide/disc.svg?svg&icon';
export { default as SVG_DOWNLOAD } from '!lucide/download.svg?svg&icon';
export { default as SVG_EYE } from '!lucide/eye.svg?svg&icon';
export { default as SVG_EYE_OFF } from '!lucide/eye-off.svg?svg&icon';
export { default as SVG_EQUAL } from '!lucide/equal.svg?svg&icon';
export { default as SVG_FACEBOOK } from '../images/facebook.svg?svg&icon';
export { default as SVG_FOLDER_PLUS } from '!lucide/folder-plus.svg?svg&icon';

View file

@ -964,6 +964,74 @@ themeObserver.observe(document.documentElement, {
attributeFilter: ['data-theme', 'style'],
});
function applyFullscreenLyricsShadowTweaks(amLyrics, container) {
if (!amLyrics || container?.id !== 'fullscreen-lyrics-content') return;
const injectStyle = () => {
const root = amLyrics.shadowRoot;
if (!root) return false;
let styleEl = root.getElementById('monochrome-fullscreen-lyrics-tweaks');
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = 'monochrome-fullscreen-lyrics-tweaks';
root.appendChild(styleEl);
}
styleEl.textContent = `
.lyrics-container {
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
.lyrics-container::-webkit-scrollbar {
width: 0 !important;
height: 0 !important;
display: none !important;
background: transparent !important;
}
.lyrics-line {
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-container {
transition:
transform 0.72s cubic-bezier(0.22, 1, 0.36, 1),
background-color 0.3s ease,
color 0.3s ease !important;
}
.lyrics-line.active .lyrics-line-container,
.lyrics-line.pre-active .lyrics-line-container {
transition:
transform 0.56s cubic-bezier(0.22, 1, 0.36, 1),
background-color 0.22s ease,
color 0.22s ease !important;
}
`;
return true;
};
if (injectStyle()) return;
let attempts = 0;
const maxAttempts = 24;
const tryInject = () => {
if (injectStyle()) return;
attempts += 1;
if (attempts < maxAttempts) {
requestAnimationFrame(tryInject);
}
};
requestAnimationFrame(tryInject);
}
async function renderLyricsComponent(container, track, audioPlayer, lyricsManager) {
container.innerHTML = '<div class="lyrics-loading">Loading lyrics...</div>';
@ -1006,6 +1074,7 @@ async function renderLyricsComponent(container, track, audioPlayer, lyricsManage
amLyrics.style.width = '100%';
container.appendChild(amLyrics);
applyFullscreenLyricsShadowTweaks(amLyrics, container);
lyricsManager.setupLyricsObserver(amLyrics);

313
js/ui.js
View file

@ -15,7 +15,7 @@ import {
escapeHtml,
getShareUrl,
} from './utils.js';
import { openLyricsPanel } from './lyrics.js';
import { openLyricsPanel, renderLyricsInFullscreen, clearFullscreenLyricsSync } from './lyrics.js';
import {
recentActivityManager,
backgroundSettings,
@ -27,9 +27,6 @@ import {
contentBlockingSettings,
settingsUiState,
fullscreenCoverNoRoundSettings,
fullscreenCoverVanillaTiltSettings,
fullscreenCoverTiltDistanceSettings,
fullscreenCoverTiltSpeedSettings,
} from './storage.js';
import { db } from './db.js';
import { getVibrantColorFromImage } from './vibrant-color.js';
@ -61,6 +58,8 @@ import {
SVG_HEART,
SVG_VOLUME,
SVG_MUTE,
SVG_EYE,
SVG_EYE_OFF,
SVG_HEART_FILLED,
SVG_CLOSE,
SVG_SORT,
@ -89,6 +88,11 @@ import {
SVG_CHECKBOX,
} from './icons.js';
const setFullscreenUIToggleIcon = (button, visualizerOnlyMode) => {
if (!button) return;
button.innerHTML = visualizerOnlyMode ? SVG_EYE(24) : SVG_EYE_OFF(24);
};
function sortTracks(tracks, sortType) {
if (sortType === 'custom') return [...tracks];
const sorted = [...tracks];
@ -151,6 +155,8 @@ export class UIRenderer {
this.renderLock = false;
this.lastRecommendedTracks = [];
this.currentArtistId = null;
this.fullscreenLyricsVisible = true;
this.fullscreenPlaybackStateCleanup = null;
// Listen for dynamic color reset events
window.addEventListener('reset-dynamic-color', () => {
@ -177,20 +183,8 @@ export class UIRenderer {
} else {
overlay.classList.remove('fullscreen-cover-no-round');
}
if (coverImage) {
if (fullscreenCoverVanillaTiltSettings.isEnabled() && window.VanillaTilt) {
if (coverImage.vanillaTilt) {
coverImage.vanillaTilt.destroy();
}
window.VanillaTilt.init(coverImage, {
max: fullscreenCoverTiltDistanceSettings.getValue(),
speed: fullscreenCoverTiltSpeedSettings.getValue(),
glare: true,
'max-glare': 0.3,
});
} else if (coverImage.vanillaTilt) {
coverImage.vanillaTilt.destroy();
}
if (coverImage?.vanillaTilt) {
coverImage.vanillaTilt.destroy();
}
}
});
@ -1117,6 +1111,23 @@ export class UIRenderer {
root.style.removeProperty('--track-hover-bg');
}
getFullscreenQualityBadgeHTML(track) {
const nowPlayingTitle = document.querySelector('.now-playing-bar .title');
if (nowPlayingTitle && this.player?.currentTrack?.id === track?.id) {
const badges = Array.from(nowPlayingTitle.querySelectorAll('.shaka-quality-badge, .quality-badge'));
const liveBadge = badges.find((badge) => getComputedStyle(badge).display !== 'none') || badges[0];
if (liveBadge) {
const badgeClone = liveBadge.cloneNode(true);
if (badgeClone instanceof HTMLElement) {
badgeClone.style.removeProperty('display');
}
return badgeClone.outerHTML;
}
}
return createQualityBadgeHTML(track);
}
async updateFullscreenMetadata(track, nextTrack) {
if (!track) return;
const overlay = document.getElementById('fullscreen-cover-overlay');
@ -1214,7 +1225,7 @@ export class UIRenderer {
await this.extractAndApplyColor(this.api.getCoverUrl(track.album?.cover, '80'));
}
const qualityBadge = createQualityBadgeHTML(track);
const qualityBadge = this.getFullscreenQualityBadgeHTML(track);
title.innerHTML = `${escapeHtml(track.title)} ${qualityBadge}`;
artist.textContent = getTrackArtists(track);
@ -1228,11 +1239,14 @@ export class UIRenderer {
async showFullscreenCover(track, nextTrack, lyricsManager, activeElement) {
if (!track) return;
this.fullscreenVisualizerSuppressed = true;
if (window.location.hash !== '#fullscreen') {
window.history.pushState({ fullscreen: true }, '', '#fullscreen');
}
const overlay = document.getElementById('fullscreen-cover-overlay');
const nextTrackEl = document.getElementById('fullscreen-next-track');
const lyricsPane = document.getElementById('fullscreen-lyrics-pane');
const lyricsContent = document.getElementById('fullscreen-lyrics-content');
const lyricsToggleBtn = document.getElementById('toggle-fullscreen-lyrics-btn');
await this.updateFullscreenMetadata(track, nextTrack);
@ -1245,27 +1259,33 @@ export class UIRenderer {
nextTrackEl.classList.remove('animate-in');
}
if (lyricsManager && activeElement) {
lyricsToggleBtn.style.display = 'flex';
lyricsToggleBtn.classList.remove('active');
const toggleLyrics = () => {
openLyricsPanel(track, activeElement, lyricsManager);
lyricsToggleBtn.classList.toggle('active');
};
const newToggleBtn = lyricsToggleBtn.cloneNode(true);
lyricsToggleBtn.parentNode.replaceChild(newToggleBtn, lyricsToggleBtn);
newToggleBtn.addEventListener('click', toggleLyrics);
const canRenderLyrics = Boolean(lyricsManager && activeElement && lyricsPane && lyricsContent && track.type !== 'video');
if (canRenderLyrics) {
lyricsToggleBtn.style.display = 'none';
overlay.classList.remove('lyrics-unavailable');
clearFullscreenLyricsSync(lyricsContent);
await renderLyricsInFullscreen(track, activeElement, lyricsManager, lyricsContent);
} else {
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>';
}
}
const playerBar = document.querySelector('.now-playing-bar');
if (playerBar) playerBar.style.display = 'none';
if (sidePanelManager.isActive('lyrics') || sidePanelManager.isActive('queue')) {
sidePanelManager.close();
}
const mainContent = document.querySelector('.main-content');
if (mainContent instanceof HTMLElement) {
this.fullscreenMainContentOverflow = mainContent.style.overflowY;
mainContent.style.overflowY = 'hidden';
}
this.setupFullscreenControls();
overlay.style.display = 'flex';
if (fullscreenCoverNoRoundSettings.isEnabled()) {
@ -1275,75 +1295,41 @@ export class UIRenderer {
}
const coverImage = document.getElementById('fullscreen-cover-image');
if (fullscreenCoverVanillaTiltSettings.isEnabled() && coverImage && window.VanillaTilt) {
window.VanillaTilt.init(coverImage, {
max: fullscreenCoverTiltDistanceSettings.getValue(),
speed: fullscreenCoverTiltSpeedSettings.getValue(),
glare: true,
'max-glare': 0.3,
});
if (coverImage?.vanillaTilt) {
coverImage.vanillaTilt.destroy();
}
const startVisualizer = async () => {
if (!visualizerSettings.isEnabled()) {
if (this.visualizer) this.visualizer.stop();
return;
}
if (!this.visualizer && activeElement) {
const canvas = document.getElementById('visualizer-canvas');
if (canvas) {
this.visualizer = new Visualizer(canvas, activeElement);
await this.visualizer.initPresets();
}
}
if (this.visualizer) {
await this.visualizer.start();
}
// Add visualizer-active class for enhanced drop shadow
overlay.classList.add('visualizer-active');
};
// Setup UI toggle button
this.setupUIToggleButton(overlay);
if (localStorage.getItem('epilepsy-warning-dismissed') === 'true') {
await startVisualizer();
} else {
const modal = document.getElementById('epilepsy-warning-modal');
if (modal) {
modal.classList.add('active');
const acceptBtn = document.getElementById('epilepsy-accept-btn');
const cancelBtn = document.getElementById('epilepsy-cancel-btn');
acceptBtn.onclick = async () => {
modal.classList.remove('active');
localStorage.setItem('epilepsy-warning-dismissed', 'true');
await startVisualizer();
};
cancelBtn.onclick = () => {
modal.classList.remove('active');
this.closeFullscreenCover();
};
} else {
await startVisualizer();
}
}
this.setupControlsAutoHide(overlay);
await this.refreshFullscreenVisualizerState(activeElement);
}
closeFullscreenCover() {
const overlay = document.getElementById('fullscreen-cover-overlay');
const coverImage = document.getElementById('fullscreen-cover-image');
const lyricsContent = document.getElementById('fullscreen-lyrics-content');
if (coverImage && coverImage.vanillaTilt) {
coverImage.vanillaTilt.destroy();
}
if (lyricsContent) {
clearFullscreenLyricsSync(lyricsContent);
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');
overlay.classList.remove('visualizer-active', 'ui-hidden', 'fullscreen-cover-no-round', 'fullscreen-paused');
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) {
mainContent.style.overflowY = this.fullscreenMainContentOverflow;
} else {
mainContent.style.removeProperty('overflow-y');
}
this.fullscreenMainContentOverflow = null;
}
if (this.player?.currentTrack?.type === 'video') {
const coverContainer = document.querySelector('.now-playing-bar .track-info');
@ -1375,6 +1361,7 @@ export class UIRenderer {
if (this.visualizer) {
this.visualizer.stop();
}
this.fullscreenVisualizerSuppressed = false;
// Clear UI toggle button timers
if (this.uiToggleMouseTimer) {
@ -1383,13 +1370,115 @@ export class UIRenderer {
}
}
async startFullscreenVisualizer(activeElement, overlay) {
if (!activeElement) return;
if (!this.visualizer) {
const canvas = document.getElementById('visualizer-canvas');
if (canvas) {
this.visualizer = new Visualizer(canvas, activeElement);
await this.visualizer.initPresets();
}
}
if (this.visualizer) {
await this.visualizer.start();
overlay.classList.add('visualizer-active');
}
}
async ensureVisualizerPermission(activeElement, overlay, { closeOnCancel = false } = {}) {
if (localStorage.getItem('epilepsy-warning-dismissed') === 'true') {
await this.startFullscreenVisualizer(activeElement, overlay);
return true;
}
const modal = document.getElementById('epilepsy-warning-modal');
if (!modal) {
await this.startFullscreenVisualizer(activeElement, overlay);
return true;
}
return await new Promise((resolve) => {
modal.classList.add('active');
const acceptBtn = document.getElementById('epilepsy-accept-btn');
const cancelBtn = document.getElementById('epilepsy-cancel-btn');
acceptBtn.onclick = async () => {
modal.classList.remove('active');
localStorage.setItem('epilepsy-warning-dismissed', 'true');
await this.startFullscreenVisualizer(activeElement, overlay);
resolve(true);
};
cancelBtn.onclick = () => {
modal.classList.remove('active');
if (closeOnCancel) {
this.closeFullscreenCover();
}
resolve(false);
};
});
}
async refreshFullscreenVisualizerState(activeElement, { closeOnCancel = false } = {}) {
const overlay = document.getElementById('fullscreen-cover-overlay');
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;
if (!overlay) return;
if (visualizerBtn) {
visualizerBtn.style.display = isVideoTrack ? 'none' : 'flex';
visualizerBtn.classList.toggle('active', enabled);
visualizerBtn.title = enabled ? 'Disable Visualizer' : 'Use Visualizer';
}
if (!enabled) {
overlay.classList.remove('visualizer-active');
overlay.classList.remove('ui-hidden');
if (this.visualizer) {
this.visualizer.stop();
}
if (toggleBtn) {
toggleBtn.classList.remove('active', 'visible');
toggleBtn.title = 'Hide UI';
setFullscreenUIToggleIcon(toggleBtn, false);
}
return;
}
const allowed = await this.ensureVisualizerPermission(activeElement, overlay, { closeOnCancel });
if (!allowed) {
this.fullscreenVisualizerSuppressed = true;
overlay.classList.remove('visualizer-active');
if (this.visualizer) {
this.visualizer.stop();
}
if (visualizerBtn) {
visualizerBtn.classList.remove('active');
visualizerBtn.title = 'Use Visualizer';
}
}
}
setupUIToggleButton(overlay) {
const toggleBtn = document.getElementById('toggle-ui-btn');
if (!toggleBtn) return;
const updateToggleButtonIcon = () => {
const visualizerOnlyMode =
overlay.classList.contains('ui-hidden') && overlay.classList.contains('visualizer-active');
setFullscreenUIToggleIcon(toggleBtn, visualizerOnlyMode);
};
let isUIHidden = overlay.classList.contains('ui-hidden');
toggleBtn.classList.toggle('active', isUIHidden);
toggleBtn.title = isUIHidden ? 'Show UI' : 'Hide UI';
updateToggleButtonIcon();
// Show button
const showButton = () => {
@ -1408,12 +1497,39 @@ export class UIRenderer {
showButton();
}
const toggleUI = (e) => {
const toggleUI = async (e) => {
if (e) e.stopPropagation();
if (!overlay.classList.contains('visualizer-active')) {
const isVideoTrack = this.player?.currentTrack?.type === 'video';
if (isVideoTrack) {
overlay.classList.remove('ui-hidden');
isUIHidden = false;
toggleBtn.classList.remove('active');
toggleBtn.title = 'Hide UI';
updateToggleButtonIcon();
showButton();
return;
}
this.fullscreenVisualizerSuppressed = false;
visualizerSettings.setEnabled(true);
await this.refreshFullscreenVisualizerState(this.player?.activeElement);
if (!overlay.classList.contains('visualizer-active')) {
overlay.classList.remove('ui-hidden');
isUIHidden = false;
toggleBtn.classList.remove('active');
toggleBtn.title = 'Hide UI';
updateToggleButtonIcon();
showButton();
return;
}
}
isUIHidden = !isUIHidden;
overlay.classList.toggle('ui-hidden', isUIHidden);
toggleBtn.classList.toggle('active', isUIHidden);
toggleBtn.title = isUIHidden ? 'Show UI' : 'Hide UI';
updateToggleButtonIcon();
if (isUIHidden) {
hideButton();
@ -1458,12 +1574,21 @@ export class UIRenderer {
};
}
setupControlsAutoHide(overlay) {
if (this.controlsIdleCleanup) this.controlsIdleCleanup();
overlay.classList.remove('controls-idle');
this.controlsIdleCleanup = () => {
overlay.classList.remove('controls-idle');
};
}
setupFullscreenControls() {
const playBtn = document.getElementById('fs-play-pause-btn');
const prevBtn = document.getElementById('fs-prev-btn');
const nextBtn = document.getElementById('fs-next-btn');
const shuffleBtn = document.getElementById('fs-shuffle-btn');
const repeatBtn = document.getElementById('fs-repeat-btn');
const visualizerBtn = document.getElementById('fs-visualizer-btn');
const progressBar = document.getElementById('fs-progress-bar');
const progressFill = document.getElementById('fs-progress-fill');
const currentTimeEl = document.getElementById('fs-current-time');
@ -1524,6 +1649,22 @@ 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);
}
await this.refreshFullscreenVisualizerState(this.player.activeElement);
};
}
// Progress bar with drag support
let isFsSeeking = false;
let wasFsPlaying = false;

View file

@ -970,6 +970,7 @@ ul {
display: grid;
height: 100vh;
height: 100dvh;
min-height: 0;
grid-template:
'sidebar main' 1fr
'player player' auto / 210px 1fr;
@ -977,6 +978,7 @@ ul {
.sidebar {
grid-area: sidebar;
min-height: 0;
background-color: var(--background);
border-right: 1px solid var(--border);
padding: 1.25rem;
@ -1023,7 +1025,10 @@ ul {
.main-content {
grid-area: main;
min-height: 0;
min-width: 0;
overflow-y: auto;
overflow-x: hidden;
padding: var(--spacing-xl);
scroll-behavior: smooth;
position: relative;
@ -3934,7 +3939,26 @@ input:checked + .slider::before {
filter: var(--cover-filter);
z-index: -1;
background-image: var(--bg-image);
transition: background-image var(--transition);
transition:
background-image var(--transition),
filter 0.65s ease,
opacity 0.65s ease;
}
#fullscreen-cover-overlay::after {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(circle at 20% 22%, rgb(var(--highlight-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;
pointer-events: none;
z-index: 0;
transition:
opacity 0.65s ease,
background 0.65s ease;
}
#visualizer-container {
@ -3944,7 +3968,13 @@ input:checked + .slider::before {
height: 100%;
z-index: 0;
pointer-events: none;
transition: opacity 0.3s ease;
filter: blur(14px) saturate(0.84) brightness(0.8);
transform: scale(1.04);
opacity: 0.82;
transition:
opacity 0.65s ease,
filter 0.65s ease,
transform 0.65s ease;
}
#visualizer-canvas {
@ -3963,6 +3993,7 @@ input:checked + .slider::before {
height: 100%;
position: relative;
padding: 1rem;
overflow: hidden;
}
/* UI Toggle Button for Visualizer Mode - Rightmost position */
@ -4079,11 +4110,27 @@ input:checked + .slider::before {
/* When UI is hidden, only toggle button stays visible at right edge (when .visible class is added) */
#fullscreen-cover-overlay.ui-hidden .fullscreen-lyrics-toggle,
#fullscreen-cover-overlay.ui-hidden #close-fullscreen-cover-btn {
#fullscreen-cover-overlay.ui-hidden .fullscreen-top-actions {
opacity: 0;
pointer-events: none;
}
#fullscreen-cover-overlay.ui-hidden::before,
#fullscreen-cover-overlay.ui-hidden::after {
opacity: 0;
}
#fullscreen-cover-overlay.ui-hidden #visualizer-container {
filter: none;
transform: none;
opacity: 1;
}
body:has(#fullscreen-cover-overlay.ui-hidden.inline-lyrics) #side-panel[data-view='lyrics'] {
opacity: 0;
pointer-events: none;
transition: opacity 0.5s ease;
}
#fullscreen-cover-overlay:not(.ui-hidden) .fullscreen-main-view,
#fullscreen-cover-overlay:not(.ui-hidden) .fullscreen-controls,
#fullscreen-cover-overlay:not(.ui-hidden) #fullscreen-next-track {
@ -4092,6 +4139,59 @@ input:checked + .slider::before {
transition: opacity 0.5s ease;
}
/* Auto-hide controls on idle */
#fullscreen-cover-overlay.controls-idle .fullscreen-track-info,
#fullscreen-cover-overlay.controls-idle .fullscreen-controls,
#fullscreen-cover-overlay.controls-idle #fullscreen-next-track,
#fullscreen-cover-overlay.controls-idle #toggle-ui-btn,
#fullscreen-cover-overlay.controls-idle .fullscreen-lyrics-toggle,
#fullscreen-cover-overlay.controls-idle .fullscreen-top-actions {
opacity: 0;
pointer-events: none;
transition:
opacity 0.6s ease,
transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
#fullscreen-cover-overlay.controls-idle #fullscreen-cover-image {
transform: translateY(4rem);
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
#fullscreen-cover-overlay:not(.controls-idle) #fullscreen-cover-image {
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
#fullscreen-cover-overlay.controls-idle .fullscreen-controls {
transform: translateY(1.5rem);
}
#fullscreen-cover-overlay.controls-idle .fullscreen-track-info {
transform: translateY(0.5rem);
}
#fullscreen-cover-overlay.controls-idle #toggle-ui-btn,
#fullscreen-cover-overlay.controls-idle .fullscreen-lyrics-toggle,
#fullscreen-cover-overlay.controls-idle .fullscreen-top-actions {
transform: translateY(-0.5rem);
}
#fullscreen-cover-overlay:not(.controls-idle) .fullscreen-track-info,
#fullscreen-cover-overlay:not(.controls-idle) .fullscreen-controls,
#fullscreen-cover-overlay:not(.controls-idle) #fullscreen-next-track,
#fullscreen-cover-overlay:not(.controls-idle) #toggle-ui-btn,
#fullscreen-cover-overlay:not(.controls-idle) .fullscreen-lyrics-toggle,
#fullscreen-cover-overlay:not(.controls-idle) .fullscreen-top-actions {
opacity: 1;
transform: translateY(0);
transition:
opacity 0.4s ease,
transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
#fullscreen-cover-overlay.controls-idle {
cursor: none;
}
#fullscreen-cover-image {
max-width: 55vw;
max-height: 45vh;
@ -4946,7 +5046,7 @@ input:checked + .slider::before {
#download-notifications {
position: fixed;
bottom: 120px;
bottom: calc(max(env(safe-area-inset-bottom), 0px) + 12px);
right: 20px;
z-index: 20000;
max-width: 350px;
@ -6774,7 +6874,7 @@ img[src=''] {
}
#download-notifications {
bottom: 10px;
bottom: calc(max(env(safe-area-inset-bottom), 0px) + 10px);
right: 10px;
left: 10px;
max-width: none;
@ -9936,3 +10036,514 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
.contrib {
font-size: 10px;
}
/* Fullscreen layout rebuild on PR 378 base */
#fullscreen-cover-overlay .fullscreen-shell {
width: 100%;
height: 100%;
display: flex;
align-items: stretch;
justify-content: center;
min-height: 0;
overflow: hidden;
}
#fullscreen-cover-overlay .fullscreen-main-view {
width: min(1240px, 100%);
height: 100%;
flex: 1;
display: grid;
grid-template-columns: minmax(360px, 430px) minmax(420px, 1fr);
gap: clamp(1.5rem, 3vw, 3rem);
align-items: center;
justify-content: center;
padding: clamp(4rem, 7vh, 5rem) clamp(2rem, 4vw, 3rem) clamp(3rem, 6vh, 4rem) clamp(4rem, 7vw, 6.25rem);
position: relative;
z-index: 1;
min-height: 0;
overflow: hidden;
}
#fullscreen-cover-overlay .fullscreen-media-column,
#fullscreen-cover-overlay .fullscreen-lyrics-pane {
min-height: 0;
}
#fullscreen-cover-overlay .fullscreen-media-column {
width: min(420px, 100%);
display: flex;
flex-direction: column;
gap: 0.95rem;
justify-self: center;
transform: translateX(clamp(0.75rem, 1.2vw, 1.4rem));
}
#fullscreen-cover-overlay .fullscreen-artwork-card {
width: min(420px, 100%);
aspect-ratio: 1 / 1;
border-radius: 18px;
overflow: hidden;
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.26);
}
#fullscreen-cover-overlay #fullscreen-cover-image {
width: 100%;
height: 100%;
margin: 0;
max-width: none;
max-height: none;
object-fit: cover;
border-radius: 18px;
transform: none !important;
}
#fullscreen-cover-overlay .fullscreen-track-info {
width: min(420px, 100%);
display: block;
text-align: left;
max-width: none;
padding: 0.15rem 0 0;
background: none;
border: 0;
box-shadow: none;
backdrop-filter: none;
}
#fullscreen-cover-overlay .fullscreen-track-text {
min-width: 0;
}
#fullscreen-cover-overlay #fullscreen-track-title {
margin: 0;
font-size: clamp(1.15rem, 1.5vw, 1.42rem);
line-height: 1.08;
letter-spacing: -0.03em;
}
#fullscreen-cover-overlay #fullscreen-track-artist {
margin: 0.12rem 0 0;
font-size: 0.94rem;
color: rgb(255 255 255 / 0.74);
}
#fullscreen-cover-overlay #toggle-fullscreen-lyrics-btn,
#fullscreen-cover-overlay .fullscreen-lyrics-toggle {
display: none !important;
}
#fullscreen-cover-overlay .fullscreen-actions {
display: flex !important;
align-items: center;
gap: 0.5rem;
margin-top: 0.9rem;
}
#fullscreen-cover-overlay .fullscreen-actions .btn-icon {
width: 38px;
height: 38px;
padding: 0;
border-radius: 999px;
color: rgb(255 255 255 / 0.74);
background: transparent;
transition:
color 0.2s ease,
background-color 0.2s ease,
transform 0.2s ease;
}
#fullscreen-cover-overlay .fullscreen-actions .btn-icon:hover {
color: rgb(255 255 255 / 0.96);
background: rgb(255 255 255 / 0.08);
transform: scale(1.03);
}
#fullscreen-cover-overlay #fullscreen-next-track {
display: flex;
align-items: center;
gap: 0.45rem;
margin-top: 0.85rem;
color: rgb(255 255 255 / 0.56);
}
#fullscreen-cover-overlay #fullscreen-next-track .label {
font-size: 0.72rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
#fullscreen-cover-overlay #fullscreen-next-track .value {
font-size: 0.84rem;
color: rgb(255 255 255 / 0.74);
}
#fullscreen-cover-overlay .fullscreen-top-actions {
position: absolute;
top: 1.25rem;
left: calc(1.5rem + env(safe-area-inset-left));
right: auto;
display: flex;
align-items: center;
gap: 0.4rem;
z-index: 12;
}
#fullscreen-cover-overlay .fullscreen-top-actions button {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 999px;
padding: 0;
background: rgb(9 12 18 / 0.34);
color: rgb(255 255 255 / 0.72);
backdrop-filter: blur(10px);
transition:
color 0.2s ease,
background-color 0.2s ease,
opacity 0.2s ease;
}
#fullscreen-cover-overlay .fullscreen-top-actions #fs-visualizer-btn {
order: 2;
}
#fullscreen-cover-overlay .fullscreen-top-actions #fs-visualizer-btn,
#fullscreen-cover-overlay .fullscreen-top-actions #close-fullscreen-cover-btn {
position: static;
top: auto;
right: auto;
left: auto;
bottom: auto;
margin: 0;
opacity: 1;
}
#fullscreen-cover-overlay .fullscreen-top-actions #close-fullscreen-cover-btn {
order: 1;
}
#fullscreen-cover-overlay #toggle-ui-btn {
top: 1.25rem;
left: calc(80px + 2.3rem + env(safe-area-inset-left));
right: auto;
width: 40px;
height: 40px;
background: rgb(9 12 18 / 0.34);
color: rgb(255 255 255 / 0.72);
backdrop-filter: blur(10px);
opacity: 1;
z-index: 12;
}
#fullscreen-cover-overlay .fullscreen-controls {
width: min(420px, 100%);
margin-top: 0;
align-items: center;
gap: 0.85rem;
position: relative;
}
#fullscreen-cover-overlay .fullscreen-buttons {
width: 100%;
justify-content: center;
gap: 0.4rem;
}
#fullscreen-cover-overlay .fullscreen-buttons button {
width: 40px;
height: 40px;
color: rgb(255 255 255 / 0.72);
border-radius: 999px;
padding: 0;
transition:
color 0.2s ease,
transform 0.2s ease,
background-color 0.2s ease,
opacity 0.2s ease;
}
#fullscreen-cover-overlay .fullscreen-buttons button:hover {
color: rgb(255 255 255 / 0.94);
background: rgb(255 255 255 / 0.08);
transform: scale(1.04);
}
#fullscreen-cover-overlay .fullscreen-buttons button.active {
color: rgb(var(--highlight-rgb) / 0.98);
}
#fullscreen-cover-overlay .fullscreen-buttons #fs-play-pause-btn {
width: 54px;
height: 54px;
background: rgb(255 255 255 / 0.96);
color: rgb(11 15 21 / 0.92);
box-shadow: 0 12px 28px rgb(0 0 0 / 0.2);
}
#fullscreen-cover-overlay .fullscreen-buttons #fs-play-pause-btn:hover {
background: rgb(255 255 255 / 1);
transform: scale(1.02);
}
#fullscreen-cover-overlay .fullscreen-volume-container {
width: 238px;
max-width: 100%;
align-self: center;
justify-content: center;
margin-top: 0.2rem;
margin-inline: auto;
position: relative;
}
#fullscreen-cover-overlay .fs-visualizer-btn,
#fullscreen-cover-overlay .fs-volume-btn {
width: 30px;
height: 30px;
padding: 0;
color: rgb(255 255 255 / 0.62);
}
#fullscreen-cover-overlay .fs-visualizer-btn:hover,
#fullscreen-cover-overlay .fs-volume-btn:hover {
background: transparent;
color: rgb(255 255 255 / 0.9);
}
#fullscreen-cover-overlay .fs-visualizer-btn.active {
color: rgb(var(--highlight-rgb) / 0.96);
}
#fullscreen-cover-overlay .fs-volume-btn {
position: absolute;
left: -2.5rem;
top: 50%;
transform: translateY(-50%);
}
#fullscreen-cover-overlay .fs-volume-btn:hover {
transform: translateY(-50%);
}
#fullscreen-cover-overlay .fs-volume-bar {
width: 238px;
height: 4px;
background: rgb(255 255 255 / 0.24);
margin-inline: auto;
}
#fullscreen-cover-overlay .fs-volume-bar:hover {
height: 4px;
}
#fullscreen-cover-overlay .fs-volume-fill,
#fullscreen-cover-overlay .fullscreen-progress-container .progress-fill {
background: rgb(255 255 255 / 0.92);
}
#fullscreen-cover-overlay .fullscreen-progress-container {
color: rgb(255 255 255 / 0.62);
font-size: 0.78rem;
}
#fullscreen-cover-overlay .fullscreen-progress-container .progress-bar {
height: 4px;
background: rgb(255 255 255 / 0.2);
}
#fullscreen-cover-overlay .fullscreen-progress-container .progress-bar:hover {
height: 4px;
}
#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);
}
#fullscreen-cover-overlay .fullscreen-progress-container .progress-bar:hover .progress-fill::after,
#fullscreen-cover-overlay .fullscreen-progress-container .progress-bar:active .progress-fill::after,
#fullscreen-cover-overlay .fs-volume-bar:hover .fs-volume-fill::after,
#fullscreen-cover-overlay .fs-volume-bar:active .fs-volume-fill::after {
width: 10px;
height: 10px;
box-shadow: 0 4px 12px rgb(0 0 0 / 0.28);
}
#fullscreen-cover-overlay .fullscreen-lyrics-pane {
display: flex;
align-items: stretch;
justify-content: flex-start;
overflow: hidden;
}
#fullscreen-cover-overlay .fullscreen-lyrics-shell,
#fullscreen-cover-overlay .fullscreen-lyrics-content,
#fullscreen-cover-overlay .fullscreen-lyrics-content am-lyrics {
background: transparent !important;
border: 0 !important;
box-shadow: none !important;
outline: none !important;
backdrop-filter: none !important;
}
#fullscreen-cover-overlay .fullscreen-lyrics-shell {
width: min(860px, 100%);
min-height: 0;
margin-left: clamp(4rem, 8vw, 8rem);
}
#fullscreen-cover-overlay .fullscreen-lyrics-content {
min-height: 0;
height: 100%;
position: relative;
padding-left: clamp(2.5rem, 4vw, 4rem);
mask-image: none;
overflow: visible;
scrollbar-width: none;
-ms-overflow-style: none;
}
#fullscreen-cover-overlay .fullscreen-lyrics-content::-webkit-scrollbar {
display: none;
}
#fullscreen-cover-overlay .fullscreen-lyrics-content am-lyrics {
--am-lyrics-highlight-color: #f6f4ef;
--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;
--lyplus-font-size-base: clamp(34px, 3vw, 52px);
--lyplus-padding-line: 8px;
--lyplus-text-color: rgba(246, 244, 239, 0.08);
--lyplus-active-color: #f6f4ef;
line-height: 1.32;
letter-spacing: -0.04em;
font-weight: 600;
isolation: isolate;
}
#fullscreen-cover-overlay .fullscreen-lyrics-content::after {
content: none;
}
#fullscreen-cover-overlay .fullscreen-lyrics-empty,
#fullscreen-cover-overlay .fullscreen-lyrics-content .lyrics-loading,
#fullscreen-cover-overlay .fullscreen-lyrics-content .lyrics-error {
padding: clamp(5rem, 14vh, 7rem) 0 0 clamp(2rem, 5vw, 4.5rem);
background: none;
border: 0;
}
#fullscreen-cover-overlay.lyrics-unavailable .fullscreen-lyrics-pane {
opacity: 0.55;
}
@media (max-width: 980px) {
#fullscreen-cover-overlay .fullscreen-main-view {
grid-template-columns: 1fr;
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))
clamp(1rem, 4vw, 1.5rem);
}
#fullscreen-cover-overlay .fullscreen-media-column {
justify-self: center;
transform: none;
}
#fullscreen-cover-overlay .fullscreen-lyrics-pane {
display: none;
}
}
@media (max-width: 768px) {
#fullscreen-cover-overlay .fullscreen-cover-content {
padding: 0.75rem 0.75rem calc(0.75rem + env(safe-area-inset-bottom));
}
#fullscreen-cover-overlay .fullscreen-top-actions {
top: calc(0.75rem + env(safe-area-inset-top));
left: calc(1rem + env(safe-area-inset-left));
gap: 0.35rem;
}
#fullscreen-cover-overlay .fullscreen-top-actions button,
#fullscreen-cover-overlay #toggle-ui-btn {
width: 44px;
height: 44px;
background: rgb(9 12 18 / 0.5);
}
#fullscreen-cover-overlay #toggle-ui-btn {
top: calc(0.75rem + env(safe-area-inset-top));
left: calc(88px + 1.25rem + env(safe-area-inset-left));
}
#fullscreen-cover-overlay .fullscreen-main-view {
width: 100%;
gap: 0.85rem;
padding:
calc(4.25rem + env(safe-area-inset-top))
0.75rem
calc(1.5rem + env(safe-area-inset-bottom))
0.75rem;
}
#fullscreen-cover-overlay .fullscreen-track-info,
#fullscreen-cover-overlay .fullscreen-controls,
#fullscreen-cover-overlay .fullscreen-media-column {
width: min(100%, 460px);
}
#fullscreen-cover-overlay .fullscreen-actions {
width: 100%;
flex-wrap: wrap;
gap: 0.45rem;
}
#fullscreen-cover-overlay .fullscreen-actions .btn-icon {
background: rgb(255 255 255 / 0.06);
}
#fullscreen-cover-overlay .fullscreen-progress-container {
gap: 0.65rem;
}
#fullscreen-cover-overlay .fullscreen-buttons {
gap: 0.2rem;
}
#fullscreen-cover-overlay .fullscreen-buttons button {
width: 38px;
height: 38px;
}
#fullscreen-cover-overlay .fullscreen-buttons #fs-play-pause-btn {
width: 52px;
height: 52px;
}
#fullscreen-cover-overlay .fullscreen-volume-container {
width: min(220px, calc(100% - 2.75rem));
}
#fullscreen-cover-overlay .fs-volume-btn {
left: -2.25rem;
}
#fullscreen-cover-overlay .fs-volume-bar {
width: 100%;
}
}