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:
parent
fca3e641dc
commit
e9c9021db0
2 changed files with 255 additions and 11 deletions
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue