feat: implement album cover background and vibrant colors
This commit is contained in:
parent
bb63ce6ccb
commit
95559f6614
6 changed files with 175 additions and 3 deletions
11
index.html
11
index.html
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
||||
|
|
|
|||
94
js/ui.js
94
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}`;
|
||||
|
||||
|
|
|
|||
42
styles.css
42
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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue