From 0b1bb3cd119f68ba9ac0dfd6d16a61d2ec395b71 Mon Sep 17 00:00:00 2001 From: Alan Brooks Date: Sat, 4 Apr 2026 23:48:20 -0400 Subject: [PATCH 1/9] 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%; + } +} From 759d9b75e29ff9a08e73dd90b9d7449a66589f29 Mon Sep 17 00:00:00 2001 From: Alan Brooks Date: Sun, 5 Apr 2026 00:29:58 -0400 Subject: [PATCH 2/9] fix: buttons when in visualizer only mode --- js/ui.js | 25 ++++++++----------------- styles.css | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/js/ui.js b/js/ui.js index 8f4c3c6..0e12d30 100644 --- a/js/ui.js +++ b/js/ui.js @@ -1532,29 +1532,20 @@ export class UIRenderer { updateToggleButtonIcon(); if (isUIHidden) { - hideButton(); + showButton(); } else { showButton(); } }; - // Mouse move handler const handleMouseMove = (e) => { - const rect = overlay.getBoundingClientRect(); - const isNearTopRight = e.clientY < 100 && e.clientX > rect.width - 150; - - if (isUIHidden) { - if (overlay.classList.contains('is-video-mode')) { - if (isNearTopRight) { - showButton(); - } else { - hideButton(); - } - } else if (isNearTopRight) { - showButton(); - } else { - hideButton(); - } + if (!isUIHidden) return; + const btnRect = toggleBtn.getBoundingClientRect(); + const nearBtn = e.clientY < 100 && Math.abs(e.clientX - (btnRect.left + btnRect.width / 2)) < 150; + if (nearBtn) { + showButton(); + } else { + hideButton(); } }; diff --git a/styles.css b/styles.css index c8b20e5..b81d210 100644 --- a/styles.css +++ b/styles.css @@ -10224,6 +10224,20 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { order: 1; } +#fullscreen-cover-overlay.ui-hidden .fullscreen-top-actions { + opacity: 1; + pointer-events: auto; + transform: none; +} + +#fullscreen-cover-overlay.ui-hidden .fullscreen-top-actions button, +#fullscreen-cover-overlay.ui-hidden .fullscreen-top-actions #close-fullscreen-cover-btn, +#fullscreen-cover-overlay.ui-hidden .fullscreen-top-actions #fs-visualizer-btn { + opacity: 1; + pointer-events: auto; + transform: none; +} + #fullscreen-cover-overlay #toggle-ui-btn { top: 1.25rem; left: calc(80px + 2.3rem + env(safe-area-inset-left)); From e00368597df26227b3ae83df055af1d27f16c9bf Mon Sep 17 00:00:00 2001 From: Alan Brooks Date: Sun, 5 Apr 2026 00:37:43 -0400 Subject: [PATCH 3/9] fix: mobile sizing --- js/side-panel.js | 13 +++++++++++++ js/ui.js | 31 +++++++++++++++++++++++++++++++ styles.css | 36 ++++++++++++++++++++++++++++++------ 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/js/side-panel.js b/js/side-panel.js index 0f399ea..5da3b4a 100644 --- a/js/side-panel.js +++ b/js/side-panel.js @@ -15,6 +15,17 @@ export class SidePanelManager { } } + emitChange() { + window.dispatchEvent( + new CustomEvent('side-panel-changed', { + detail: { + active: this.panel.classList.contains('active'), + view: this.currentView, + }, + }) + ); + } + initResizer() { this.resizerElement.addEventListener('mousedown', this.startResize.bind(this)); @@ -86,6 +97,7 @@ export class SidePanelManager { if (renderContentCallback) renderContentCallback(this.contentElement); this.panel.classList.add('active'); + this.emitChange(); } close() { @@ -105,6 +117,7 @@ export class SidePanelManager { this.panel.classList.remove('active'); this.currentView = null; + this.emitChange(); // Optionally clear content after transition setTimeout(() => { if (!this.panel.classList.contains('active')) { diff --git a/js/ui.js b/js/ui.js index 0e12d30..5827d7f 100644 --- a/js/ui.js +++ b/js/ui.js @@ -1302,6 +1302,7 @@ export class UIRenderer { // Setup UI toggle button this.setupUIToggleButton(overlay); this.setupControlsAutoHide(overlay); + this.setupFullscreenSidePanelSync(overlay); await this.refreshFullscreenVisualizerState(activeElement); } @@ -1368,6 +1369,16 @@ export class UIRenderer { clearTimeout(this.uiToggleMouseTimer); this.uiToggleMouseTimer = null; } + + if (this.controlsIdleCleanup) { + this.controlsIdleCleanup(); + this.controlsIdleCleanup = null; + } + + if (this.fullscreenSidePanelSyncCleanup) { + this.fullscreenSidePanelSyncCleanup(); + this.fullscreenSidePanelSyncCleanup = null; + } } async startFullscreenVisualizer(activeElement, overlay) { @@ -1573,6 +1584,26 @@ export class UIRenderer { overlay.classList.remove('controls-idle'); }; } + + setupFullscreenSidePanelSync(overlay) { + if (this.fullscreenSidePanelSyncCleanup) { + this.fullscreenSidePanelSyncCleanup(); + } + + const syncState = () => { + overlay.classList.toggle('queue-panel-active', sidePanelManager.isActive('queue')); + }; + + const handleChange = () => syncState(); + window.addEventListener('side-panel-changed', handleChange); + syncState(); + + this.fullscreenSidePanelSyncCleanup = () => { + window.removeEventListener('side-panel-changed', handleChange); + overlay.classList.remove('queue-panel-active'); + }; + } + setupFullscreenControls() { const playBtn = document.getElementById('fs-play-pause-btn'); const prevBtn = document.getElementById('fs-prev-btn'); diff --git a/styles.css b/styles.css index b81d210..57d66e9 100644 --- a/styles.css +++ b/styles.css @@ -10459,6 +10459,20 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { opacity: 0.55; } +#fullscreen-cover-overlay.queue-panel-active .fullscreen-main-view { + grid-template-columns: 1fr; + width: min(760px, 100%); +} + +#fullscreen-cover-overlay.queue-panel-active .fullscreen-media-column { + justify-self: center; + transform: none; +} + +#fullscreen-cover-overlay.queue-panel-active .fullscreen-lyrics-pane { + display: none; +} + @media (max-width: 980px) { #fullscreen-cover-overlay .fullscreen-main-view { grid-template-columns: 1fr; @@ -10483,33 +10497,43 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { } @media (max-width: 768px) { + #fullscreen-cover-overlay { + --fs-mobile-top-btn-size: 44px; + --fs-mobile-top-btn-gap: 0.6rem; + --fs-mobile-top-btn-left: calc(1rem + env(safe-area-inset-left)); + } + #fullscreen-cover-overlay .fullscreen-cover-content { padding: 0.75rem 0.75rem calc(0.75rem + env(safe-area-inset-bottom)); } #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; + left: var(--fs-mobile-top-btn-left); + gap: var(--fs-mobile-top-btn-gap); } #fullscreen-cover-overlay .fullscreen-top-actions button, #fullscreen-cover-overlay #toggle-ui-btn { - width: 44px; - height: 44px; + width: var(--fs-mobile-top-btn-size); + height: var(--fs-mobile-top-btn-size); 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)); + left: calc( + var(--fs-mobile-top-btn-left) + + (var(--fs-mobile-top-btn-size) * 2) + + (var(--fs-mobile-top-btn-gap) * 2) + ); } #fullscreen-cover-overlay .fullscreen-main-view { width: 100%; gap: 0.85rem; padding: - calc(4.25rem + env(safe-area-inset-top)) + calc(7.25rem + env(safe-area-inset-top)) 0.75rem calc(1.5rem + env(safe-area-inset-bottom)) 0.75rem; From 02021691ca822266612fe6c5334b854acd2ad36f Mon Sep 17 00:00:00 2001 From: a <252674497+ap5z@users.noreply.github.com> Date: Sun, 5 Apr 2026 09:12:35 -0400 Subject: [PATCH 4/9] Update styles.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- styles.css | 5 ----- 1 file changed, 5 deletions(-) diff --git a/styles.css b/styles.css index 57d66e9..d9d2a76 100644 --- a/styles.css +++ b/styles.css @@ -4126,11 +4126,6 @@ input:checked + .slider::before { 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 { From 476648ff8a33312fbcfc34ce5acdd1a9f0126a54 Mon Sep 17 00:00:00 2001 From: a <252674497+ap5z@users.noreply.github.com> Date: Sun, 5 Apr 2026 09:13:22 -0400 Subject: [PATCH 5/9] Update js/ui.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- js/ui.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/js/ui.js b/js/ui.js index 5827d7f..b1741fd 100644 --- a/js/ui.js +++ b/js/ui.js @@ -155,8 +155,6 @@ 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', () => { From 2573870004a3e131ca79fb32056c00231746c0db Mon Sep 17 00:00:00 2001 From: Alan Brooks Date: Sun, 5 Apr 2026 22:37:10 -0400 Subject: [PATCH 6/9] feat: refine fullscreen apple player --- functions/album/[id].js | 45 +---- functions/artist/[id].js | 43 +---- functions/playlist/[id].js | 44 +---- functions/track/[id].js | 43 +---- index.html | 7 +- js/lyrics.js | 9 + js/storage.js | 177 +----------------- js/ui.js | 260 ++++++++++++++++++++++++-- js/visualizer.js | 12 ++ public/instances.json | 21 +-- styles.css | 363 +++++++++++++++++++++++++++++-------- 11 files changed, 571 insertions(+), 453 deletions(-) diff --git a/functions/album/[id].js b/functions/album/[id].js index b75c0bf..1b002f5 100644 --- a/functions/album/[id].js +++ b/functions/album/[id].js @@ -47,52 +47,11 @@ class TidalAPI { class ServerAPI { constructor() { - this.INSTANCES_URLS = [ - 'https://tidal-uptime.jiffy-puffs-1j.workers.dev/', - 'https://tidal-uptime.props-76styles.workers.dev/', - ]; - this.apiInstances = null; + this.apiInstances = ['https://hifi.geeked.wtf']; } async getInstances() { - if (this.apiInstances) return this.apiInstances; - - let data = null; - const urls = [...this.INSTANCES_URLS].sort(() => Math.random() - 0.5); - - for (const url of urls) { - try { - const response = await fetch(url); - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - data = await response.json(); - break; - } catch (error) { - console.warn(`Failed to fetch from ${url}:`, error); - } - } - - if (data) { - this.apiInstances = (data.api || []) - .map((item) => item.url || item) - .filter((url) => !/\.squid\.wtf/i.test(url)); - return this.apiInstances; - } - - console.error('Failed to load instances from all uptime APIs'); - return [ - 'https://hifi.geeked.wtf', - 'https://eu-central.monochrome.tf', - 'https://us-west.monochrome.tf', - 'https://arran.monochrome.tf', - 'https://api.monochrome.tf', - 'https://monochrome-api.samidy.com', - 'https://maus.qqdl.site', - 'https://vogel.qqdl.site', - 'https://katze.qqdl.site', - 'https://hund.qqdl.site', - 'https://tidal.kinoplus.online', - 'https://wolf.qqdl.site', - ]; + return this.apiInstances; } async fetchWithRetry(relativePath) { diff --git a/functions/artist/[id].js b/functions/artist/[id].js index 1c62591..a924773 100644 --- a/functions/artist/[id].js +++ b/functions/artist/[id].js @@ -47,50 +47,11 @@ class TidalAPI { class ServerAPI { constructor() { - this.INSTANCES_URLS = [ - 'https://tidal-uptime.jiffy-puffs-1j.workers.dev/', - 'https://tidal-uptime.props-76styles.workers.dev/', - ]; - this.apiInstances = null; + this.apiInstances = ['https://hifi.geeked.wtf']; } async getInstances() { - if (this.apiInstances) return this.apiInstances; - - let data = null; - const urls = [...this.INSTANCES_URLS].sort(() => Math.random() - 0.5); - - for (const url of urls) { - try { - const response = await fetch(url); - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - data = await response.json(); - break; - } catch (error) { - console.warn(`Failed to fetch from ${url}:`, error); - } - } - - if (data) { - this.apiInstances = (data.api || []).map((item) => item.url || item); - return this.apiInstances; - } - - console.error('Failed to load instances from all uptime APIs'); - return [ - 'https://eu-central.monochrome.tf', - 'https://us-west.monochrome.tf', - 'https://arran.monochrome.tf', - 'https://triton.squid.wtf', - 'https://api.monochrome.tf', - 'https://monochrome-api.samidy.com', - 'https://maus.qqdl.site', - 'https://vogel.qqdl.site', - 'https://katze.qqdl.site', - 'https://hund.qqdl.site', - 'https://tidal.kinoplus.online', - 'https://wolf.qqdl.site', - ]; + return this.apiInstances; } async fetchWithRetry(relativePath) { diff --git a/functions/playlist/[id].js b/functions/playlist/[id].js index 15fe6f6..4e1dd21 100644 --- a/functions/playlist/[id].js +++ b/functions/playlist/[id].js @@ -47,51 +47,11 @@ class TidalAPI { class ServerAPI { constructor() { - this.INSTANCES_URLS = [ - 'https://tidal-uptime.jiffy-puffs-1j.workers.dev/', - 'https://tidal-uptime.props-76styles.workers.dev/', - ]; - this.apiInstances = null; + this.apiInstances = ['https://hifi.geeked.wtf']; } async getInstances() { - if (this.apiInstances) return this.apiInstances; - - let data = null; - const urls = [...this.INSTANCES_URLS].sort(() => Math.random() - 0.5); - - for (const url of urls) { - try { - const response = await fetch(url); - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - data = await response.json(); - break; - } catch (error) { - console.warn(`Failed to fetch from ${url}:`, error); - } - } - - if (data) { - this.apiInstances = (data.api || []) - .map((item) => item.url || item) - .filter((url) => !/\.squid\.wtf/i.test(url)); - return this.apiInstances; - } - - console.error('Failed to load instances from all uptime APIs'); - return [ - 'https://eu-central.monochrome.tf', - 'https://us-west.monochrome.tf', - 'https://arran.monochrome.tf', - 'https://api.monochrome.tf', - 'https://monochrome-api.samidy.com', - 'https://maus.qqdl.site', - 'https://vogel.qqdl.site', - 'https://katze.qqdl.site', - 'https://hund.qqdl.site', - 'https://tidal.kinoplus.online', - 'https://wolf.qqdl.site', - ]; + return this.apiInstances; } async fetchWithRetry(relativePath) { diff --git a/functions/track/[id].js b/functions/track/[id].js index cf50d44..a274b12 100644 --- a/functions/track/[id].js +++ b/functions/track/[id].js @@ -69,50 +69,11 @@ class TidalAPI { class ServerAPI { constructor() { - this.INSTANCES_URLS = [ - 'https://tidal-uptime.jiffy-puffs-1j.workers.dev/', - 'https://tidal-uptime.props-76styles.workers.dev/', - ]; - this.apiInstances = null; + this.apiInstances = ['https://hifi.geeked.wtf']; } async getInstances() { - if (this.apiInstances) return this.apiInstances; - - let data = null; - const urls = [...this.INSTANCES_URLS].sort(() => Math.random() - 0.5); - - for (const url of urls) { - try { - const response = await fetch(url); - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - data = await response.json(); - break; - } catch (error) { - console.warn(`Failed to fetch from ${url}:`, error); - } - } - - if (data) { - this.apiInstances = (data.api || []).map((item) => item.url || item); - return this.apiInstances; - } - - console.error('Failed to load instances from all uptime APIs'); - return [ - 'https://eu-central.monochrome.tf', - 'https://us-west.monochrome.tf', - 'https://arran.monochrome.tf', - 'https://triton.squid.wtf', - 'https://api.monochrome.tf', - 'https://monochrome-api.samidy.com', - 'https://maus.qqdl.site', - 'https://vogel.qqdl.site', - 'https://katze.qqdl.site', - 'https://hund.qqdl.site', - 'https://tidal.kinoplus.online', - 'https://wolf.qqdl.site', - ]; + return this.apiInstances; } async fetchWithRetry(relativePath) { diff --git a/index.html b/index.html index 2898360..eb274e0 100644 --- a/index.html +++ b/index.html @@ -138,13 +138,14 @@ z-index: 0; " >
+ -
+ diff --git a/js/lyrics.js b/js/lyrics.js index 62f245e..59307ca 100644 --- a/js/lyrics.js +++ b/js/lyrics.js @@ -992,12 +992,17 @@ 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), @@ -1012,6 +1017,10 @@ function applyFullscreenLyricsShadowTweaks(amLyrics, container) { background-color 0.22s ease, color 0.22s ease !important; } + + .lyrics-line.active .lyrics-line-container { + transform: scale(1.015); + } `; return true; diff --git a/js/storage.js b/js/storage.js index edce8ba..743c42c 100644 --- a/js/storage.js +++ b/js/storage.js @@ -4,11 +4,11 @@ import { SVG_RIGHT_ARROW } from './icons'; export const apiSettings = { STORAGE_KEY: 'monochrome-api-instances-v9', - INSTANCES_URLS: [ - 'https://tidal-uptime.jiffy-puffs-1j.workers.dev/', - 'https://tidal-uptime.props-76styles.workers.dev/', - ], - defaultInstances: { api: [], streaming: [] }, + PINNED_INSTANCE: Object.freeze({ url: 'https://hifi.geeked.wtf', version: '2.7' }), + defaultInstances: { + api: [{ url: 'https://hifi.geeked.wtf', version: '2.7' }], + streaming: [{ url: 'https://hifi.geeked.wtf', version: '2.7' }], + }, userInstances: null, instancesLoaded: false, _loadPromise: null, @@ -29,136 +29,13 @@ export const apiSettings = { }, async loadInstancesFromGitHub() { - if (this.instancesLoaded) { - return this.defaultInstances; - } - - if (this._loadPromise) { - return this._loadPromise; - } - - this._loadPromise = (async () => { - const cachedData = localStorage.getItem(this.STORAGE_KEY); - if (cachedData) { - try { - const parsed = JSON.parse(cachedData); - const now = Date.now(); - // Check if cached data is less than 15 minutes old - if (parsed.timestamp && now - parsed.timestamp < 15 * 60 * 1000) { - this.defaultInstances = parsed.data; - this.instancesLoaded = true; - this._loadPromise = null; - return this.defaultInstances; - } - } catch (e) { - console.warn('Failed to parse cached instances:', e); - } - } - - let data = null; - let fetchError = null; - - // Prefer first URL, only try others as fallback - const urls = [...this.INSTANCES_URLS]; - - for (const url of urls) { - try { - const response = await fetch(url); - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - data = await response.json(); - break; // Success, exit loop - } catch (error) { - console.warn(`Failed to fetch from ${url}:`, error); - fetchError = error; - } - } - - if (!data) { - console.error('Failed to load instances from all uptime APIs:', fetchError); - this.defaultInstances = { - api: [ - { url: 'https://hifi.geeked.wtf', version: '2.7' }, - { url: 'https://eu-central.monochrome.tf', version: '2.7' }, - { url: 'https://us-west.monochrome.tf', version: '2.7' }, - { url: 'https://api.monochrome.tf', version: '2.5' }, - { url: 'https://monochrome-api.samidy.com', version: '2.3' }, - { url: 'https://maus.qqdl.site', version: '2.6' }, - { url: 'https://vogel.qqdl.site', version: '2.6' }, - { url: 'https://katze.qqdl.site', version: '2.6' }, - { url: 'https://hund.qqdl.site', version: '2.6' }, - { url: 'https://tidal.kinoplus.online', version: '2.2' }, - { url: 'https://wolf.qqdl.site', version: '2.2' }, - ], - streaming: [ - { url: 'https://hifi.geeked.wtf', version: '2.7' }, - { url: 'https://maus.qqdl.site', version: '2.6' }, - { url: 'https://vogel.qqdl.site', version: '2.6' }, - { url: 'https://katze.qqdl.site', version: '2.6' }, - { url: 'https://hund.qqdl.site', version: '2.6' }, - { url: 'https://wolf.qqdl.site', version: '2.6' }, - ], - }; - this.instancesLoaded = true; - this._loadPromise = null; - return this.defaultInstances; - } - - let groupedInstances = { api: [], streaming: [] }; - - const isBlockedInstance = (item) => { - const url = typeof item === 'string' ? item : item.url; - return url && /\.squid\.wtf/i.test(url); - }; - - if (data.api && Array.isArray(data.api)) { - groupedInstances.api = data.api.filter((item) => !isBlockedInstance(item)); - } - - if (data.streaming && Array.isArray(data.streaming)) { - groupedInstances.streaming = data.streaming.filter((item) => !isBlockedInstance(item)); - } else if (groupedInstances.api.length > 0) { - groupedInstances.streaming = [...groupedInstances.api]; - } - - this.defaultInstances = groupedInstances; - this.instancesLoaded = true; - - try { - localStorage.setItem( - this.STORAGE_KEY, - JSON.stringify({ - timestamp: Date.now(), - data: groupedInstances, - }) - ); - } catch (e) { - console.warn('Failed to cache instances:', e); - } - - this._loadPromise = null; - return groupedInstances; - })(); - - return this._loadPromise; + this.instancesLoaded = true; + return this.defaultInstances; }, async getInstances(type = 'api', _sortBySpeed = false) { - let instancesObj; - - instancesObj = await this.loadInstancesFromGitHub(); - const userInst = this._loadUserInstances(); - - const defaultUrls = instancesObj[type] || instancesObj.api || []; - const userUrls = userInst[type] || []; - - const combined = [ - ...userUrls.map((u) => (typeof u === 'string' ? { url: u, isUser: true } : { ...u, isUser: true })), - ...defaultUrls, - ]; - - if (combined.length === 0) return []; - - return combined; + const instancesObj = await this.loadInstancesFromGitHub(); + return instancesObj[type] || instancesObj.api || []; }, addUserInstance(type, url) { @@ -191,42 +68,6 @@ export const apiSettings = { this.instancesLoaded = false; this._loadPromise = null; localStorage.removeItem(this.STORAGE_KEY); - - const instances = await this.loadInstancesFromGitHub(); - - const shuffle = (array) => { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } - return array; - }; - - const prioritySort = (array) => { - const getUrl = (item) => (typeof item === 'string' ? item : item.url || ''); - const top = []; - const middle = []; - const bottom = []; - for (const item of array) { - const url = getUrl(item); - if (url.includes('hifi.geeked.wtf')) top.push(item); - else if (url.includes('.qqdl.site')) bottom.push(item); - else middle.push(item); - } - return [...top, ...shuffle(middle), ...shuffle(bottom)]; - }; - - if (instances.api && instances.api.length) { - instances.api = prioritySort([...instances.api]); - } - - if (instances.streaming && instances.streaming.length) { - instances.streaming = prioritySort([...instances.streaming]); - } - - this.saveInstances(instances); - - // Return API instances for the UI to render (default view) return this.getInstances('api'); }, saveInstances(instances, type) { diff --git a/js/ui.js b/js/ui.js index 5827d7f..164c30b 100644 --- a/js/ui.js +++ b/js/ui.js @@ -93,6 +93,8 @@ 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]; @@ -157,6 +159,8 @@ export class UIRenderer { 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', () => { @@ -1046,9 +1050,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. @@ -1075,6 +1083,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'; @@ -1086,6 +1111,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; @@ -1108,6 +1135,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'); } @@ -1221,7 +1250,6 @@ 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')); } @@ -1239,7 +1267,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'); } @@ -1261,18 +1289,21 @@ export class UIRenderer { const canRenderLyrics = Boolean(lyricsManager && activeElement && lyricsPane && lyricsContent && track.type !== 'video'); if (canRenderLyrics) { - lyricsToggleBtn.style.display = 'none'; + this.fullscreenLyricsVisible = true; + if (lyricsToggleBtn) lyricsToggleBtn.style.removeProperty('display'); overlay.classList.remove('lyrics-unavailable'); clearFullscreenLyricsSync(lyricsContent); await renderLyricsInFullscreen(track, activeElement, lyricsManager, lyricsContent); } else { - lyricsToggleBtn.style.display = 'none'; + this.fullscreenLyricsVisible = false; + if (lyricsToggleBtn) lyricsToggleBtn.style.display = 'none'; overlay.classList.add('lyrics-unavailable'); if (lyricsContent) { clearFullscreenLyricsSync(lyricsContent); lyricsContent.innerHTML = '
Lyrics are not available for this track.
'; } } + this.updateFullscreenLyricsVisibility(overlay); const playerBar = document.querySelector('.now-playing-bar'); if (playerBar) playerBar.style.display = 'none'; @@ -1303,9 +1334,64 @@ 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 lyricsToggleBtn = document.getElementById('toggle-fullscreen-lyrics-btn'); + const lyricsUnavailable = overlay.classList.contains('lyrics-unavailable'); + const shouldShowLyrics = this.fullscreenLyricsVisible && !lyricsUnavailable; + + overlay.classList.toggle('lyrics-hidden', !shouldShowLyrics); + + if (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'); + } + } + } + + 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'); @@ -1318,7 +1404,16 @@ export class UIRenderer { lyricsContent.innerHTML = '
Lyrics appear here.
'; } overlay.style.display = 'none'; - overlay.classList.remove('visualizer-active', 'ui-hidden', 'fullscreen-cover-no-round', 'fullscreen-paused'); + overlay.classList.remove( + 'visualizer-active', + 'ui-hidden', + 'fullscreen-cover-no-round', + 'fullscreen-paused', + 'fullscreen-dragging', + 'fullscreen-dismissing' + ); + overlay.style.removeProperty('--fullscreen-drag-offset'); + overlay.style.removeProperty('--fullscreen-drag-progress'); const playerBar = document.querySelector('.now-playing-bar'); if (playerBar) playerBar.style.removeProperty('display'); @@ -1379,6 +1474,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) { @@ -1393,6 +1498,7 @@ export class UIRenderer { } if (this.visualizer) { + this.visualizer.applyPresetOverride('kawarp'); await this.visualizer.start(); overlay.classList.add('visualizer-active'); } @@ -1438,7 +1544,8 @@ 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; @@ -1604,6 +1711,136 @@ 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 toggleBtn = document.getElementById('toggle-fullscreen-lyrics-btn'); + if (!toggleBtn) return; + + const handleToggle = (event) => { + event.preventDefault(); + event.stopPropagation(); + if (overlay.classList.contains('lyrics-unavailable')) return; + this.fullscreenLyricsVisible = !this.fullscreenLyricsVisible; + this.updateFullscreenLyricsVisibility(overlay); + }; + + toggleBtn.addEventListener('click', handleToggle); + this.updateFullscreenLyricsVisibility(overlay); + + this.fullscreenLyricsToggleCleanup = () => { + toggleBtn.removeEventListener('click', handleToggle); + }; + } + setupFullscreenControls() { const playBtn = document.getElementById('fs-play-pause-btn'); const prevBtn = document.getElementById('fs-prev-btn'); @@ -1673,16 +1910,7 @@ export class UIRenderer { if (visualizerBtn) { visualizerBtn.onclick = async () => { - if (this.fullscreenVisualizerSuppressed) { - this.fullscreenVisualizerSuppressed = false; - visualizerSettings.setEnabled(true); - } else if (visualizerSettings.isEnabled()) { - visualizerSettings.setEnabled(false); - this.fullscreenVisualizerSuppressed = false; - } else { - this.fullscreenVisualizerSuppressed = false; - visualizerSettings.setEnabled(true); - } + this.fullscreenVisualizerSuppressed = !this.fullscreenVisualizerSuppressed; await this.refreshFullscreenVisualizerState(this.player.activeElement); }; } diff --git a/js/visualizer.js b/js/visualizer.js index 49017d2..ff8a988 100644 --- a/js/visualizer.js +++ b/js/visualizer.js @@ -318,4 +318,16 @@ export class Visualizer { }); } } + + applyPresetOverride(key) { + if (!this.presets?.[key] || this.activePresetKey === key) return; + + if (this.activePreset?.destroy) { + this.activePreset.destroy(); + } + + this._currentContextType = undefined; + this.ctx = null; + this.activePresetKey = key; + } } diff --git a/public/instances.json b/public/instances.json index d5a8bb9..b9fce85 100644 --- a/public/instances.json +++ b/public/instances.json @@ -1,25 +1,8 @@ { "api": [ - "https://eu-central.monochrome.tf", - "https://us-west.monochrome.tf", - "https://arran.monochrome.tf", - "https://api.monochrome.tf/", - "https://monochrome-api.samidy.com", - "https://triton.squid.wtf", - "https://wolf.qqdl.site", - "https://maus.qqdl.site", - "https://vogel.qqdl.site", - "https://hund.qqdl.site", - "https://tidal.kinoplus.online" + "https://hifi.geeked.wtf" ], "streaming": [ - "https://arran.monochrome.tf", - "https://triton.squid.wtf", - "https://wolf.qqdl.site", - "https://maus.qqdl.site", - "https://vogel.qqdl.site", - "https://katze.qqdl.site", - "https://hund.qqdl.site", - "https://hifi.p1nkhamster.xyz/" + "https://hifi.geeked.wtf" ] } diff --git a/styles.css b/styles.css index 57d66e9..7b32f2e 100644 --- a/styles.css +++ b/styles.css @@ -3920,29 +3920,25 @@ input:checked + .slider::before { justify-content: center; animation: fade-in 0.3s ease; overflow: hidden; - background-color: var(--background); - - /* Use a CSS variable for the image so we can set it in JS */ - --bg-image: none; + background-color: rgb(11 13 17); /* Reserve space above taskbar / system UI so volume controls stay visible (fixes #322) */ padding-bottom: max(env(safe-area-inset-bottom), 1.5rem); + --fullscreen-drag-progress: 0; + --fs-accent-rgb: var(--highlight-rgb); } #fullscreen-cover-overlay::before { content: ''; position: absolute; - inset: -20px; - background-size: cover; - background-position: center; - background-repeat: no-repeat; - filter: var(--cover-filter); + inset: 0; + background: + radial-gradient(circle at 50% 50%, rgb(255 255 255 / 0.035), transparent 58%), + linear-gradient(180deg, rgb(6 8 12 / 0.12), rgb(6 8 12 / 0.34)); z-index: -1; - background-image: var(--bg-image); transition: - background-image var(--transition), - filter 0.65s ease, opacity 0.65s ease; + opacity: calc(1 - (var(--fullscreen-drag-progress, 0) * 0.32)); } #fullscreen-cover-overlay::after { @@ -3950,10 +3946,10 @@ input:checked + .slider::before { position: absolute; inset: 0; background: - radial-gradient(circle at 20% 22%, rgb(var(--highlight-rgb) / 0.28), transparent 36%), + radial-gradient(circle at 20% 22%, rgb(var(--fs-accent-rgb) / 0.28), transparent 36%), radial-gradient(circle at 82% 18%, rgb(255 255 255 / 0.09), transparent 28%), - linear-gradient(135deg, rgb(10 13 18 / 0.48), rgb(10 13 18 / 0.2) 38%, rgb(var(--highlight-rgb) / 0.12) 100%); - opacity: 0.36; + linear-gradient(135deg, rgb(10 13 18 / 0.48), rgb(10 13 18 / 0.2) 38%, rgb(var(--fs-accent-rgb) / 0.12) 100%); + opacity: calc(0.36 - (var(--fullscreen-drag-progress, 0) * 0.26)); pointer-events: none; z-index: 0; transition: @@ -3968,9 +3964,9 @@ input:checked + .slider::before { height: 100%; z-index: 0; pointer-events: none; - filter: blur(14px) saturate(0.84) brightness(0.8); - transform: scale(1.04); - opacity: 0.82; + filter: blur(8px) saturate(0.9) brightness(0.8); + transform: scale(1.03); + opacity: 0.8; transition: opacity 0.65s ease, filter 0.65s ease, @@ -3994,6 +3990,51 @@ input:checked + .slider::before { position: relative; padding: 1rem; overflow: hidden; + transform: translateY(var(--fullscreen-drag-offset, 0px)); + opacity: calc(1 - (var(--fullscreen-drag-progress, 0) * 0.16)); + transition: + transform 0.26s cubic-bezier(0.22, 1, 0.36, 1), + opacity 0.22s ease; + will-change: transform, opacity; +} + +#fullscreen-dismiss-handle { + position: absolute; + top: calc(0.75rem + env(safe-area-inset-top)); + left: 50%; + width: 3.25rem; + height: 1rem; + border: 0; + padding: 0; + margin: 0; + background: transparent; + transform: translateX(-50%); + z-index: 14; + display: none; + cursor: grab; + touch-action: none; +} + +#fullscreen-dismiss-handle::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 3rem; + height: 0.3rem; + border-radius: 999px; + transform: translate(-50%, -50%); + background: rgb(255 255 255 / 0.28); + box-shadow: 0 2px 12px rgb(0 0 0 / 0.25); +} + +#fullscreen-cover-overlay.fullscreen-dragging .fullscreen-cover-content { + transition: none; +} + +#fullscreen-cover-overlay.fullscreen-dismissing .fullscreen-cover-content { + transform: translateY(calc(100% + 3rem)); + opacity: 0; } /* UI Toggle Button for Visualizer Mode - Rightmost position */ @@ -10049,19 +10090,25 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { } #fullscreen-cover-overlay .fullscreen-main-view { - width: min(1240px, 100%); + --fs-media-column-size: minmax(340px, 430px); + --fs-lyrics-column-size: minmax(520px, 760px); + width: min(1480px, 100%); height: 100%; flex: 1; display: grid; - grid-template-columns: minmax(360px, 430px) minmax(420px, 1fr); - gap: clamp(1.5rem, 3vw, 3rem); + grid-template-columns: var(--fs-media-column-size) var(--fs-lyrics-column-size); + gap: clamp(2rem, 4vw, 4.5rem); align-items: center; justify-content: center; - padding: clamp(4rem, 7vh, 5rem) clamp(2rem, 4vw, 3rem) clamp(3rem, 6vh, 4rem) clamp(4rem, 7vw, 6.25rem); + padding: clamp(4rem, 7vh, 5rem) clamp(3rem, 6vw, 5rem) clamp(3rem, 6vh, 4rem); position: relative; z-index: 1; min-height: 0; overflow: hidden; + transition: + grid-template-columns 0.34s cubic-bezier(0.22, 1, 0.36, 1), + width 0.34s cubic-bezier(0.22, 1, 0.36, 1), + gap 0.34s cubic-bezier(0.22, 1, 0.36, 1); } #fullscreen-cover-overlay .fullscreen-media-column, @@ -10075,7 +10122,11 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { flex-direction: column; gap: 0.95rem; justify-self: center; - transform: translateX(clamp(0.75rem, 1.2vw, 1.4rem)); + transform: none; + transition: + width 0.34s cubic-bezier(0.22, 1, 0.36, 1), + transform 0.34s cubic-bezier(0.22, 1, 0.36, 1), + opacity 0.24s ease; } #fullscreen-cover-overlay .fullscreen-artwork-card { @@ -10128,7 +10179,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { #fullscreen-cover-overlay #toggle-fullscreen-lyrics-btn, #fullscreen-cover-overlay .fullscreen-lyrics-toggle { - display: none !important; + display: flex; } #fullscreen-cover-overlay .fullscreen-actions { @@ -10206,9 +10257,10 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { } #fullscreen-cover-overlay .fullscreen-top-actions #fs-visualizer-btn { - order: 2; + order: 3; } +#fullscreen-cover-overlay .fullscreen-top-actions #toggle-fullscreen-lyrics-btn, #fullscreen-cover-overlay .fullscreen-top-actions #fs-visualizer-btn, #fullscreen-cover-overlay .fullscreen-top-actions #close-fullscreen-cover-btn { position: static; @@ -10220,6 +10272,15 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { opacity: 1; } +#fullscreen-cover-overlay .fullscreen-top-actions #toggle-fullscreen-lyrics-btn { + order: 2; +} + +#fullscreen-cover-overlay .fullscreen-top-actions #toggle-fullscreen-lyrics-btn.active { + color: rgb(255 255 255 / 0.96); + background: rgb(255 255 255 / 0.12); +} + #fullscreen-cover-overlay .fullscreen-top-actions #close-fullscreen-cover-btn { order: 1; } @@ -10240,7 +10301,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { #fullscreen-cover-overlay #toggle-ui-btn { top: 1.25rem; - left: calc(80px + 2.3rem + env(safe-area-inset-left)); + left: calc(9.9rem + env(safe-area-inset-left)); right: auto; width: 40px; height: 40px; @@ -10285,7 +10346,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { } #fullscreen-cover-overlay .fullscreen-buttons button.active { - color: rgb(var(--highlight-rgb) / 0.98); + color: rgb(var(--fs-accent-rgb) / 0.98); } #fullscreen-cover-overlay .fullscreen-buttons #fs-play-pause-btn { @@ -10326,7 +10387,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { } #fullscreen-cover-overlay .fs-visualizer-btn.active { - color: rgb(var(--highlight-rgb) / 0.96); + color: rgb(var(--fs-accent-rgb) / 0.96); } #fullscreen-cover-overlay .fs-volume-btn { @@ -10372,7 +10433,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { #fullscreen-cover-overlay .fullscreen-progress-container .progress-bar:hover .progress-fill, #fullscreen-cover-overlay .fs-volume-bar:hover .fs-volume-fill { - background: rgb(var(--highlight-rgb) / 0.94); + background: rgb(var(--fs-accent-rgb) / 0.94); } #fullscreen-cover-overlay .fullscreen-progress-container .progress-bar:hover .progress-fill::after, @@ -10389,6 +10450,13 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { align-items: stretch; justify-content: flex-start; overflow: hidden; + min-width: 0; + opacity: 1; + transform: translateX(0); + transition: + opacity 0.24s ease, + transform 0.34s cubic-bezier(0.22, 1, 0.36, 1), + visibility 0s linear 0s; } #fullscreen-cover-overlay .fullscreen-lyrics-shell, @@ -10404,14 +10472,14 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { #fullscreen-cover-overlay .fullscreen-lyrics-shell { width: min(860px, 100%); min-height: 0; - margin-left: clamp(4rem, 8vw, 8rem); + margin-left: 0; } #fullscreen-cover-overlay .fullscreen-lyrics-content { min-height: 0; height: 100%; position: relative; - padding-left: clamp(2.5rem, 4vw, 4rem); + padding-left: clamp(0.5rem, 1.6vw, 1.5rem); mask-image: none; overflow: visible; scrollbar-width: none; @@ -10459,6 +10527,34 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { opacity: 0.55; } +#fullscreen-cover-overlay.lyrics-hidden .fullscreen-main-view { + --fs-media-column-size: minmax(420px, 760px); + --fs-lyrics-column-size: minmax(0, 0fr); + width: min(760px, 100%); + gap: 0; +} + +#fullscreen-cover-overlay.lyrics-hidden .fullscreen-media-column { + justify-self: center; + width: min(520px, 100%); + transform: translateX(clamp(2rem, 4vw, 3.5rem)); +} + +#fullscreen-cover-overlay.lyrics-hidden .fullscreen-lyrics-pane { + opacity: 0; + transform: translateX(2rem); + visibility: hidden; + pointer-events: none; +} + +@media (prefers-reduced-motion: reduce) { + #fullscreen-cover-overlay .fullscreen-main-view, + #fullscreen-cover-overlay .fullscreen-media-column, + #fullscreen-cover-overlay .fullscreen-lyrics-pane { + transition: none !important; + } +} + #fullscreen-cover-overlay.queue-panel-active .fullscreen-main-view { grid-template-columns: 1fr; width: min(760px, 100%); @@ -10475,110 +10571,217 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { @media (max-width: 980px) { #fullscreen-cover-overlay .fullscreen-main-view { - grid-template-columns: 1fr; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr) auto; width: min(760px, 100%); - gap: 1rem; - align-items: start; + gap: 1.25rem; + align-items: stretch; padding: - calc(4.5rem + env(safe-area-inset-top)) - clamp(1rem, 4vw, 1.5rem) + calc(5rem + env(safe-area-inset-top)) + clamp(1rem, 4vw, 1.75rem) calc(1.5rem + env(safe-area-inset-bottom)) - clamp(1rem, 4vw, 1.5rem); + clamp(1rem, 4vw, 1.75rem); } #fullscreen-cover-overlay .fullscreen-media-column { justify-self: center; transform: none; + width: min(100%, 620px); } #fullscreen-cover-overlay .fullscreen-lyrics-pane { - display: none; + display: flex; + width: min(100%, 620px); + justify-self: center; + min-height: min(48vh, 440px); + } + + #fullscreen-cover-overlay .fullscreen-lyrics-shell { + width: 100%; + margin-left: 0; + } + + #fullscreen-cover-overlay .fullscreen-lyrics-content { + padding-left: 0; } } @media (max-width: 768px) { #fullscreen-cover-overlay { - --fs-mobile-top-btn-size: 44px; + --fs-mobile-top-btn-size: 42px; --fs-mobile-top-btn-gap: 0.6rem; --fs-mobile-top-btn-left: calc(1rem + env(safe-area-inset-left)); } #fullscreen-cover-overlay .fullscreen-cover-content { - padding: 0.75rem 0.75rem calc(0.75rem + env(safe-area-inset-bottom)); + padding: 0 calc(0.9rem + env(safe-area-inset-right)) calc(0.9rem + env(safe-area-inset-bottom)) + calc(0.9rem + env(safe-area-inset-left)); + } + + #fullscreen-dismiss-handle { + display: block; } #fullscreen-cover-overlay .fullscreen-top-actions { - top: calc(0.75rem + env(safe-area-inset-top)); - left: var(--fs-mobile-top-btn-left); - gap: var(--fs-mobile-top-btn-gap); + display: none; } - #fullscreen-cover-overlay .fullscreen-top-actions button, + #fullscreen-cover-overlay .fullscreen-lyrics-toggle, #fullscreen-cover-overlay #toggle-ui-btn { - width: var(--fs-mobile-top-btn-size); - height: var(--fs-mobile-top-btn-size); - background: rgb(9 12 18 / 0.5); - } - - #fullscreen-cover-overlay #toggle-ui-btn { - top: calc(0.75rem + env(safe-area-inset-top)); - left: calc( - var(--fs-mobile-top-btn-left) + - (var(--fs-mobile-top-btn-size) * 2) + - (var(--fs-mobile-top-btn-gap) * 2) - ); + display: none !important; } #fullscreen-cover-overlay .fullscreen-main-view { width: 100%; - gap: 0.85rem; + height: 100%; + grid-template-columns: minmax(78px, 92px) minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr) auto; + grid-template-areas: + 'art info' + 'lyrics lyrics' + 'controls controls'; + gap: 1rem 0.9rem; padding: - calc(7.25rem + env(safe-area-inset-top)) - 0.75rem - calc(1.5rem + env(safe-area-inset-bottom)) - 0.75rem; + calc(4.45rem + env(safe-area-inset-top)) + 0 + calc(0.8rem + env(safe-area-inset-bottom)) + 0; } - #fullscreen-cover-overlay .fullscreen-track-info, - #fullscreen-cover-overlay .fullscreen-controls, #fullscreen-cover-overlay .fullscreen-media-column { - width: min(100%, 460px); + display: contents; + width: auto; + } + + #fullscreen-cover-overlay .fullscreen-artwork-card { + grid-area: art; + width: 100%; + max-width: 92px; + border-radius: 12px; + align-self: start; + margin-left: 0.95rem; + box-shadow: 0 20px 48px rgb(0 0 0 / 0.34); + } + + #fullscreen-cover-overlay #fullscreen-cover-image { + border-radius: 12px; + } + + #fullscreen-cover-overlay .fullscreen-track-info { + grid-area: info; + width: 100%; + min-width: 0; + align-self: center; + display: grid; + gap: 0.3rem; + padding-top: 0.2rem; + padding-left: 0.95rem; + } + + #fullscreen-cover-overlay .fullscreen-track-text { + display: grid; + gap: 0.14rem; + } + + #fullscreen-cover-overlay #fullscreen-track-title { + font-size: clamp(1.1rem, 4.7vw, 1.34rem); + line-height: 1.04; + } + + #fullscreen-cover-overlay #fullscreen-track-artist { + margin-top: 0; + font-size: 0.92rem; + color: rgb(255 255 255 / 0.7); } #fullscreen-cover-overlay .fullscreen-actions { + display: none !important; + } + + #fullscreen-cover-overlay #fullscreen-next-track { + display: none !important; + } + + #fullscreen-cover-overlay .fullscreen-lyrics-pane { + grid-area: lyrics; width: 100%; - flex-wrap: wrap; - gap: 0.45rem; + min-height: 0; + justify-self: stretch; } - #fullscreen-cover-overlay .fullscreen-actions .btn-icon { - background: rgb(255 255 255 / 0.06); + #fullscreen-cover-overlay .fullscreen-lyrics-shell { + min-height: 0; + position: relative; + background: transparent !important; + box-shadow: none !important; + border-radius: 0; + overflow: visible; } - #fullscreen-cover-overlay .fullscreen-progress-container { - gap: 0.65rem; + #fullscreen-cover-overlay .fullscreen-lyrics-content { + height: 100%; + padding: 0 0 0.2rem; + overflow: hidden; + mask-image: linear-gradient(180deg, transparent 0%, black 10%, black 88%, transparent 100%); } - #fullscreen-cover-overlay .fullscreen-buttons { - gap: 0.2rem; + #fullscreen-cover-overlay .fullscreen-lyrics-content am-lyrics { + --lyrics-scroll-padding-top: 18%; + --lyplus-font-size-base: clamp(1.75rem, 7vw, 2.35rem); + --lyplus-padding-line: 6px; + --lyplus-text-color: rgba(246, 244, 239, 0.16); + --lyplus-blur-amount: 0.16em; + --lyplus-blur-amount-near: 0.08em; + line-height: 1.2; } - #fullscreen-cover-overlay .fullscreen-buttons button { - width: 38px; - height: 38px; + #fullscreen-cover-overlay .fullscreen-lyrics-empty, + #fullscreen-cover-overlay .fullscreen-lyrics-content .lyrics-loading, + #fullscreen-cover-overlay .fullscreen-lyrics-content .lyrics-error { + padding: 2.5rem 1.2rem 0; } - #fullscreen-cover-overlay .fullscreen-buttons #fs-play-pause-btn { - width: 52px; - height: 52px; + #fullscreen-cover-overlay .fullscreen-controls { + grid-area: controls; + width: 100%; + max-width: none; + margin-top: 0; + padding: 0.15rem 0 0; + gap: 0.9rem; } #fullscreen-cover-overlay .fullscreen-volume-container { - width: min(220px, calc(100% - 2.75rem)); + display: none; + } + + #fullscreen-cover-overlay .fullscreen-progress-container { + gap: 0.55rem; + font-size: 0.72rem; + } + + #fullscreen-cover-overlay .fullscreen-buttons { + gap: 0.1rem; + justify-content: space-between; + } + + #fullscreen-cover-overlay .fullscreen-buttons button { + width: 42px; + height: 42px; + } + + #fullscreen-cover-overlay .fullscreen-buttons #fs-play-pause-btn { + width: 62px; + height: 62px; + box-shadow: 0 14px 28px rgb(0 0 0 / 0.3); + } + + #fullscreen-cover-overlay .fullscreen-volume-container { + width: min(280px, calc(100% - 3rem)); + margin-top: 0; } #fullscreen-cover-overlay .fs-volume-btn { - left: -2.25rem; + left: -2.5rem; } #fullscreen-cover-overlay .fs-volume-bar { From 4dd3ec1a0c64bfe1562dcc5f562f263eaf80102b Mon Sep 17 00:00:00 2001 From: Alan Brooks Date: Sun, 5 Apr 2026 23:03:59 -0400 Subject: [PATCH 7/9] fix: add lyrics toggle for mobile --- index.html | 3 ++ js/ui.js | 20 ++++++++---- styles.css | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 7 deletions(-) diff --git a/index.html b/index.html index 4125d3c..4415024 100644 --- a/index.html +++ b/index.html @@ -206,6 +206,9 @@ " >
+ diff --git a/js/ui.js b/js/ui.js index b7cb8e8..1df95f6 100644 --- a/js/ui.js +++ b/js/ui.js @@ -1392,13 +1392,16 @@ export class UIRenderer { updateFullscreenLyricsVisibility(overlay = document.getElementById('fullscreen-cover-overlay')) { if (!overlay) return; - const lyricsToggleBtn = document.getElementById('toggle-fullscreen-lyrics-btn'); + 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); - if (lyricsToggleBtn) { + lyricsToggleButtons.forEach((lyricsToggleBtn) => { lyricsToggleBtn.classList.toggle('active', shouldShowLyrics); lyricsToggleBtn.title = shouldShowLyrics ? 'Hide Lyrics' : 'Show Lyrics'; lyricsToggleBtn.setAttribute('aria-pressed', shouldShowLyrics ? 'true' : 'false'); @@ -1407,7 +1410,7 @@ export class UIRenderer { } else { lyricsToggleBtn.style.removeProperty('display'); } - } + }); } async dismissFullscreenCover({ animate = true } = {}) { @@ -1870,8 +1873,11 @@ export class UIRenderer { this.fullscreenLyricsToggleCleanup = null; } - const toggleBtn = document.getElementById('toggle-fullscreen-lyrics-btn'); - if (!toggleBtn) return; + 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(); @@ -1881,11 +1887,11 @@ export class UIRenderer { this.updateFullscreenLyricsVisibility(overlay); }; - toggleBtn.addEventListener('click', handleToggle); + toggleButtons.forEach((toggleBtn) => toggleBtn.addEventListener('click', handleToggle)); this.updateFullscreenLyricsVisibility(overlay); this.fullscreenLyricsToggleCleanup = () => { - toggleBtn.removeEventListener('click', handleToggle); + toggleButtons.forEach((toggleBtn) => toggleBtn.removeEventListener('click', handleToggle)); }; } setupFullscreenControls() { diff --git a/styles.css b/styles.css index 32f6a52..67ef486 100644 --- a/styles.css +++ b/styles.css @@ -4112,6 +4112,38 @@ input:checked + .slider::before { background: var(--primary); } +#toggle-fullscreen-lyrics-mobile-btn { + display: none; + position: absolute; + top: calc(0.85rem + env(safe-area-inset-top)); + right: calc(0.9rem + env(safe-area-inset-right)); + width: 38px; + height: 38px; + border: none; + border-radius: 999px; + padding: 0; + align-items: center; + justify-content: center; + background: rgb(9 12 18 / 0.32); + color: rgb(255 255 255 / 0.76); + backdrop-filter: blur(10px); + z-index: 14; + transition: + background-color 0.2s ease, + color 0.2s ease, + opacity 0.2s ease, + transform 0.2s ease; +} + +#toggle-fullscreen-lyrics-mobile-btn.active { + background: rgb(255 255 255 / 0.12); + color: rgb(255 255 255 / 0.96); +} + +#toggle-fullscreen-lyrics-mobile-btn:hover { + transform: scale(1.04); +} + /* Close Button - Leftmost position */ #close-fullscreen-cover-btn { position: absolute; @@ -10626,6 +10658,10 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { display: none !important; } + #toggle-fullscreen-lyrics-mobile-btn { + display: flex; + } + #fullscreen-cover-overlay .fullscreen-main-view { width: 100%; height: 100%; @@ -10770,6 +10806,64 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn { box-shadow: 0 14px 28px rgb(0 0 0 / 0.3); } + #fullscreen-cover-overlay.lyrics-hidden .fullscreen-main-view { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr) auto auto auto; + grid-template-areas: + '.' + 'art' + 'info' + 'controls'; + align-content: stretch; + justify-items: center; + gap: 0.95rem; + padding: + calc(4.45rem + env(safe-area-inset-top)) + 0 + calc(0.8rem + env(safe-area-inset-bottom)) + 0; + height: 100%; + } + + #fullscreen-cover-overlay.lyrics-hidden .fullscreen-lyrics-pane { + display: none; + } + + #fullscreen-cover-overlay.lyrics-hidden .fullscreen-artwork-card { + max-width: min(88vw, 320px); + width: min(88vw, 320px); + margin-left: 0; + justify-self: center; + border-radius: 14px; + box-shadow: 0 24px 56px rgb(0 0 0 / 0.28); + } + + #fullscreen-cover-overlay.lyrics-hidden #fullscreen-cover-image { + border-radius: 14px; + } + + #fullscreen-cover-overlay.lyrics-hidden .fullscreen-track-info { + align-self: start; + justify-self: center; + width: min(88vw, 320px); + padding: 0; + gap: 0.3rem; + } + + #fullscreen-cover-overlay.lyrics-hidden .fullscreen-track-text { + gap: 0.2rem; + } + + #fullscreen-cover-overlay.lyrics-hidden #fullscreen-track-title { + font-size: clamp(1.15rem, 5vw, 1.5rem); + line-height: 1.05; + } + + #fullscreen-cover-overlay.lyrics-hidden #fullscreen-track-artist { + font-size: 0.95rem; + color: rgb(255 255 255 / 0.66); + } + #fullscreen-cover-overlay .fullscreen-volume-container { width: min(280px, calc(100% - 3rem)); margin-top: 0; From daa553488f84e865ac1468ac53cc9695cfa1e73e Mon Sep 17 00:00:00 2001 From: Alan Brooks Date: Sun, 5 Apr 2026 23:41:34 -0400 Subject: [PATCH 8/9] add lyrics toggle for mobile --- index.html | 2 +- js/ui.js | 20 ++++++++++-- styles.css | 96 +++++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 100 insertions(+), 18 deletions(-) diff --git a/index.html b/index.html index 4415024..605a632 100644 --- a/index.html +++ b/index.html @@ -261,6 +261,7 @@
+
0:00
@@ -284,7 +285,6 @@ -