+
+ Album Cover +
+

+

+
+
+ +
+ + +
  • Remove from Queue
  • @@ -299,7 +314,8 @@ Choose what shows when you click the album art
diff --git a/js/app.js b/js/app.js index 89af530..5e04325 100644 --- a/js/app.js +++ b/js/app.js @@ -251,9 +251,20 @@ document.addEventListener('DOMContentLoaded', async () => { // Clear sync when hiding clearLyricsPanelSync(audioPlayer, lyricsPanel); } + } else if (mode === 'cover') { + ui.showFullscreenCover(player.currentTrack); + } else { + // Default to 'album' mode - navigate to album + if (player.currentTrack.album?.id) { + window.location.hash = `#album/${player.currentTrack.album.id}`; + } } }); + document.getElementById('close-fullscreen-cover-btn')?.addEventListener('click', () => { + ui.closeFullscreenCover(); + }); + document.getElementById('close-lyrics-btn')?.addEventListener('click', (e) => { e.stopPropagation(); lyricsPanel.classList.add('hidden'); diff --git a/js/storage.js b/js/storage.js index 3c7250c..1ceb671 100644 --- a/js/storage.js +++ b/js/storage.js @@ -305,9 +305,9 @@ export const nowPlayingSettings = { getMode() { try { - return localStorage.getItem(this.STORAGE_KEY) || 'cover'; + return localStorage.getItem(this.STORAGE_KEY) || 'album'; } catch (e) { - return 'cover'; + return 'album'; } }, diff --git a/js/ui.js b/js/ui.js index 78fb887..ef8041b 100644 --- a/js/ui.js +++ b/js/ui.js @@ -259,6 +259,30 @@ export class UIRenderer { root.style.removeProperty('--ring'); } + showFullscreenCover(track) { + if (!track) return; + + const overlay = document.getElementById('fullscreen-cover-overlay'); + const image = document.getElementById('fullscreen-cover-image'); + const title = document.getElementById('fullscreen-track-title'); + const artist = document.getElementById('fullscreen-track-artist'); + + const coverUrl = this.api.getCoverUrl(track.album?.cover, '1280'); + + image.src = coverUrl; + title.textContent = track.title; + artist.textContent = track.artist?.name || 'Unknown Artist'; + + // Set the background image via CSS variable for the pseudo-element to use + overlay.style.setProperty('--bg-image', `url('${coverUrl}')`); + + overlay.style.display = 'flex'; + } + + closeFullscreenCover() { + document.getElementById('fullscreen-cover-overlay').style.display = 'none'; + } + showPage(pageId) { document.querySelectorAll('.page').forEach(page => { page.classList.toggle('active', page.id === `page-${pageId}`); diff --git a/styles.css b/styles.css index f945bcb..6575356 100644 --- a/styles.css +++ b/styles.css @@ -259,6 +259,8 @@ kbd { grid-template-columns: 1fr 2fr 1fr; align-items: center; gap: var(--spacing-xl); + position: relative; + z-index: 2100; } .sidebar-logo { @@ -1222,6 +1224,120 @@ input:checked + .slider::before { animation: fadeIn 0.2s cubic-bezier(0.4, 0, 0.2, 1); } +#fullscreen-cover-overlay { + position: fixed; + inset: 0; + z-index: 2000; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + animation: fadeIn 0.3s ease; + overflow: hidden; + background-color: #000; /* Solid background to prevent see-through */ + /* Use a CSS variable for the image so we can set it in JS */ + --bg-image: none; + padding-bottom: 90px; /* Account for desktop player bar */ +} + +#fullscreen-cover-overlay::before { + content: ""; + position: absolute; + inset: -20px; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + filter: blur(30px) brightness(0.4); + z-index: -1; + background-image: var(--bg-image); +} + +.fullscreen-cover-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + /* Remove fixed padding to allow flex centering to work within the overlay's padded box */ + padding: 1rem; + position: relative; +} + +#close-fullscreen-cover-btn { + position: absolute; + top: 1rem; + right: 1rem; + background: rgba(0, 0, 0, 0.5); + border: none; + color: #fff; + font-size: 2rem; + width: 48px; + height: 48px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s; + z-index: 10; +} + +#close-fullscreen-cover-btn:hover { + background-color: rgba(0, 0, 0, 0.8); +} + +#fullscreen-cover-image { + max-width: 80vw; + max-height: 60vh; + border-radius: var(--radius); + box-shadow: 0 20px 50px rgba(0,0,0,0.5); + object-fit: contain; + margin-bottom: 2rem; + z-index: 1; +} + +.fullscreen-track-info { + text-align: center; + z-index: 1; + max-width: 90%; +} + +#fullscreen-track-title { + font-size: 2rem; + font-weight: 700; + margin-bottom: 0.5rem; + color: #ffffff; + text-shadow: 0 2px 10px rgba(0,0,0,0.5); + word-break: break-word; +} + +#fullscreen-track-artist { + font-size: 1.25rem; + color: rgba(255, 255, 255, 0.8); + font-weight: 500; + text-shadow: 0 2px 10px rgba(0,0,0,0.5); +} + +@media (max-width: 768px) { + #fullscreen-cover-overlay { + padding-bottom: 160px; /* Account for taller mobile player bar */ + } + + #fullscreen-cover-image { + max-height: 45vh; /* Reduce height on mobile to fit text */ + margin-bottom: 1.5rem; + } + + #fullscreen-track-title { + font-size: 1.5rem; + } + + #fullscreen-track-artist { + font-size: 1rem; + } +} + #queue-modal { background-color: var(--card); width: 90%; From 8361d31408acd70f2391225cf5f2a7620937fe82 Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Tue, 23 Dec 2025 22:55:23 +0100 Subject: [PATCH 3/7] feat: add next track info to enlarged cover view with animation --- index.html | 4 ++++ js/app.js | 10 +++++++++- js/player.js | 13 +++++++++++++ js/ui.js | 16 +++++++++++++++- styles.css | 27 +++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 8d00374..53b2a85 100644 --- a/index.html +++ b/index.html @@ -43,6 +43,10 @@

+
diff --git a/js/app.js b/js/app.js index 5e04325..3c2feac 100644 --- a/js/app.js +++ b/js/app.js @@ -252,7 +252,8 @@ document.addEventListener('DOMContentLoaded', async () => { clearLyricsPanelSync(audioPlayer, lyricsPanel); } } else if (mode === 'cover') { - ui.showFullscreenCover(player.currentTrack); + const nextTrack = player.getNextTrack(); + ui.showFullscreenCover(player.currentTrack, nextTrack); } else { // Default to 'album' mode - navigate to album if (player.currentTrack.album?.id) { @@ -315,6 +316,13 @@ document.addEventListener('DOMContentLoaded', async () => { } } } + + // Update Fullscreen/Enlarged Cover if it's open + const fullscreenOverlay = document.getElementById('fullscreen-cover-overlay'); + if (fullscreenOverlay && getComputedStyle(fullscreenOverlay).display !== 'none') { + const nextTrack = player.getNextTrack(); + ui.showFullscreenCover(player.currentTrack, nextTrack); + } }); document.addEventListener('click', async (e) => { diff --git a/js/player.js b/js/player.js index c67b689..8cb5090 100644 --- a/js/player.js +++ b/js/player.js @@ -366,6 +366,19 @@ export class Player { return this.shuffleActive ? this.shuffledQueue : this.queue; } + getNextTrack() { + const currentQueue = this.getCurrentQueue(); + if (this.currentQueueIndex === -1 || currentQueue.length === 0) return null; + + const nextIndex = this.currentQueueIndex + 1; + if (nextIndex < currentQueue.length) { + return currentQueue[nextIndex]; + } else if (this.repeatMode === REPEAT_MODE.ALL) { + return currentQueue[0]; + } + return null; + } + updatePlayingTrackIndicator() { const currentTrack = this.getCurrentQueue()[this.currentQueueIndex]; document.querySelectorAll('.track-item').forEach(item => { diff --git a/js/ui.js b/js/ui.js index ef8041b..179c745 100644 --- a/js/ui.js +++ b/js/ui.js @@ -259,13 +259,14 @@ export class UIRenderer { root.style.removeProperty('--ring'); } - showFullscreenCover(track) { + showFullscreenCover(track, nextTrack) { if (!track) return; const overlay = document.getElementById('fullscreen-cover-overlay'); const image = document.getElementById('fullscreen-cover-image'); const title = document.getElementById('fullscreen-track-title'); const artist = document.getElementById('fullscreen-track-artist'); + const nextTrackEl = document.getElementById('fullscreen-next-track'); const coverUrl = this.api.getCoverUrl(track.album?.cover, '1280'); @@ -273,6 +274,19 @@ export class UIRenderer { title.textContent = track.title; artist.textContent = track.artist?.name || 'Unknown Artist'; + if (nextTrack) { + nextTrackEl.style.display = 'flex'; + nextTrackEl.querySelector('.value').textContent = `${nextTrack.title} • ${nextTrack.artist?.name || 'Unknown'}`; + + // Replay animation + nextTrackEl.classList.remove('animate-in'); + void nextTrackEl.offsetWidth; // Trigger reflow + nextTrackEl.classList.add('animate-in'); + } else { + nextTrackEl.style.display = 'none'; + nextTrackEl.classList.remove('animate-in'); + } + // Set the background image via CSS variable for the pseudo-element to use overlay.style.setProperty('--bg-image', `url('${coverUrl}')`); diff --git a/styles.css b/styles.css index 6575356..61543d0 100644 --- a/styles.css +++ b/styles.css @@ -1319,6 +1319,33 @@ input:checked + .slider::before { text-shadow: 0 2px 10px rgba(0,0,0,0.5); } +#fullscreen-next-track { + margin-top: 1.5rem; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.6); + text-shadow: 0 1px 4px rgba(0,0,0,0.5); + display: flex; + flex-direction: column; + gap: 0.2rem; + opacity: 0; /* Initially hidden for animation */ +} + +#fullscreen-next-track.animate-in { + animation: fadeIn 0.5s ease 0.2s forwards; /* Added forwards to keep opacity 1 */ +} + +#fullscreen-next-track .label { + text-transform: uppercase; + letter-spacing: 0.05em; + font-size: 0.75rem; + opacity: 0.8; +} + +#fullscreen-next-track .value { + font-weight: 500; + color: rgba(255, 255, 255, 0.9); +} + @media (max-width: 768px) { #fullscreen-cover-overlay { padding-bottom: 160px; /* Account for taller mobile player bar */ From 66e384a59199385227833980e4873b5a1ebda826 Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Tue, 23 Dec 2025 23:31:37 +0100 Subject: [PATCH 4/7] fix: improve contrast and theming for enlarged cover view and hover colors --- js/ui.js | 15 +++++++++++++++ styles.css | 32 ++++++++++++++++++++++---------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/js/ui.js b/js/ui.js index 179c745..78c067c 100644 --- a/js/ui.js +++ b/js/ui.js @@ -216,8 +216,10 @@ export class UIRenderer { if (backgroundSettings.isEnabled() && imageUrl) { bgElement.style.backgroundImage = `url('${imageUrl}')`; bgElement.classList.add('active'); + document.body.classList.add('has-page-background'); } else { bgElement.classList.remove('active'); + document.body.classList.remove('has-page-background'); // Delay clearing the image to allow transition setTimeout(() => { if (!bgElement.classList.contains('active')) { @@ -247,6 +249,18 @@ export class UIRenderer { root.style.setProperty('--highlight-rgb', `${r}, ${g}, ${b}`); root.style.setProperty('--active-highlight', color); root.style.setProperty('--ring', color); + + // Calculate a safe hover color (darken if too light) + let hoverColor; + if (brightness > 200) { + const dr = Math.floor(r * 0.85); + const dg = Math.floor(g * 0.85); + const db = Math.floor(b * 0.85); + hoverColor = `rgba(${dr}, ${dg}, ${db}, 0.25)`; + } else { + hoverColor = `rgba(${r}, ${g}, ${b}, 0.15)`; + } + root.style.setProperty('--track-hover-bg', hoverColor); } resetVibrantColor() { @@ -257,6 +271,7 @@ export class UIRenderer { root.style.removeProperty('--highlight-rgb'); root.style.removeProperty('--active-highlight'); root.style.removeProperty('--ring'); + root.style.removeProperty('--track-hover-bg'); } showFullscreenCover(track, nextTrack) { diff --git a/styles.css b/styles.css index 61543d0..06ed8e8 100644 --- a/styles.css +++ b/styles.css @@ -12,6 +12,7 @@ --shadow-md: 0 6px 16px rgba(0, 0, 0, 0.2); --shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.5); --shadow-xl: 0 20px 60px rgba(0, 0, 0, 0.8); + --cover-filter: blur(30px) brightness(0.3); } :root[data-theme="monochrome"] { @@ -32,6 +33,7 @@ --highlight-rgb: 255, 255, 255; --active-highlight: var(--highlight); --explicit-badge: #fafafa; + --cover-filter: blur(30px) brightness(0.3); } :root[data-theme="dark"] { @@ -133,6 +135,7 @@ --active-highlight: var(--highlight); --explicit-badge: #ef4444; --explicit-badge-foreground: #ffffff; + --cover-filter: blur(30px) brightness(1.1) opacity(0.8); } *, *::before, *::after { @@ -373,6 +376,14 @@ kbd { border-color: var(--ring); } +body.has-page-background .search-bar input { + background-color: var(--background); +} + +body.has-page-background .track-item:hover { + background-color: var(--track-hover-bg, var(--secondary)); +} + .page { display: none; } @@ -1234,7 +1245,7 @@ input:checked + .slider::before { justify-content: center; animation: fadeIn 0.3s ease; overflow: hidden; - background-color: #000; /* Solid background to prevent see-through */ + background-color: var(--background); /* Use a CSS variable for the image so we can set it in JS */ --bg-image: none; padding-bottom: 90px; /* Account for desktop player bar */ @@ -1247,7 +1258,7 @@ input:checked + .slider::before { background-size: cover; background-position: center; background-repeat: no-repeat; - filter: blur(30px) brightness(0.4); + filter: var(--cover-filter); z-index: -1; background-image: var(--bg-image); } @@ -1268,9 +1279,9 @@ input:checked + .slider::before { position: absolute; top: 1rem; right: 1rem; - background: rgba(0, 0, 0, 0.5); + background-color: var(--background); border: none; - color: #fff; + color: var(--foreground); font-size: 2rem; width: 48px; height: 48px; @@ -1279,12 +1290,13 @@ input:checked + .slider::before { display: flex; align-items: center; justify-content: center; - transition: background-color 0.2s; + transition: opacity 0.2s; z-index: 10; + opacity: 0.5; } #close-fullscreen-cover-btn:hover { - background-color: rgba(0, 0, 0, 0.8); + opacity: 0.8; } #fullscreen-cover-image { @@ -1307,14 +1319,14 @@ input:checked + .slider::before { font-size: 2rem; font-weight: 700; margin-bottom: 0.5rem; - color: #ffffff; + color: var(--foreground); text-shadow: 0 2px 10px rgba(0,0,0,0.5); word-break: break-word; } #fullscreen-track-artist { font-size: 1.25rem; - color: rgba(255, 255, 255, 0.8); + color: var(--muted-foreground); font-weight: 500; text-shadow: 0 2px 10px rgba(0,0,0,0.5); } @@ -1322,7 +1334,7 @@ input:checked + .slider::before { #fullscreen-next-track { margin-top: 1.5rem; font-size: 0.9rem; - color: rgba(255, 255, 255, 0.6); + color: var(--muted-foreground); text-shadow: 0 1px 4px rgba(0,0,0,0.5); display: flex; flex-direction: column; @@ -1343,7 +1355,7 @@ input:checked + .slider::before { #fullscreen-next-track .value { font-weight: 500; - color: rgba(255, 255, 255, 0.9); + color: var(--foreground); } @media (max-width: 768px) { From 2564b809e60e89963e896eabe6eb3a7612a9a1de Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Tue, 23 Dec 2025 23:37:32 +0100 Subject: [PATCH 5/7] FIX: playlist button colors --- index.html | 2 +- styles.css | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/index.html b/index.html index 53b2a85..da63e13 100644 --- a/index.html +++ b/index.html @@ -205,7 +205,7 @@ Play -