diff --git a/index.html b/index.html
index e0c550c..da63e13 100644
--- a/index.html
+++ b/index.html
@@ -35,6 +35,25 @@
Gapless Playback
diff --git a/js/app.js b/js/app.js
index 44455fb..40feda1 100644
--- a/js/app.js
+++ b/js/app.js
@@ -251,9 +251,30 @@ document.addEventListener('DOMContentLoaded', async () => {
// Clear sync when hiding
clearLyricsPanelSync(audioPlayer, lyricsPanel);
}
+ } else if (mode === 'cover') {
+ const overlay = document.getElementById('fullscreen-cover-overlay');
+ if (overlay && overlay.style.display === 'flex') {
+ ui.closeFullscreenCover();
+ } else {
+ const nextTrack = player.getNextTrack();
+ ui.showFullscreenCover(player.currentTrack, nextTrack);
+ }
+ } 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('fullscreen-cover-image')?.addEventListener('click', () => {
+ ui.closeFullscreenCover();
+ });
+
document.getElementById('close-lyrics-btn')?.addEventListener('click', (e) => {
e.stopPropagation();
lyricsPanel.classList.add('hidden');
@@ -278,6 +299,9 @@ document.addEventListener('DOMContentLoaded', async () => {
audioPlayer.addEventListener('play', async () => {
if (!player.currentTrack) return;
+ // Update UI with current track info for theme
+ ui.setCurrentTrack(player.currentTrack);
+
const currentTrackId = player.currentTrack.id;
if (currentTrackId === previousTrackId) return;
previousTrackId = currentTrackId;
@@ -301,6 +325,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/settings.js b/js/settings.js
index 9204c21..e8dd8ec 100644
--- a/js/settings.js
+++ b/js/settings.js
@@ -1,5 +1,5 @@
//js/settings
-import { themeManager, lastFMStorage, nowPlayingSettings, lyricsSettings } from './storage.js';
+import { themeManager, lastFMStorage, nowPlayingSettings, lyricsSettings, backgroundSettings } from './storage.js';
export function initializeSettings(scrobbler, player, api, ui) {
const lastfmConnectBtn = document.getElementById('lastfm-connect-btn');
@@ -185,6 +185,15 @@ export function initializeSettings(scrobbler, player, api, ui) {
});
}
+ // Album Background Toggle
+ const albumBackgroundToggle = document.getElementById('album-background-toggle');
+ if (albumBackgroundToggle) {
+ albumBackgroundToggle.checked = backgroundSettings.isEnabled();
+ albumBackgroundToggle.addEventListener('change', (e) => {
+ backgroundSettings.setEnabled(e.target.checked);
+ });
+ }
+
// Filename template setting
const filenameTemplate = document.getElementById('filename-template');
if (filenameTemplate) {
diff --git a/js/storage.js b/js/storage.js
index c8fcfec..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';
}
},
@@ -332,6 +332,23 @@ export const lyricsSettings = {
}
};
+export const backgroundSettings = {
+ STORAGE_KEY: 'album-background-enabled',
+
+ isEnabled() {
+ try {
+ // Default to true if not set
+ return localStorage.getItem(this.STORAGE_KEY) !== 'false';
+ } catch (e) {
+ return true;
+ }
+ },
+
+ setEnabled(enabled) {
+ localStorage.setItem(this.STORAGE_KEY, enabled ? 'true' : 'false');
+ }
+};
+
export const queueManager = {
STORAGE_KEY: 'monochrome-queue',
diff --git a/js/ui.js b/js/ui.js
index 4c1aa43..78c067c 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -1,10 +1,37 @@
//js/ui.js
import { formatTime, createPlaceholder, trackDataStore, hasExplicitContent, getTrackArtists, getTrackTitle, calculateTotalDuration, formatDuration } from './utils.js';
-import { recentActivityManager } from './storage.js';
+import { recentActivityManager, backgroundSettings } from './storage.js';
export class UIRenderer {
constructor(api) {
this.api = api;
+ this.currentTrack = null;
+ }
+
+ setCurrentTrack(track) {
+ this.currentTrack = track;
+ this.updateGlobalTheme();
+ }
+
+ updateGlobalTheme() {
+ // If the album background setting is disabled, we don't do global coloring
+ // except possibly for the album page which handles its own check.
+ // But here we are handling the "not on album page" case or general updates.
+
+ // Check if we are currently viewing an album page
+ const isAlbumPage = document.getElementById('page-album').classList.contains('active');
+
+ if (isAlbumPage) {
+ // The album page render logic handles its own coloring.
+ // We shouldn't override it here.
+ return;
+ }
+
+ if (backgroundSettings.isEnabled() && this.currentTrack?.album?.vibrantColor) {
+ this.setVibrantColor(this.currentTrack.album.vibrantColor);
+ } else {
+ this.resetVibrantColor();
+ }
}
createExplicitBadge() {
@@ -184,6 +211,107 @@ export class UIRenderer {
});
}
+ setPageBackground(imageUrl) {
+ const bgElement = document.getElementById('page-background');
+ 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')) {
+ bgElement.style.backgroundImage = '';
+ }
+ }, 500);
+ }
+ }
+
+ setVibrantColor(color) {
+ if (!color) return;
+
+ const root = document.documentElement;
+
+ // Calculate contrast text color
+ const hex = color.replace('#', '');
+ const r = parseInt(hex.substr(0, 2), 16);
+ const g = parseInt(hex.substr(2, 2), 16);
+ const b = parseInt(hex.substr(4, 2), 16);
+ const brightness = ((r * 299) + (g * 587) + (b * 114)) / 1000;
+ const foreground = brightness > 128 ? '#000000' : '#ffffff';
+
+ // Set global CSS variables
+ root.style.setProperty('--primary', color);
+ root.style.setProperty('--primary-foreground', foreground);
+ root.style.setProperty('--highlight', color);
+ 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() {
+ const root = document.documentElement;
+ root.style.removeProperty('--primary');
+ root.style.removeProperty('--primary-foreground');
+ root.style.removeProperty('--highlight');
+ root.style.removeProperty('--highlight-rgb');
+ root.style.removeProperty('--active-highlight');
+ root.style.removeProperty('--ring');
+ root.style.removeProperty('--track-hover-bg');
+ }
+
+ 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');
+
+ image.src = coverUrl;
+ 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}')`);
+
+ 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}`);
@@ -195,6 +323,12 @@ export class UIRenderer {
document.querySelector('.main-content').scrollTop = 0;
+ // Clear background and color if not on album page
+ if (pageId !== 'album') {
+ this.setPageBackground(null);
+ this.updateGlobalTheme();
+ }
+
if (pageId === 'settings') {
this.renderApiSettings();
}
@@ -331,9 +465,18 @@ export class UIRenderer {
try {
const { album, tracks } = await this.api.getAlbum(albumId);
- imageEl.src = this.api.getCoverUrl(album.cover, '1280');
+ const coverUrl = this.api.getCoverUrl(album.cover, '1280');
+ imageEl.src = coverUrl;
imageEl.style.backgroundColor = '';
+ // Set background and vibrant color
+ this.setPageBackground(coverUrl);
+ if (backgroundSettings.isEnabled() && album.vibrantColor) {
+ this.setVibrantColor(album.vibrantColor);
+ } else {
+ this.resetVibrantColor();
+ }
+
const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : '';
titleEl.innerHTML = `${album.title} ${explicitBadge}`;
diff --git a/styles.css b/styles.css
index 04ea83e..df56d4f 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.6) opacity(0.85);
}
*, *::before, *::after {
@@ -206,6 +209,48 @@ kbd {
overflow-y: auto;
padding: var(--spacing-xl);
scroll-behavior: smooth;
+ position: relative; /* Context for background */
+}
+
+/* Ensure content sits on top of background */
+.main-header,
+.page {
+ position: relative;
+ z-index: 1;
+}
+
+#page-background {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 60vh;
+ min-height: 400px;
+ z-index: 0;
+ background-size: cover;
+ background-position: center 20%;
+ background-repeat: no-repeat;
+ opacity: 0;
+ transition: opacity 0.5s ease-in-out;
+
+ /* Fade out at the bottom */
+ mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,0.8) 40%, rgba(0,0,0,0) 100%);
+ -webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,0.8) 40%, rgba(0,0,0,0) 100%);
+
+ /* Blur effect */
+ filter: blur(50px) saturate(1.4) brightness(0.5);
+ pointer-events: none;
+}
+
+#page-background.active {
+ opacity: 1;
+}
+
+/* Light mode adjustments */
+:root[data-theme="light"] #page-background {
+ filter: blur(50px) saturate(1.5) brightness(1.1) opacity(0.5);
+ mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%);
+ -webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%);
}
.now-playing-bar {
@@ -217,6 +262,8 @@ kbd {
grid-template-columns: 1fr 2fr 1fr;
align-items: center;
gap: var(--spacing-xl);
+ position: relative;
+ z-index: 2100;
}
.sidebar-logo {
@@ -329,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;
}
@@ -1174,12 +1229,157 @@ input:checked + .slider::before {
inset: 0;
background-color: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
- z-index: 1000;
+ z-index: 3000;
justify-content: center;
align-items: center;
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: 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 */
+}
+
+#fullscreen-cover-overlay::before {
+ content: "";
+ position: absolute;
+ inset: -20px;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ filter: var(--cover-filter);
+ 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-color: var(--background);
+ border: none;
+ color: var(--foreground);
+ font-size: 2rem;
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: opacity 0.2s;
+ z-index: 10;
+ opacity: 0.5;
+}
+
+#close-fullscreen-cover-btn:hover {
+ opacity: 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%;
+ background: color-mix(in srgb, var(--card), transparent 25%);
+ padding: 1.5rem 2rem;
+ border-radius: var(--radius);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ border: 1px solid color-mix(in srgb, var(--border), transparent 50%);
+}
+
+#fullscreen-track-title {
+ font-size: 2rem;
+ font-weight: 700;
+ margin-bottom: 0.5rem;
+ color: var(--foreground);
+ word-break: break-word;
+}
+
+#fullscreen-track-artist {
+ font-size: 1.25rem;
+ color: var(--muted-foreground);
+ font-weight: 500;
+}
+
+#fullscreen-next-track {
+ margin-top: 1.5rem;
+ font-size: 0.9rem;
+ color: var(--muted-foreground);
+ 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: var(--foreground);
+}
+
+@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%;
@@ -2385,12 +2585,13 @@ input:checked + .slider::before {
display: flex;
flex-direction: column;
transform: translateX(100%);
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- box-shadow: var(--shadow-xl);
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease;
+ box-shadow: none;
}
.lyrics-panel:not(.hidden) {
transform: translateX(0);
+ box-shadow: var(--shadow-xl);
}
.lyrics-header {
@@ -2712,16 +2913,6 @@ input:checked + .slider::before {
box-shadow: var(--shadow-sm);
}
-#play-playlist-btn {
- background-color: var(--primary);
- color: var(--primary-foreground);
-}
-
-#download-playlist-btn {
- background-color: var(--secondary);
- color: var(--foreground);
-}
-
#play-playlist-btn:hover,
#download-playlist-btn:hover {
transform: scale(1.05);