Wishlist Nebula — expanded view redesign, live processing, download flow
Expanded albums: click album tile to reveal track list with per-track remove buttons. Art shrinks to banner, title/count move to static position, tracks scroll at 200px max-height. Handles many albums with flex-wrap and align-items: flex-start. Expanded singles: visible labels below 44px art circles, remove button on hover. No longer tooltip-only. Live processing: polls wishlist stats every 5s, detects auto-processing and manual download batches. Orbs pulse with accent glow during active processing. Nebula auto-refreshes when tracks complete (count decreases). Polling stops on page navigation. Download flow: single "Download Wishlist" button opens category choice dialog (Albums/Singles with counts). If processing is already active, shows toast or reopens existing download modal. Toned down processing pulse animation (3s cycle, scale 1.03, brightness 1.1).
This commit is contained in:
parent
16ad6184ed
commit
f59c564382
3 changed files with 488 additions and 42 deletions
|
|
@ -6777,16 +6777,10 @@
|
|||
<!-- Action bar -->
|
||||
<div class="wl-nebula-bar">
|
||||
<input type="text" class="wl-nebula-search" id="wl-nebula-search" placeholder="Search artists, albums..." oninput="_filterNebula()">
|
||||
<div class="wl-nebula-bar-actions">
|
||||
<button class="watchlist-action-btn watchlist-action-primary" onclick="selectWishlistCategory('albums')">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Albums
|
||||
</button>
|
||||
<button class="watchlist-action-btn watchlist-action-secondary" onclick="selectWishlistCategory('singles')">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Singles
|
||||
</button>
|
||||
</div>
|
||||
<button class="watchlist-action-btn watchlist-action-primary" onclick="_nebulaDownload()">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Download Wishlist
|
||||
</button>
|
||||
</div>
|
||||
<!-- Artist orbs container -->
|
||||
<div class="wl-nebula-field" id="wl-nebula-field">
|
||||
|
|
|
|||
|
|
@ -2974,6 +2974,7 @@ async function loadPageData(pageId) {
|
|||
// Stop watchlist/wishlist page timers when navigating away
|
||||
if (watchlistCountdownInterval) { clearInterval(watchlistCountdownInterval); watchlistCountdownInterval = null; }
|
||||
if (wishlistCountdownInterval) { clearInterval(wishlistCountdownInterval); wishlistCountdownInterval = null; }
|
||||
if (typeof _stopNebulaLivePolling === 'function') _stopNebulaLivePolling();
|
||||
switch (pageId) {
|
||||
case 'dashboard':
|
||||
await loadDashboardData();
|
||||
|
|
@ -40535,6 +40536,10 @@ async function initializeWishlistPage() {
|
|||
|
||||
_renderWishlistNebula(albumRes.tracks || [], singleRes.tracks || [], _artistImageMap, currentCycle);
|
||||
startWishlistCountdownTimer(currentCycle, statsRes.next_run_in_seconds || 0);
|
||||
|
||||
// Live processing: check if wishlist download is active and start polling
|
||||
_startNebulaLivePolling(currentCycle, _artistImageMap);
|
||||
|
||||
wishlistPageState.isInitialized = true;
|
||||
|
||||
} catch (error) {
|
||||
|
|
@ -40629,23 +40634,34 @@ function _renderWishlistNebula(albumTracks, singleTracks, artistImageMap, curren
|
|||
// Expanded content
|
||||
html += `<div class="wl-orb-expanded">`;
|
||||
if (data.albums.size > 0) {
|
||||
html += `<div class="wl-orb-albums">`;
|
||||
html += `<div class="wl-album-fan">`;
|
||||
for (const [an, ad] of data.albums) {
|
||||
html += `<div class="wl-satellite" data-album="${escapeHtml(an)}">`;
|
||||
html += `<div class="wl-satellite-art">${ad.image ? `<img src="${ad.image}" alt="">` : ''}</div>`;
|
||||
html += `<div class="wl-satellite-info"><div class="wl-satellite-name">${escapeHtml(an)}</div><div class="wl-satellite-count">${ad.tracks.length} tracks</div></div>`;
|
||||
html += `<button class="wl-satellite-remove" onclick="event.stopPropagation();_removeWishlistAlbum('${escapeHtml(an)}')" title="Remove">✕</button>`;
|
||||
const tileId = 'wl-tile-' + an.replace(/\W/g, '_') + '_' + idx;
|
||||
html += `<div class="wl-album-tile" data-album="${escapeHtml(an)}" onclick="_toggleAlbumTile(this)">`;
|
||||
html += `<div class="wl-album-tile-art">${ad.image ? `<img src="${ad.image}" alt="">` : `<div class="wl-album-tile-fallback">💿</div>`}</div>`;
|
||||
html += `<div class="wl-album-tile-info"><div class="wl-album-tile-name">${escapeHtml(an)}</div><div class="wl-album-tile-count">${ad.tracks.length} track${ad.tracks.length !== 1 ? 's' : ''}</div></div>`;
|
||||
html += `<span class="wl-album-tile-badge">${ad.tracks.length}</span>`;
|
||||
html += `<button class="wl-album-tile-remove" onclick="event.stopPropagation();_removeWishlistAlbum('${escapeHtml(an)}')" title="Remove album">✕</button>`;
|
||||
// Track list (hidden until tile clicked)
|
||||
html += `<div class="wl-tile-tracks">`;
|
||||
for (const tr of ad.tracks) {
|
||||
html += `<div class="wl-tile-track">`;
|
||||
html += `<span class="wl-tile-track-name">${escapeHtml(tr.track)}</span>`;
|
||||
html += `<button class="wl-tile-track-remove" onclick="event.stopPropagation();_removeWishlistTrack('${escapeHtml(tr.id)}')" title="Remove track">✕</button>`;
|
||||
html += `</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
html += `</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
if (data.singles.length > 0) {
|
||||
html += `<div class="wl-orb-singles">`;
|
||||
html += `<div class="wl-singles-orbit">`;
|
||||
for (const s of data.singles) {
|
||||
html += `<div class="wl-single-pill" data-track-id="${escapeHtml(s.id)}">`;
|
||||
if (s.image) html += `<img class="wl-pill-art" src="${s.image}" alt="">`;
|
||||
html += `<span class="wl-pill-name">${escapeHtml(s.track)}</span>`;
|
||||
html += `<button class="wl-pill-remove" onclick="event.stopPropagation();_removeWishlistTrack('${escapeHtml(s.id)}')" title="Remove">✕</button>`;
|
||||
html += `<div class="wl-single-moon" data-track-id="${escapeHtml(s.id)}">`;
|
||||
html += s.image ? `<img src="${s.image}" alt="">` : `<span class="wl-moon-fallback">⭐</span>`;
|
||||
html += `<div class="wl-moon-label">${escapeHtml(s.track)}</div>`;
|
||||
html += `<button class="wl-moon-remove-btn" onclick="event.stopPropagation();_removeWishlistTrack('${escapeHtml(s.id)}')" title="Remove">✕</button>`;
|
||||
html += `</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
|
@ -40666,6 +40682,13 @@ function _navigateToArtistFromWishlist(artistName) {
|
|||
}, 300);
|
||||
}
|
||||
|
||||
function _toggleAlbumTile(tileEl) {
|
||||
const wasExpanded = tileEl.classList.contains('tile-expanded');
|
||||
// Collapse all tiles in this group
|
||||
tileEl.closest('.wl-album-fan')?.querySelectorAll('.wl-album-tile.tile-expanded').forEach(t => t.classList.remove('tile-expanded'));
|
||||
if (!wasExpanded) tileEl.classList.add('tile-expanded');
|
||||
}
|
||||
|
||||
function _toggleOrbExpand(el) {
|
||||
const g = el.closest('.wl-orb-group');
|
||||
if (!g) return;
|
||||
|
|
@ -40703,6 +40726,82 @@ function _filterNebula() {
|
|||
});
|
||||
}
|
||||
|
||||
async function _nebulaDownload() {
|
||||
// Check if wishlist is already processing
|
||||
try {
|
||||
const statsResp = await fetch('/api/wishlist/stats');
|
||||
if (statsResp.ok) {
|
||||
const stats = await statsResp.json();
|
||||
if (stats.is_auto_processing) {
|
||||
showToast('Wishlist is currently being auto-processed. Check the Downloads page for progress.', 'info');
|
||||
return;
|
||||
}
|
||||
}
|
||||
const procResp = await fetch('/api/active-processes');
|
||||
if (procResp.ok) {
|
||||
const procData = await procResp.json();
|
||||
const wishlistBatch = (procData.active_processes || []).find(p => p.playlist_id === 'wishlist');
|
||||
if (wishlistBatch) {
|
||||
// Show the existing download modal
|
||||
WishlistModalState.clearUserClosed();
|
||||
const clientProcess = activeDownloadProcesses['wishlist'];
|
||||
if (clientProcess && clientProcess.modalElement && document.body.contains(clientProcess.modalElement)) {
|
||||
clientProcess.modalElement.style.display = 'flex';
|
||||
WishlistModalState.setVisible();
|
||||
} else {
|
||||
await rehydrateModal(wishlistBatch, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// No active process — show category choice
|
||||
const choice = await _showNebulaDownloadChoice();
|
||||
if (choice) await openDownloadMissingWishlistModal(choice);
|
||||
}
|
||||
|
||||
function _showNebulaDownloadChoice() {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'modal-overlay';
|
||||
overlay.style.display = 'flex';
|
||||
overlay.onclick = (e) => { if (e.target === overlay) { overlay.remove(); resolve(null); } };
|
||||
|
||||
const albumCount = document.getElementById('wishlist-stat-albums')?.textContent || '0';
|
||||
const singleCount = document.getElementById('wishlist-stat-singles')?.textContent || '0';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="delete-group-dialog">
|
||||
<div class="delete-group-icon">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="rgb(var(--accent-rgb))" stroke-width="1.8"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
</div>
|
||||
<h3 class="delete-group-title">Download Wishlist</h3>
|
||||
<p class="delete-group-message">Choose which category to process</p>
|
||||
<div class="delete-group-actions">
|
||||
<button class="delete-group-btn delete-group-keep" id="ndc-albums">
|
||||
💿 Albums & EPs <span style="opacity:0.5;margin-left:6px">${albumCount} tracks</span>
|
||||
</button>
|
||||
<button class="delete-group-btn delete-group-keep" id="ndc-singles" style="border-color: rgba(var(--accent-rgb), 0.15); background: rgba(var(--accent-rgb), 0.06);">
|
||||
⭐ Singles <span style="opacity:0.5;margin-left:6px">${singleCount} tracks</span>
|
||||
</button>
|
||||
<button class="delete-group-btn delete-group-cancel" id="ndc-cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.querySelector('#ndc-albums').onclick = () => { overlay.remove(); resolve('albums'); };
|
||||
overlay.querySelector('#ndc-singles').onclick = () => { overlay.remove(); resolve('singles'); };
|
||||
overlay.querySelector('#ndc-cancel').onclick = () => { overlay.remove(); resolve(null); };
|
||||
|
||||
document.addEventListener('keydown', function esc(e) {
|
||||
if (e.key === 'Escape') { overlay.remove(); resolve(null); document.removeEventListener('keydown', esc); }
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
});
|
||||
}
|
||||
|
||||
function _nebulaBack() {
|
||||
const t = document.getElementById('wishlist-category-tracks');
|
||||
const n = document.getElementById('wishlist-nebula');
|
||||
|
|
@ -40713,6 +40812,86 @@ function _nebulaBack() {
|
|||
initializeWishlistPage();
|
||||
}
|
||||
|
||||
// ── Live processing state for nebula ──
|
||||
let _nebulaLivePollInterval = null;
|
||||
let _nebulaLastTotal = null;
|
||||
|
||||
function _startNebulaLivePolling(currentCycle, artistImageMap) {
|
||||
_stopNebulaLivePolling();
|
||||
_nebulaLastTotal = null;
|
||||
|
||||
_nebulaLivePollInterval = setInterval(async () => {
|
||||
if (currentPage !== 'wishlist') { _stopNebulaLivePolling(); return; }
|
||||
|
||||
try {
|
||||
// Use wishlist stats which has is_auto_processing flag
|
||||
const statsResp = await fetch('/api/wishlist/stats');
|
||||
if (!statsResp.ok) return;
|
||||
const stats = await statsResp.json();
|
||||
const isProcessing = stats.is_auto_processing || false;
|
||||
const newTotal = stats.total || 0;
|
||||
|
||||
// Also check for manual wishlist download batches
|
||||
let hasBatch = false;
|
||||
try {
|
||||
const procResp = await fetch('/api/active-processes');
|
||||
if (procResp.ok) {
|
||||
const procData = await procResp.json();
|
||||
hasBatch = (procData.active_processes || []).some(p => p.playlist_id === 'wishlist');
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
const active = isProcessing || hasBatch;
|
||||
const nebulaField = document.getElementById('wl-nebula-field');
|
||||
if (!nebulaField) return;
|
||||
|
||||
if (active) {
|
||||
nebulaField.classList.add('nebula-processing');
|
||||
document.querySelectorAll('.wl-orb-group').forEach(g => g.classList.add('orb-processing'));
|
||||
|
||||
// Tracks completed — re-render
|
||||
if (_nebulaLastTotal !== null && newTotal < _nebulaLastTotal) {
|
||||
const [albumRes, singleRes] = await Promise.all([
|
||||
fetch('/api/wishlist/tracks?category=albums').then(r => r.json()),
|
||||
fetch('/api/wishlist/tracks?category=singles').then(r => r.json()),
|
||||
]);
|
||||
_renderWishlistNebula(albumRes.tracks || [], singleRes.tracks || [], artistImageMap, currentCycle);
|
||||
|
||||
const countEl = document.getElementById('wishlist-page-count');
|
||||
if (countEl) countEl.textContent = `${newTotal} track${newTotal !== 1 ? 's' : ''}`;
|
||||
const sa = document.getElementById('wishlist-stat-albums');
|
||||
const ss = document.getElementById('wishlist-stat-singles');
|
||||
if (sa) sa.textContent = stats.albums || 0;
|
||||
if (ss) ss.textContent = stats.singles || 0;
|
||||
|
||||
// Re-add processing classes after re-render
|
||||
document.getElementById('wl-nebula-field')?.classList.add('nebula-processing');
|
||||
document.querySelectorAll('.wl-orb-group').forEach(g => g.classList.add('orb-processing'));
|
||||
}
|
||||
_nebulaLastTotal = newTotal;
|
||||
} else {
|
||||
nebulaField.classList.remove('nebula-processing');
|
||||
document.querySelectorAll('.wl-orb-group.orb-processing').forEach(g => g.classList.remove('orb-processing'));
|
||||
|
||||
if (_nebulaLastTotal !== null) {
|
||||
_nebulaLastTotal = null;
|
||||
wishlistPageState.isInitialized = false;
|
||||
await initializeWishlistPage();
|
||||
await updateWishlistCount();
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function _stopNebulaLivePolling() {
|
||||
if (_nebulaLivePollInterval) {
|
||||
clearInterval(_nebulaLivePollInterval);
|
||||
_nebulaLivePollInterval = null;
|
||||
}
|
||||
_nebulaLastTotal = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the watchlist artist grid by the selected criteria.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -57232,7 +57232,7 @@ body.reduce-effects *::after {
|
|||
|
||||
/* ── Orb group ── */
|
||||
.wl-orb-group { display: flex; flex-direction: column; align-items: center; gap: 6px; transition: all 0.4s cubic-bezier(0.4,0,0.2,1); position: relative; }
|
||||
.wl-orb-group.expanded { background: rgba(255,255,255,0.02); border-radius: 16px; padding: 16px; box-shadow: 0 0 30px rgba(0,0,0,0.3); z-index: 5; flex-basis: 100%; max-width: 600px; }
|
||||
.wl-orb-group.expanded { background: rgba(255,255,255,0.02); border-radius: 16px; padding: 16px; box-shadow: 0 0 30px rgba(0,0,0,0.3); z-index: 5; flex-basis: 100%; max-width: 800px; }
|
||||
|
||||
/* ── Orb ── */
|
||||
.wl-orb { position: relative; border-radius: 50%; cursor: pointer; overflow: hidden; transition: all 0.35s cubic-bezier(0.4,0,0.2,1); }
|
||||
|
|
@ -57288,6 +57288,39 @@ body.reduce-effects *::after {
|
|||
.wl-orb:hover .wl-art-ring-item { opacity: 0.85; }
|
||||
@keyframes artRingSpin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ── Live processing state ── */
|
||||
.nebula-processing .wl-orb-group.orb-processing .wl-orb {
|
||||
animation: processingOrbPulse 3s ease-in-out infinite !important;
|
||||
}
|
||||
@keyframes processingOrbPulse {
|
||||
0%, 100% { transform: scale(1); filter: brightness(1); }
|
||||
50% { transform: scale(1.03); filter: brightness(1.1); }
|
||||
}
|
||||
|
||||
.nebula-processing .wl-orb-group.orb-processing .wl-orb-ring {
|
||||
border-color: rgb(var(--accent-rgb)) !important;
|
||||
border-width: 3px !important;
|
||||
box-shadow: 0 0 20px rgba(var(--accent-rgb), 0.6), 0 0 40px rgba(var(--accent-rgb), 0.3), 0 0 60px rgba(var(--accent-rgb), 0.1) !important;
|
||||
animation: processingRing 1.5s ease-in-out infinite !important;
|
||||
}
|
||||
@keyframes processingRing {
|
||||
0%, 100% { box-shadow: 0 0 20px rgba(var(--accent-rgb), 0.6), 0 0 40px rgba(var(--accent-rgb), 0.3); }
|
||||
50% { box-shadow: 0 0 30px rgba(var(--accent-rgb), 0.8), 0 0 50px rgba(var(--accent-rgb), 0.4), 0 0 70px rgba(var(--accent-rgb), 0.15); }
|
||||
}
|
||||
|
||||
.nebula-processing .wl-orb-group.orb-processing .wl-orb-glow {
|
||||
filter: blur(6px) brightness(1.8) !important;
|
||||
opacity: 0.9 !important;
|
||||
}
|
||||
|
||||
.nebula-processing .wl-orb-group.orb-processing .wl-orb-label {
|
||||
color: rgb(var(--accent-light-rgb));
|
||||
}
|
||||
|
||||
.nebula-processing .wl-orb-group.orb-processing .wl-orb-meta {
|
||||
color: rgba(var(--accent-rgb), 0.6);
|
||||
}
|
||||
|
||||
/* Enhancement 7: staggered entry animation */
|
||||
.wl-orb-group { animation: orbEntrance 0.5s ease backwards; }
|
||||
@keyframes orbEntrance { from { opacity: 0; transform: translateY(20px) scale(0.9); } to { opacity: 1; transform: translateY(0) scale(1); } }
|
||||
|
|
@ -57301,28 +57334,268 @@ body.reduce-effects *::after {
|
|||
.wl-orb-group.expanded .wl-orb-expanded { display: block; animation: expandFadeIn 0.3s ease; }
|
||||
@keyframes expandFadeIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
/* ── Satellites (albums) ── */
|
||||
.wl-orb-albums { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
|
||||
.wl-satellite { display: flex; align-items: center; gap: 10px; padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; transition: all 0.15s; }
|
||||
.wl-satellite:hover { background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.1); }
|
||||
.wl-satellite-art { width: 44px; height: 44px; border-radius: 6px; overflow: hidden; flex-shrink: 0; background: rgba(255,255,255,0.05); }
|
||||
.wl-satellite-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.wl-satellite-info { flex: 1; min-width: 0; }
|
||||
.wl-satellite-name { font-size: 12px; font-weight: 600; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.wl-satellite-count { font-size: 10px; color: rgba(255,255,255,0.35); }
|
||||
.wl-satellite-remove { width: 22px; height: 22px; border-radius: 50%; border: none; background: transparent; color: rgba(255,255,255,0.2); font-size: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; opacity: 0; transition: all 0.15s; flex-shrink: 0; }
|
||||
.wl-satellite:hover .wl-satellite-remove { opacity: 1; }
|
||||
.wl-satellite-remove:hover { background: rgba(239,68,68,0.15); color: #f87171; }
|
||||
/* ── Album fan (expanded) ── */
|
||||
.wl-album-fan {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* ── Single pills ── */
|
||||
.wl-orb-singles { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.wl-single-pill { display: flex; align-items: center; gap: 6px; padding: 4px 10px 4px 4px; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06); border-radius: 20px; font-size: 11px; color: rgba(255,255,255,0.6); transition: all 0.15s; }
|
||||
.wl-single-pill:hover { background: rgba(255,255,255,0.06); border-color: rgba(255,255,255,0.1); color: #fff; }
|
||||
.wl-pill-art { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
|
||||
.wl-pill-name { max-width: 140px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.wl-pill-remove { width: 16px; height: 16px; border-radius: 50%; border: none; background: transparent; color: rgba(255,255,255,0.2); font-size: 9px; cursor: pointer; display: flex; align-items: center; justify-content: center; opacity: 0; transition: all 0.15s; flex-shrink: 0; }
|
||||
.wl-single-pill:hover .wl-pill-remove { opacity: 1; }
|
||||
.wl-pill-remove:hover { background: rgba(239,68,68,0.15); color: #f87171; }
|
||||
.wl-album-tile {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||
animation: tileAppear 0.3s ease backwards;
|
||||
}
|
||||
|
||||
.wl-album-tile:nth-child(1) { animation-delay: 0s; }
|
||||
.wl-album-tile:nth-child(2) { animation-delay: 0.05s; }
|
||||
.wl-album-tile:nth-child(3) { animation-delay: 0.1s; }
|
||||
.wl-album-tile:nth-child(4) { animation-delay: 0.15s; }
|
||||
.wl-album-tile:nth-child(5) { animation-delay: 0.2s; }
|
||||
.wl-album-tile:nth-child(n+6) { animation-delay: 0.25s; }
|
||||
|
||||
@keyframes tileAppear {
|
||||
from { opacity: 0; transform: scale(0.8) translateY(10px); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
|
||||
.wl-album-tile:hover {
|
||||
transform: translateY(-4px) scale(1.05);
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.5), 0 0 12px rgba(var(--accent-rgb), 0.15);
|
||||
}
|
||||
|
||||
.wl-album-tile-art {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
background: linear-gradient(135deg, rgba(30,30,30,1), rgba(20,20,20,1));
|
||||
}
|
||||
|
||||
.wl-album-tile-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
|
||||
.wl-album-tile-fallback {
|
||||
width: 100%; height: 100%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 28px; opacity: 0.2;
|
||||
}
|
||||
|
||||
.wl-album-tile-info {
|
||||
position: absolute;
|
||||
bottom: 0; left: 0; right: 0;
|
||||
padding: 20px 8px 6px;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.wl-album-tile-name {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.wl-album-tile-badge {
|
||||
position: absolute;
|
||||
top: 5px; right: 5px;
|
||||
background: rgba(0,0,0,0.7);
|
||||
color: rgba(255,255,255,0.8);
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.wl-album-tile-count {
|
||||
font-size: 9px;
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
|
||||
.wl-album-tile-remove {
|
||||
position: absolute;
|
||||
top: 5px; left: 5px;
|
||||
width: 20px; height: 20px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(239, 68, 68, 0.8);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s, transform 0.15s;
|
||||
}
|
||||
|
||||
.wl-album-tile:hover .wl-album-tile-remove { opacity: 1; }
|
||||
.wl-album-tile-remove:hover { transform: scale(1.15); }
|
||||
|
||||
/* ── Tile track expansion ── */
|
||||
.wl-tile-tracks {
|
||||
display: none;
|
||||
width: 100%;
|
||||
padding: 4px 0 2px;
|
||||
background: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.wl-album-tile.tile-expanded {
|
||||
width: 220px;
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wl-album-tile.tile-expanded .wl-album-tile-art {
|
||||
aspect-ratio: auto;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.wl-album-tile.tile-expanded .wl-album-tile-info {
|
||||
position: static;
|
||||
background: none;
|
||||
padding: 6px 8px 4px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||
}
|
||||
|
||||
.wl-album-tile.tile-expanded .wl-album-tile-name {
|
||||
font-size: 11px;
|
||||
white-space: normal;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.wl-album-tile.tile-expanded .wl-album-tile-count {
|
||||
color: rgba(var(--accent-rgb), 0.6);
|
||||
}
|
||||
|
||||
.wl-album-tile.tile-expanded .wl-album-tile-badge {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wl-album-tile.tile-expanded .wl-tile-tracks {
|
||||
display: block;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255,255,255,0.1) transparent;
|
||||
}
|
||||
|
||||
.wl-tile-track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 3px 8px;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.wl-tile-track:hover { background: rgba(255,255,255,0.05); }
|
||||
|
||||
.wl-tile-track-name {
|
||||
font-size: 10px;
|
||||
color: rgba(255,255,255,0.6);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wl-tile-track-remove {
|
||||
width: 14px; height: 14px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255,255,255,0.15);
|
||||
font-size: 8px;
|
||||
cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.wl-tile-track:hover .wl-tile-track-remove { opacity: 1; }
|
||||
.wl-tile-track-remove:hover { background: rgba(239,68,68,0.2); color: #f87171; }
|
||||
|
||||
/* ── Singles orbit (small moons) ── */
|
||||
.wl-singles-orbit {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
.wl-single-moon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 60px;
|
||||
cursor: default;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.wl-single-moon img {
|
||||
width: 44px; height: 44px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
transition: border-color 0.2s, transform 0.2s;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.wl-single-moon:hover img {
|
||||
border-color: rgba(var(--accent-rgb), 0.4);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.wl-moon-fallback {
|
||||
width: 44px; height: 44px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 16px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
.wl-moon-label {
|
||||
font-size: 9px;
|
||||
color: rgba(255,255,255,0.45);
|
||||
text-align: center;
|
||||
max-width: 60px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.wl-moon-remove-btn {
|
||||
position: absolute;
|
||||
top: -2px; right: 2px;
|
||||
width: 16px; height: 16px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(239,68,68,0.8);
|
||||
color: #fff;
|
||||
font-size: 8px;
|
||||
cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.wl-single-moon:hover .wl-moon-remove-btn { opacity: 1; }
|
||||
.wl-moon-remove-btn:hover { transform: scale(1.15); }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.wl-nebula-bar { flex-direction: column; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue