feat: implement album cover background and vibrant colors

This commit is contained in:
Julien Maille 2025-12-23 22:02:26 +01:00
parent bb63ce6ccb
commit 95559f6614
6 changed files with 175 additions and 3 deletions

View file

@ -88,6 +88,7 @@
</aside>
<main class="main-content">
<div id="page-background"></div>
<header class="main-header">
<button class="hamburger-menu" id="hamburger-btn" title="Open navigation">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@ -313,6 +314,16 @@
<span class="slider"></span>
</label>
</div>
<div class="setting-item">
<div class="info">
<span class="label">Album Cover Background</span>
<span class="description">Use the album cover as a blurred background on album pages</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="album-background-toggle">
<span class="slider"></span>
</label>
</div>
<div class="setting-item">
<div class="info">
<span class="label">Gapless Playback</span>

View file

@ -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;

View file

@ -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) {

View file

@ -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',

View file

@ -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}`;

View file

@ -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 {