diff --git a/index.html b/index.html index e0c550c..86e68be 100644 --- a/index.html +++ b/index.html @@ -88,6 +88,7 @@ + @@ -313,6 +314,16 @@ + + + Album Cover Background + Use the album cover as a blurred background on album pages + + + + + + Gapless Playback diff --git a/js/app.js b/js/app.js index 44455fb..89af530 100644 --- a/js/app.js +++ b/js/app.js @@ -278,6 +278,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; 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..3c7250c 100644 --- a/js/storage.js +++ b/js/storage.js @@ -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..78fb887 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,54 @@ export class UIRenderer { }); } + setPageBackground(imageUrl) { + const bgElement = document.getElementById('page-background'); + if (backgroundSettings.isEnabled() && imageUrl) { + bgElement.style.backgroundImage = `url('${imageUrl}')`; + bgElement.classList.add('active'); + } else { + bgElement.classList.remove('active'); + // 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); + } + + 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'); + } + showPage(pageId) { document.querySelectorAll('.page').forEach(page => { page.classList.toggle('active', page.id === `page-${pageId}`); @@ -195,6 +270,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 +412,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..f945bcb 100644 --- a/styles.css +++ b/styles.css @@ -206,6 +206,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 {