Merge pull request #15 from JulienMaille/cover-color

Cover color/background and full screen cover option
This commit is contained in:
Samidy 2025-12-24 01:17:49 -08:00 committed by GitHub
commit 3db7a04971
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 455 additions and 20 deletions

View file

@ -35,6 +35,25 @@
<div id="queue-list"></div>
</div>
</div>
<div id="fullscreen-cover-overlay" style="display: none;">
<div class="fullscreen-cover-content">
<button id="close-fullscreen-cover-btn" title="Close">&times;</button>
<img id="fullscreen-cover-image" src="" alt="Album Cover">
<div class="fullscreen-track-info">
<h2 id="fullscreen-track-title"></h2>
<h3 id="fullscreen-track-artist"></h3>
<div id="fullscreen-next-track" style="display: none;">
<span class="label">Up Next: </span>
<span class="value"></span>
</div>
</div>
<div class="fullscreen-controls">
<!-- Controls will be cloned or managed here if needed, or we just rely on main controls -->
</div>
</div>
</div>
<div class="queue-track-menu" id="queue-track-menu">
<ul>
<li data-action="remove">Remove from Queue</li>
@ -88,6 +107,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">
@ -185,7 +205,7 @@
</svg>
<span>Play</span>
</button>
<button id="download-playlist-btn" class="btn-secondary">
<button id="download-playlist-btn" class="btn-primary">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
@ -298,7 +318,8 @@
<span class="description">Choose what shows when you click the album art</span>
</div>
<select id="now-playing-mode">
<option value="cover">Album Cover</option>
<option value="album">Show Album</option>
<option value="cover">Enlarged Cover</option>
<option value="lyrics">Lyrics Panel</option>
<option value="karaoke">Karaoke Mode</option>
</select>
@ -313,6 +334,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

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

View file

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

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

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

147
js/ui.js
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,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}`;

View file

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