From 0b1bb3cd119f68ba9ac0dfd6d16a61d2ec395b71 Mon Sep 17 00:00:00 2001 From: Alan Brooks Date: Sat, 4 Apr 2026 23:48:20 -0400 Subject: [PATCH] Refine fullscreen player to look more like apple music --- index.html | 156 +++++++------ js/icons.ts | 2 + js/lyrics.js | 69 ++++++ js/ui.js | 313 +++++++++++++++++++------- styles.css | 621 ++++++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 1003 insertions(+), 158 deletions(-) diff --git a/index.html b/index.html index 8ba694e..2898360 100644 --- a/index.html +++ b/index.html @@ -144,77 +144,99 @@ - -
- Album Cover +
+ + +
+
+
+
+
+ Album Cover +
-
-

-

-
- - - - - -
- -
-
-
- 0:00 -
-
+
+
+

+

+
+
+ + + + + +
+
- 0:00 -
-
- - - - - - - -
-
- -
-
+ +
+
+ 0:00 +
+
+
+ 0:00 +
+
+ + + + + + + +
+
+ +
+
+
+
+ +
diff --git a/js/icons.ts b/js/icons.ts index 39ebf20..a2ed6ec 100644 --- a/js/icons.ts +++ b/js/icons.ts @@ -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'; diff --git a/js/lyrics.js b/js/lyrics.js index e59ffae..62f245e 100644 --- a/js/lyrics.js +++ b/js/lyrics.js @@ -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 = '
Loading lyrics...
'; @@ -1006,6 +1074,7 @@ async function renderLyricsComponent(container, track, audioPlayer, lyricsManage amLyrics.style.width = '100%'; container.appendChild(amLyrics); + applyFullscreenLyricsShadowTweaks(amLyrics, container); lyricsManager.setupLyricsObserver(amLyrics); diff --git a/js/ui.js b/js/ui.js index 8019611..8f4c3c6 100644 --- a/js/ui.js +++ b/js/ui.js @@ -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 = '
Lyrics are not available for this track.
'; + } } 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 = '
Lyrics appear here.
'; + } 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; diff --git a/styles.css b/styles.css index 2d3d2e4..c8b20e5 100644 --- a/styles.css +++ b/styles.css @@ -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%; + } +}