Add animated mosaic backgrounds to wishlist categories

Introduces a dynamic mosaic grid background with scrolling album covers for the wishlist category cards in the overview modal. Adds supporting JavaScript functions to extract unique cover images and generate the mosaic HTML, and updates CSS to style and animate the mosaic, including fallbacks and hover effects for improved visual appeal.
This commit is contained in:
Broque Thomas 2025-12-12 23:02:28 -08:00
parent fca3e641dc
commit e9c9021db0
2 changed files with 255 additions and 11 deletions

View file

@ -5035,6 +5035,106 @@ async function closeDownloadMissingModal(playlistId) {
}
}
/**
* Extract unique album cover images from tracks
*/
function extractUniqueCoverImages(tracks, maxCovers = 20) {
const uniqueCovers = new Set();
const covers = [];
for (const track of tracks) {
if (covers.length >= maxCovers) break;
let coverUrl = null;
let spotifyData = track.spotify_data;
// Parse spotify_data if it's a string
if (typeof spotifyData === 'string') {
try {
spotifyData = JSON.parse(spotifyData);
} catch (e) {
continue;
}
}
// Extract cover URL
coverUrl = spotifyData?.album?.images?.[0]?.url;
// Add to list if unique and valid
if (coverUrl && !uniqueCovers.has(coverUrl)) {
uniqueCovers.add(coverUrl);
covers.push(coverUrl);
}
}
return covers;
}
/**
* Shuffle array using Fisher-Yates algorithm
*/
function shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
/**
* Generate mosaic grid background HTML with continuous scrolling rows
*/
function generateMosaicBackground(coverUrls) {
// If less than 3 covers, use gradient fallback
if (!coverUrls || coverUrls.length < 3) {
return `
<div class="wishlist-mosaic-fallback"></div>
<div class="wishlist-mosaic-overlay"></div>
`;
}
const rows = 4;
let mosaicHTML = '<div class="wishlist-mosaic-background">';
// Calculate scroll speed based on number of images
// More images = longer duration to maintain consistent visual speed
// Minimum 40s to prevent scrolling too fast
const scrollSpeed = Math.max(40, coverUrls.length * 2);
for (let row = 0; row < rows; row++) {
const isEvenRow = row % 2 === 0;
const direction = isEvenRow ? 'left' : 'right';
// Randomize order for each row
const shuffledCovers = shuffleArray(coverUrls);
// Create row wrapper
mosaicHTML += `<div class="wishlist-mosaic-row-wrapper">`;
mosaicHTML += `<div class="wishlist-mosaic-row scroll-${direction}" style="--speed: ${scrollSpeed}s;">`;
// Generate tiles - duplicate 3 times for smooth infinite scroll
for (let duplicate = 0; duplicate < 3; duplicate++) {
for (let i = 0; i < shuffledCovers.length; i++) {
const coverUrl = shuffledCovers[i];
mosaicHTML += `
<div class="wishlist-mosaic-tile">
<div class="wishlist-mosaic-image" style="background-image: url('${coverUrl}');"></div>
</div>
`;
}
}
mosaicHTML += '</div>'; // Close row
mosaicHTML += '</div>'; // Close wrapper
}
mosaicHTML += '</div>';
mosaicHTML += '<div class="wishlist-mosaic-overlay"></div>'; // Dark overlay for readability
return mosaicHTML;
}
/**
* Open wishlist overview modal showing category breakdown
* This is the NEW entry point for wishlist from dashboard
@ -5059,6 +5159,16 @@ async function openWishlistOverviewModal() {
return;
}
// Fetch album covers for mosaic backgrounds
const albumCoversPromise = fetch('/api/wishlist/tracks?category=albums').then(r => r.json());
const singleCoversPromise = fetch('/api/wishlist/tracks?category=singles').then(r => r.json());
const [albumTracksData, singleTracksData] = await Promise.all([albumCoversPromise, singleCoversPromise]);
// Extract unique album covers (max 20 per category)
const albumCovers = extractUniqueCoverImages(albumTracksData.tracks || [], 20);
const singleCovers = extractUniqueCoverImages(singleTracksData.tracks || [], 20);
// Create modal if it doesn't exist
let modal = document.getElementById('wishlist-overview-modal');
if (!modal) {
@ -5095,18 +5205,24 @@ async function openWishlistOverviewModal() {
<div class="wishlist-category-grid">
<!-- Albums/EPs Category -->
<div class="wishlist-category-card ${currentCycle === 'albums' ? 'next-in-queue' : ''}" data-category="albums" onclick="selectWishlistCategory('albums')">
<div class="wishlist-category-icon">💿</div>
<div class="wishlist-category-title">Albums / EPs</div>
<div class="wishlist-category-count">${albums} tracks</div>
${currentCycle === 'albums' ? '<div class="wishlist-category-badge">Next in Queue</div>' : ''}
${generateMosaicBackground(albumCovers)}
<div class="wishlist-category-content">
<div class="wishlist-category-icon">💿</div>
<div class="wishlist-category-title">Albums / EPs</div>
<div class="wishlist-category-count">${albums} tracks</div>
${currentCycle === 'albums' ? '<div class="wishlist-category-badge">Next in Queue</div>' : ''}
</div>
</div>
<!-- Singles Category -->
<div class="wishlist-category-card ${currentCycle === 'singles' ? 'next-in-queue' : ''}" data-category="singles" onclick="selectWishlistCategory('singles')">
<div class="wishlist-category-icon">🎵</div>
<div class="wishlist-category-title">Singles</div>
<div class="wishlist-category-count">${singles} tracks</div>
${currentCycle === 'singles' ? '<div class="wishlist-category-badge">Next in Queue</div>' : ''}
${generateMosaicBackground(singleCovers)}
<div class="wishlist-category-content">
<div class="wishlist-category-icon">🎵</div>
<div class="wishlist-category-title">Singles</div>
<div class="wishlist-category-count">${singles} tracks</div>
${currentCycle === 'singles' ? '<div class="wishlist-category-badge">Next in Queue</div>' : ''}
</div>
</div>
</div>

View file

@ -7627,6 +7627,122 @@ body {
transition: all 0.3s ease;
position: relative;
overflow: hidden;
min-height: 250px;
display: flex;
align-items: center;
justify-content: center;
}
/* Mosaic Background with Scrolling Rows */
.wishlist-mosaic-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
z-index: 0;
}
.wishlist-mosaic-row-wrapper {
flex: 1;
overflow: hidden;
position: relative;
}
.wishlist-mosaic-row {
display: flex;
width: fit-content;
height: 100%;
opacity: 0;
animation: fadeInRow 1s ease-out forwards, infiniteScroll var(--speed, 30s) linear infinite;
}
.wishlist-mosaic-row.scroll-right {
animation-direction: normal, reverse;
}
.wishlist-mosaic-tile {
width: 80px;
height: 100%;
flex-shrink: 0;
overflow: hidden;
}
.wishlist-mosaic-image {
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
transition: transform 0.5s ease;
}
/* Fallback gradient for small collections */
.wishlist-mosaic-fallback {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg,
rgba(29, 185, 84, 0.1) 0%,
rgba(138, 43, 226, 0.1) 50%,
rgba(255, 20, 147, 0.1) 100%);
background-size: 200% 200%;
animation: gradientShift 8s ease infinite;
z-index: 0;
}
@keyframes gradientShift {
0%, 100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
/* Overlay for text readability */
.wishlist-mosaic-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg,
rgba(0, 0, 0, 0.85) 0%,
rgba(0, 0, 0, 0.75) 100%);
z-index: 1;
pointer-events: none;
}
/* Content layer above mosaic */
.wishlist-category-content {
position: relative;
z-index: 2;
width: 100%;
}
/* Fade in rows initially */
@keyframes fadeInRow {
0% {
opacity: 0;
}
100% {
opacity: 0.7;
}
}
/* Infinite scrolling animation */
/* Content is duplicated 3x, animation moves by -33.333% (one set) for seamless loop */
@keyframes infiniteScroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-33.333%);
}
}
.wishlist-category-card::before {
@ -7646,14 +7762,21 @@ body {
}
.wishlist-category-card:hover {
background: linear-gradient(135deg,
rgba(29, 185, 84, 0.1) 0%,
rgba(18, 18, 18, 0.98) 100%);
border-color: rgba(29, 185, 84, 0.3);
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(29, 185, 84, 0.2);
}
.wishlist-category-card:hover .wishlist-mosaic-image {
transform: scale(1.1);
}
.wishlist-category-card:hover .wishlist-mosaic-overlay {
background: linear-gradient(135deg,
rgba(29, 185, 84, 0.2) 0%,
rgba(0, 0, 0, 0.8) 100%);
}
.wishlist-category-card.next-in-queue {
border-color: rgba(29, 185, 84, 0.4);
background: linear-gradient(135deg,
@ -8101,6 +8224,11 @@ body {
.wishlist-category-card {
padding: 24px 20px;
min-height: 200px;
}
.wishlist-mosaic-tile {
width: 60px; /* Smaller tiles on mobile */
}
.wishlist-category-icon {