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>
|
</aside>
|
||||||
|
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
|
<div id="page-background"></div>
|
||||||
<header class="main-header">
|
<header class="main-header">
|
||||||
<button class="hamburger-menu" id="hamburger-btn" title="Open navigation">
|
<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">
|
<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>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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="setting-item">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<span class="label">Gapless Playback</span>
|
<span class="label">Gapless Playback</span>
|
||||||
|
|
|
||||||
|
|
@ -278,6 +278,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
audioPlayer.addEventListener('play', async () => {
|
audioPlayer.addEventListener('play', async () => {
|
||||||
if (!player.currentTrack) return;
|
if (!player.currentTrack) return;
|
||||||
|
|
||||||
|
// Update UI with current track info for theme
|
||||||
|
ui.setCurrentTrack(player.currentTrack);
|
||||||
|
|
||||||
const currentTrackId = player.currentTrack.id;
|
const currentTrackId = player.currentTrack.id;
|
||||||
if (currentTrackId === previousTrackId) return;
|
if (currentTrackId === previousTrackId) return;
|
||||||
previousTrackId = currentTrackId;
|
previousTrackId = currentTrackId;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//js/settings
|
//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) {
|
export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
const lastfmConnectBtn = document.getElementById('lastfm-connect-btn');
|
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
|
// Filename template setting
|
||||||
const filenameTemplate = document.getElementById('filename-template');
|
const filenameTemplate = document.getElementById('filename-template');
|
||||||
if (filenameTemplate) {
|
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 = {
|
export const queueManager = {
|
||||||
STORAGE_KEY: 'monochrome-queue',
|
STORAGE_KEY: 'monochrome-queue',
|
||||||
|
|
||||||
|
|
|
||||||
94
js/ui.js
94
js/ui.js
|
|
@ -1,10 +1,37 @@
|
||||||
//js/ui.js
|
//js/ui.js
|
||||||
import { formatTime, createPlaceholder, trackDataStore, hasExplicitContent, getTrackArtists, getTrackTitle, calculateTotalDuration, formatDuration } from './utils.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 {
|
export class UIRenderer {
|
||||||
constructor(api) {
|
constructor(api) {
|
||||||
this.api = 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() {
|
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) {
|
showPage(pageId) {
|
||||||
document.querySelectorAll('.page').forEach(page => {
|
document.querySelectorAll('.page').forEach(page => {
|
||||||
page.classList.toggle('active', page.id === `page-${pageId}`);
|
page.classList.toggle('active', page.id === `page-${pageId}`);
|
||||||
|
|
@ -195,6 +270,12 @@ export class UIRenderer {
|
||||||
|
|
||||||
document.querySelector('.main-content').scrollTop = 0;
|
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') {
|
if (pageId === 'settings') {
|
||||||
this.renderApiSettings();
|
this.renderApiSettings();
|
||||||
}
|
}
|
||||||
|
|
@ -331,9 +412,18 @@ export class UIRenderer {
|
||||||
try {
|
try {
|
||||||
const { album, tracks } = await this.api.getAlbum(albumId);
|
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 = '';
|
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() : '';
|
const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : '';
|
||||||
titleEl.innerHTML = `${album.title} ${explicitBadge}`;
|
titleEl.innerHTML = `${album.title} ${explicitBadge}`;
|
||||||
|
|
||||||
|
|
|
||||||
42
styles.css
42
styles.css
|
|
@ -206,6 +206,48 @@ kbd {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl);
|
||||||
scroll-behavior: smooth;
|
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 {
|
.now-playing-bar {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue