Redesign watchlist modal with enriched artist detail view

This commit is contained in:
Broque Thomas 2026-03-03 20:26:21 -08:00
parent d06b7e5a25
commit 84de4ad16b
4 changed files with 811 additions and 37 deletions

View file

@ -21527,7 +21527,14 @@ def get_watchlist_artists():
"created_at": artist.created_at.isoformat() if artist.created_at else None,
"updated_at": artist.updated_at.isoformat() if artist.updated_at else None,
"image_url": artist.image_url, # Cached during watchlist scans
"itunes_artist_id": artist.itunes_artist_id # For iTunes-only artists
"itunes_artist_id": artist.itunes_artist_id, # For iTunes-only artists
"include_albums": artist.include_albums,
"include_eps": artist.include_eps,
"include_singles": artist.include_singles,
"include_live": artist.include_live,
"include_remixes": artist.include_remixes,
"include_acoustic": artist.include_acoustic,
"include_compilations": artist.include_compilations,
})
return jsonify({"success": True, "artists": artists_data})
@ -22362,7 +22369,8 @@ def watchlist_artist_config(artist_id):
cursor.execute("""
SELECT include_albums, include_eps, include_singles,
include_live, include_remixes, include_acoustic, include_compilations,
artist_name, image_url, spotify_artist_id, itunes_artist_id
artist_name, image_url, spotify_artist_id, itunes_artist_id,
last_scan_timestamp, date_added
FROM watchlist_artists
WHERE spotify_artist_id = ? OR itunes_artist_id = ?
""", (artist_id, artist_id))
@ -22405,6 +22413,53 @@ def watchlist_artist_config(artist_id):
'genres': []
}
# Enrich with library artist data (banner, bio, style, mood, label)
try:
conn2 = sqlite3.connect(str(database.database_path))
cur2 = conn2.cursor()
cur2.execute("""
SELECT banner_url, summary, style, mood, label, genres
FROM artists
WHERE spotify_artist_id = ? OR itunes_artist_id = ?
LIMIT 1
""", (artist_id, artist_id))
lib_row = cur2.fetchone()
if lib_row:
artist_info['banner_url'] = lib_row[0]
artist_info['summary'] = lib_row[1]
artist_info['style'] = lib_row[2]
artist_info['mood'] = lib_row[3]
artist_info['label'] = lib_row[4]
# Backfill genres from library if Spotify didn't provide any
if not artist_info.get('genres') and lib_row[5]:
try:
artist_info['genres'] = json.loads(lib_row[5])
except (json.JSONDecodeError, TypeError):
pass
# Get recent releases for this watchlist artist
cur2.execute("""
SELECT rr.album_name, rr.release_date, rr.album_cover_url, rr.track_count
FROM recent_releases rr
JOIN watchlist_artists wa ON rr.watchlist_artist_id = wa.id
WHERE wa.spotify_artist_id = ? OR wa.itunes_artist_id = ?
ORDER BY rr.release_date DESC
LIMIT 6
""", (artist_id, artist_id))
releases = [
{
'album_name': r[0],
'release_date': r[1],
'album_cover_url': r[2],
'track_count': r[3],
}
for r in cur2.fetchall()
]
conn2.close()
except Exception as e:
print(f"Warning: Could not enrich artist from library: {e}")
releases = []
config = {
'include_albums': bool(result[0]), # Convert INTEGER to boolean
'include_eps': bool(result[1]),
@ -22412,13 +22467,16 @@ def watchlist_artist_config(artist_id):
'include_live': bool(result[3]),
'include_remixes': bool(result[4]),
'include_acoustic': bool(result[5]),
'include_compilations': bool(result[6])
'include_compilations': bool(result[6]),
'last_scan_timestamp': result[11],
'date_added': result[12],
}
return jsonify({
"success": True,
"config": config,
"artist": artist_info
"artist": artist_info,
"recent_releases": releases
})
else: # POST

View file

@ -1486,12 +1486,34 @@
font-size: 10px;
}
/* Show checkbox and remove always on mobile (no hover) */
/* Show checkbox, remove, and gear always on mobile (no hover) */
.watchlist-card-checkbox,
.watchlist-artist-card .watchlist-card-remove {
.watchlist-artist-card .watchlist-card-remove,
.watchlist-artist-card .watchlist-card-gear {
opacity: 1;
}
.watchlist-detail-hero {
flex-direction: column;
text-align: center;
}
.watchlist-detail-hero-stats {
justify-content: center;
}
.watchlist-detail-hero-genres {
justify-content: center;
}
.watchlist-detail-actions {
flex-direction: column;
}
.watchlist-detail-actions button {
width: 100%;
}
.watchlist-search-container {
padding: 0 16px;
}
@ -1904,6 +1926,32 @@
padding: 8px 10px;
}
.watchlist-pill {
font-size: 8px;
padding: 1px 4px;
}
.watchlist-card-pills {
padding: 0 10px 6px;
}
.watchlist-detail-content {
padding: 20px 16px;
}
.watchlist-detail-hero img {
width: 80px;
height: 80px;
}
.watchlist-detail-hero-name {
font-size: 22px;
}
.watchlist-detail-releases {
grid-template-columns: 1fr;
}
/* Download modal - even more compact */
.download-missing-modal-hero {
padding: 10px 12px;

View file

@ -26757,25 +26757,25 @@ async function showWatchlistModal() {
</div>
<div class="playlist-modal-sync-status" id="watchlist-scan-status" style="display: ${scanStatus !== 'idle' ? 'flex' : 'none'}; flex-direction: column; align-items: center;">
<!-- Live Visual Activity Display -->
<div id="watchlist-live-activity" style="display: ${scanStatus === 'scanning' ? 'flex' : 'none'}; gap: 15px; margin-top: 15px; padding: 15px; background: #2a2a2a; border-radius: 8px; border: 1px solid #444; justify-content: center; align-items: flex-start;">
<div id="watchlist-live-activity" class="watchlist-live-activity" style="display: ${scanStatus === 'scanning' ? 'flex' : 'none'};">
<!-- Artist Photo -->
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<img id="watchlist-artist-img" src="" alt="Artist" style="width: 80px; height: 80px; border-radius: 50%; border: 2px solid rgb(var(--accent-rgb)); object-fit: cover; background: #1a1a1a;" onerror="this.style.display='none';" />
<div id="watchlist-artist-name" style="font-size: 11px; font-weight: bold; color: #fff; text-align: center; max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">Waiting...</div>
<div class="watchlist-live-activity-col">
<img id="watchlist-artist-img" class="watchlist-live-activity-artist-img" src="" alt="Artist" onerror="this.style.display='none';" />
<div id="watchlist-artist-name" class="watchlist-live-activity-label">Waiting...</div>
</div>
<!-- Album Cover -->
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
<img id="watchlist-album-img" src="" alt="Album" style="width: 80px; height: 80px; border-radius: 6px; border: 2px solid #ffc107; object-fit: cover; background: #1a1a1a;" onerror="this.style.display='none';" />
<div id="watchlist-album-name" style="font-size: 11px; font-weight: bold; color: #fff; text-align: center; max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">Waiting...</div>
<div class="watchlist-live-activity-col">
<img id="watchlist-album-img" class="watchlist-live-activity-album-img" src="" alt="Album" onerror="this.style.display='none';" />
<div id="watchlist-album-name" class="watchlist-live-activity-label">Waiting...</div>
</div>
<!-- Track and Wishlist Feed -->
<div style="display: flex; flex-direction: column; gap: 6px; min-width: 250px; max-width: 300px;">
<div style="font-size: 10px; color: #b3b3b3; text-transform: uppercase;">Current Track:</div>
<div id="watchlist-track-name" style="font-size: 11px; font-weight: bold; color: rgb(var(--accent-light-rgb)); margin-bottom: 8px;">Waiting...</div>
<div class="watchlist-live-activity-feed">
<div class="watchlist-live-activity-feed-label">Current Track:</div>
<div id="watchlist-track-name" class="watchlist-live-activity-track">Waiting...</div>
<div style="font-size: 10px; color: #ff9800; text-transform: uppercase;"> Recently Added:</div>
<div class="watchlist-live-activity-feed-label-orange"> Recently Added:</div>
<div id="watchlist-additions-feed" style="max-height: 80px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px; font-size: 10px;">
<!-- Populated by JavaScript -->
</div>
@ -26845,7 +26845,16 @@ async function showWatchlistModal() {
</div>
<div class="watchlist-artists-grid" id="watchlist-artists-list">
${artistsData.artists.map(artist => `
${artistsData.artists.map(artist => {
const pills = [];
if (artist.include_albums) pills.push('<span class="watchlist-pill watchlist-pill-active">Albums</span>');
if (artist.include_eps) pills.push('<span class="watchlist-pill watchlist-pill-active">EPs</span>');
if (artist.include_singles) pills.push('<span class="watchlist-pill watchlist-pill-active">Singles</span>');
if (artist.include_live) pills.push('<span class="watchlist-pill watchlist-pill-filter">Live</span>');
if (artist.include_remixes) pills.push('<span class="watchlist-pill watchlist-pill-filter">Remixes</span>');
if (artist.include_acoustic) pills.push('<span class="watchlist-pill watchlist-pill-filter">Acoustic</span>');
if (artist.include_compilations) pills.push('<span class="watchlist-pill watchlist-pill-filter">Compilations</span>');
return `
<div class="watchlist-artist-card"
data-artist-name="${artist.artist_name.toLowerCase().replace(/"/g, '&quot;')}"
data-artist-id="${artist.spotify_artist_id || artist.itunes_artist_id}">
@ -26858,11 +26867,13 @@ async function showWatchlistModal() {
<span class="watchlist-checkbox-custom"></span>
</label>
<button class="watchlist-card-remove"
<button class="watchlist-card-gear"
data-artist-id="${artist.spotify_artist_id || artist.itunes_artist_id}"
data-artist-name="${escapeHtml(artist.artist_name)}"
onclick="event.stopPropagation();"
title="Remove from watchlist">&times;</button>
title="Artist settings">
<svg viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 00.12-.61l-1.92-3.32a.49.49 0 00-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.48.48 0 00-.48-.41h-3.84a.48.48 0 00-.48.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.49.49 0 00-.59.22L2.74 8.87a.48.48 0 00.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 00-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.26.41.48.41h3.84c.24 0 .44-.17.48-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6A3.6 3.6 0 1115.6 12 3.6 3.6 0 0112 15.6z"/></svg>
</button>
<div class="watchlist-card-image">
${artist.image_url ? `
@ -26877,29 +26888,29 @@ async function showWatchlistModal() {
<div class="watchlist-card-info">
<span class="watchlist-card-name">${escapeHtml(artist.artist_name)}</span>
<span class="watchlist-card-meta">Added ${new Date(artist.date_added).toLocaleDateString()}</span>
<span class="watchlist-card-meta">${formatRelativeScanTime(artist.last_scan_timestamp)}</span>
</div>
${pills.length > 0 ? `<div class="watchlist-card-pills">${pills.join('')}</div>` : ''}
</div>
`).join('')}
`}).join('')}
</div>
</div>
</div>
`;
// Add event listeners for remove buttons
modal.querySelectorAll('.watchlist-card-remove').forEach(button => {
// Add event listeners for gear buttons
modal.querySelectorAll('.watchlist-card-gear').forEach(button => {
button.addEventListener('click', () => {
const artistId = button.getAttribute('data-artist-id');
const artistName = button.getAttribute('data-artist-name');
removeFromWatchlistModal(artistId, artistName);
openWatchlistArtistConfigModal(artistId, artistName);
});
});
// Add click handlers to artist cards (except for remove button or checkbox)
// Add click handlers to artist cards (except for gear button or checkbox)
modal.querySelectorAll('.watchlist-artist-card').forEach(item => {
item.addEventListener('click', (e) => {
// Don't trigger if clicking the remove button or checkbox
if (e.target.closest('.watchlist-card-remove') || e.target.closest('.watchlist-card-checkbox')) {
if (e.target.closest('.watchlist-card-gear') || e.target.closest('.watchlist-card-checkbox')) {
return;
}
@ -26907,7 +26918,7 @@ async function showWatchlistModal() {
const artistName = item.querySelector('.watchlist-card-name').textContent;
console.log(`🎵 Artist card clicked: ${artistName} (${artistId})`);
openWatchlistArtistConfigModal(artistId, artistName);
openWatchlistArtistDetailView(artistId, artistName);
});
});
@ -27113,6 +27124,177 @@ function closeWatchlistArtistConfigModal() {
}
}
/**
* Open watchlist artist detail view (slides in from right)
*/
async function openWatchlistArtistDetailView(artistId, artistName) {
try {
const response = await fetch(`/api/watchlist/artist/${artistId}/config`);
const data = await response.json();
if (!data.success) {
showToast(`Error loading artist info: ${data.error}`, 'error');
return;
}
const { config, artist, recent_releases } = data;
// Remove existing overlay if any
const existing = document.querySelector('.watchlist-artist-detail-overlay');
if (existing) existing.remove();
const overlay = document.createElement('div');
overlay.className = 'watchlist-artist-detail-overlay';
// Build pills
const pills = [];
if (config.include_albums) pills.push('<span class="watchlist-pill watchlist-pill-active">Albums</span>');
if (config.include_eps) pills.push('<span class="watchlist-pill watchlist-pill-active">EPs</span>');
if (config.include_singles) pills.push('<span class="watchlist-pill watchlist-pill-active">Singles</span>');
if (config.include_live) pills.push('<span class="watchlist-pill watchlist-pill-filter">Live</span>');
if (config.include_remixes) pills.push('<span class="watchlist-pill watchlist-pill-filter">Remixes</span>');
if (config.include_acoustic) pills.push('<span class="watchlist-pill watchlist-pill-filter">Acoustic</span>');
if (config.include_compilations) pills.push('<span class="watchlist-pill watchlist-pill-filter">Compilations</span>');
// Build scan info
const scanTimeText = config.last_scan_timestamp ? formatRelativeScanTime(config.last_scan_timestamp) : 'Never scanned';
const dateAddedText = config.date_added ? `Added ${new Date(config.date_added).toLocaleDateString()}` : '';
// Build metadata tags (style, mood, label)
const metaTags = [];
if (artist.style) metaTags.push(`<span class="watchlist-detail-genre-tag">${escapeHtml(artist.style)}</span>`);
if (artist.mood) metaTags.push(`<span class="watchlist-detail-genre-tag">${escapeHtml(artist.mood)}</span>`);
if (artist.label) metaTags.push(`<span class="watchlist-detail-genre-tag">${escapeHtml(artist.label)}</span>`);
overlay.innerHTML = `
${artist.banner_url ? `
<div class="watchlist-detail-banner">
<img src="${artist.banner_url}" alt="" onerror="this.parentElement.remove();">
<div class="watchlist-detail-banner-fade"></div>
</div>
` : ''}
<div class="watchlist-detail-content ${artist.banner_url ? 'has-banner' : ''}">
<button class="watchlist-detail-back watchlist-detail-back-btn">
Back to Watchlist
</button>
<div class="watchlist-detail-hero">
${artist.image_url ? `<img src="${artist.image_url}" alt="${escapeHtml(artist.name)}" onerror="this.style.display='none';">` : ''}
<div class="watchlist-detail-hero-info">
<h2 class="watchlist-detail-hero-name">${escapeHtml(artist.name)}</h2>
${artist.followers || artist.popularity ? `
<div class="watchlist-detail-hero-stats">
${artist.followers ? `
<div class="watchlist-detail-stat">
<span class="watchlist-detail-stat-value">${formatNumber(artist.followers)}</span>
<span class="watchlist-detail-stat-label">Followers</span>
</div>` : ''}
${artist.popularity ? `
<div class="watchlist-detail-stat">
<span class="watchlist-detail-stat-value">${artist.popularity}/100</span>
<span class="watchlist-detail-stat-label">Popularity</span>
</div>` : ''}
</div>
` : ''}
${artist.genres && artist.genres.length > 0 ? `
<div class="watchlist-detail-hero-genres">
${artist.genres.map(g => `<span class="watchlist-detail-genre-tag">${escapeHtml(g)}</span>`).join('')}
</div>
` : ''}
</div>
</div>
${artist.summary ? `
<div class="watchlist-detail-section">
<div class="watchlist-detail-section-title">About</div>
<p class="watchlist-detail-bio">${escapeHtml(artist.summary)}</p>
</div>
` : ''}
${metaTags.length > 0 ? `
<div class="watchlist-detail-section">
<div class="watchlist-detail-section-title">Info</div>
<div class="watchlist-detail-hero-genres">${metaTags.join('')}</div>
</div>
` : ''}
${recent_releases && recent_releases.length > 0 ? `
<div class="watchlist-detail-section">
<div class="watchlist-detail-section-title">Recent Releases</div>
<div class="watchlist-detail-releases">
${recent_releases.map(r => `
<div class="watchlist-detail-release">
${r.album_cover_url ? `<img src="${r.album_cover_url}" alt="" onerror="this.style.display='none';">` : ''}
<div class="watchlist-detail-release-info">
<span class="watchlist-detail-release-name">${escapeHtml(r.album_name)}</span>
<span class="watchlist-detail-release-meta">${r.release_date}${r.track_count ? ` · ${r.track_count} tracks` : ''}</span>
</div>
</div>
`).join('')}
</div>
</div>
` : ''}
<div class="watchlist-detail-section">
<div class="watchlist-detail-section-title">Watchlist</div>
<div class="watchlist-detail-watchlist-info">
<span class="watchlist-card-meta">${scanTimeText}</span>
${dateAddedText ? `<span class="watchlist-detail-info-sep">·</span><span class="watchlist-card-meta">${dateAddedText}</span>` : ''}
</div>
<div class="watchlist-card-pills" style="padding: 0; margin-top: 8px;">
${pills.length > 0 ? pills.join('') : '<span class="watchlist-card-meta">No release types enabled</span>'}
</div>
</div>
<div class="watchlist-detail-actions">
<button class="watchlist-detail-settings-btn watchlist-detail-settings-action">Settings</button>
<button class="watchlist-detail-remove-btn watchlist-detail-remove-action">Remove from Watchlist</button>
</div>
</div>
`;
// Wire up event listeners (avoids inline onclick escaping issues)
overlay.querySelector('.watchlist-detail-back-btn').addEventListener('click', () => {
closeWatchlistArtistDetailView();
});
overlay.querySelector('.watchlist-detail-settings-action').addEventListener('click', () => {
// Remove overlay immediately so it doesn't block the config modal
const detailOverlay = document.querySelector('.watchlist-artist-detail-overlay');
if (detailOverlay) detailOverlay.remove();
openWatchlistArtistConfigModal(artistId, artistName);
});
overlay.querySelector('.watchlist-detail-remove-action').addEventListener('click', () => {
removeFromWatchlistModal(artistId, artistName);
});
// Append to the modal container
const container = document.querySelector('.watchlist-fullscreen');
if (container) {
container.appendChild(overlay);
// Trigger slide-in animation
requestAnimationFrame(() => overlay.classList.add('visible'));
}
} catch (error) {
console.error('Error opening artist detail view:', error);
showToast(`Error: ${error.message}`, 'error');
}
}
/**
* Close watchlist artist detail view (slides out)
*/
function closeWatchlistArtistDetailView() {
const overlay = document.querySelector('.watchlist-artist-detail-overlay');
if (overlay) {
overlay.classList.remove('visible');
overlay.addEventListener('transitionend', () => overlay.remove(), { once: true });
}
}
/**
* Open global watchlist settings modal
*/
@ -27365,6 +27547,23 @@ function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
/**
* Format last scan timestamp as relative time
*/
function formatRelativeScanTime(isoString) {
if (!isoString) return 'Never scanned';
const diff = Date.now() - new Date(isoString).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'Scanned just now';
if (mins < 60) return `Scanned ${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `Scanned ${hrs}h ago`;
const days = Math.floor(hrs / 24);
if (days < 30) return `Scanned ${days}d ago`;
const months = Math.floor(days / 30);
return `Scanned ${months}mo ago`;
}
/**
* Filter watchlist artists based on search input
*/
@ -27476,16 +27675,16 @@ function handleWatchlistScanData(data) {
if (additionsFeed) {
if (data.recent_wishlist_additions && data.recent_wishlist_additions.length > 0) {
additionsFeed.innerHTML = data.recent_wishlist_additions.map(item => `
<div style="display: flex; gap: 6px; align-items: center; padding: 3px; background: #1a1a1a; border-radius: 4px;">
<img src="${item.album_image_url || ''}" alt="" style="width: 24px; height: 24px; border-radius: 3px; object-fit: cover;" onerror="this.style.display='none';" />
<div style="flex: 1; overflow: hidden;">
<div style="font-weight: bold; color: rgb(var(--accent-light-rgb)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${item.track_name}</div>
<div style="font-size: 9px; color: #b3b3b3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${item.artist_name}</div>
<div class="watchlist-live-addition-item">
<img src="${item.album_image_url || ''}" alt="" onerror="this.style.display='none';" />
<div class="watchlist-live-addition-item-info">
<div class="watchlist-live-addition-item-track">${item.track_name}</div>
<div class="watchlist-live-addition-item-artist">${item.artist_name}</div>
</div>
</div>
`).join('');
} else {
additionsFeed.innerHTML = '<div style="color: #666; font-size: 10px;">No tracks added yet...</div>';
additionsFeed.innerHTML = '<div class="watchlist-live-addition-empty">No tracks added yet...</div>';
}
}
} else if (liveActivity && data.status !== 'scanning') {
@ -27523,8 +27722,8 @@ function handleWatchlistScanData(data) {
// Update the scan status display with completion message and summary
statusDiv.innerHTML = `
<div style="text-align: center; padding: 15px; background: #2a2a2a; border-radius: 8px; border: 1px solid #444;">
<div style="font-size: 14px; color: rgb(var(--accent-light-rgb)); margin-bottom: 10px;">${completionMessage}</div>
<div class="watchlist-scan-completion">
<div class="watchlist-scan-completion-message">${completionMessage}</div>
<div style="font-size: 13px; opacity: 0.8;">
<span class="sync-stat">Artists: ${totalArtists}</span>
<span class="sync-separator"> </span>
@ -27694,6 +27893,9 @@ async function removeFromWatchlistModal(artistId, artistName) {
console.log(`❌ Removed ${artistName} from watchlist`);
// Close detail view if open
closeWatchlistArtistDetailView();
// Refresh the modal
showWatchlistModal();

View file

@ -8714,6 +8714,8 @@ body {
max-width: 95vw;
width: 95vw;
max-height: 95vh;
position: relative;
overflow: hidden;
}
/* Override playlist-modal-body within watchlist to allow grid scrolling */
@ -8807,6 +8809,34 @@ body {
font-weight: 500;
}
/* Card release-type pills */
.watchlist-card-pills {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 0 16px 10px;
}
.watchlist-pill {
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
padding: 2px 6px;
border-radius: 6px;
line-height: 1.3;
letter-spacing: 0.3px;
}
.watchlist-pill-active {
background: rgba(var(--accent-rgb), 0.15);
color: rgb(var(--accent-rgb));
}
.watchlist-pill-filter {
background: rgba(255, 152, 0, 0.15);
color: #ff9800;
}
/* Checkbox — top-left, hidden until hover or checked */
.watchlist-card-checkbox {
position: absolute;
@ -8877,6 +8907,442 @@ body {
color: #ffffff;
}
/* Gear icon — top-right, visible on hover */
.watchlist-card-gear {
position: absolute;
top: 10px;
right: 10px;
z-index: 3;
opacity: 0;
transition: opacity 0.15s ease, background 0.15s ease;
width: 30px;
height: 30px;
border-radius: 50%;
border: none;
background: rgba(0, 0, 0, 0.55);
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.watchlist-artist-card:hover .watchlist-card-gear {
opacity: 1;
}
.watchlist-card-gear:hover {
background: rgba(var(--accent-rgb), 0.7);
color: #ffffff;
}
.watchlist-card-gear svg {
width: 16px;
height: 16px;
fill: currentColor;
}
/* Watchlist Artist Detail Overlay */
.watchlist-artist-detail-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(18, 18, 18, 0.98);
z-index: 10;
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow-y: auto;
}
.watchlist-artist-detail-overlay.visible {
transform: translateX(0);
}
/* Banner at top of detail view */
.watchlist-detail-banner {
position: relative;
width: 100%;
}
.watchlist-detail-banner img {
width: 100%;
height: auto;
display: block;
}
.watchlist-detail-banner-fade {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 80px;
background: linear-gradient(to bottom, transparent, rgba(18, 18, 18, 0.98));
}
.watchlist-detail-content {
padding: 32px;
max-width: 640px;
margin: 0 auto;
}
.watchlist-detail-content.has-banner {
padding-top: 16px;
}
/* Bio text */
.watchlist-detail-bio {
font-size: 13px;
line-height: 1.6;
color: rgba(255, 255, 255, 0.6);
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Watchlist info row */
.watchlist-detail-watchlist-info {
display: flex;
align-items: center;
gap: 8px;
}
.watchlist-detail-info-sep {
color: rgba(255, 255, 255, 0.3);
font-size: 11px;
}
/* Recent releases grid */
.watchlist-detail-releases {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 10px;
}
.watchlist-detail-release {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.04);
transition: background 0.15s ease;
}
.watchlist-detail-release:hover {
background: rgba(255, 255, 255, 0.08);
}
.watchlist-detail-release img {
width: 48px;
height: 48px;
border-radius: 4px;
object-fit: cover;
flex-shrink: 0;
}
.watchlist-detail-release-info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.watchlist-detail-release-name {
font-size: 13px;
font-weight: 600;
color: #ffffff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.watchlist-detail-release-meta {
font-size: 11px;
color: rgba(255, 255, 255, 0.4);
}
.watchlist-detail-back {
display: inline-flex;
align-items: center;
gap: 8px;
background: none;
border: none;
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
cursor: pointer;
padding: 8px 0;
margin-bottom: 24px;
transition: color 0.15s ease;
}
.watchlist-detail-back:hover {
color: rgb(var(--accent-rgb));
}
.watchlist-detail-hero {
display: flex;
align-items: center;
gap: 24px;
margin-bottom: 32px;
}
.watchlist-detail-hero img {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 3px solid rgba(var(--accent-rgb), 0.3);
background: #1a1a1a;
flex-shrink: 0;
}
.watchlist-detail-hero-info {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
}
.watchlist-detail-hero-name {
font-size: 28px;
font-weight: 700;
color: #ffffff;
margin: 0;
line-height: 1.2;
}
.watchlist-detail-hero-stats {
display: flex;
gap: 20px;
}
.watchlist-detail-stat {
display: flex;
flex-direction: column;
gap: 2px;
}
.watchlist-detail-stat-value {
font-size: 16px;
font-weight: 700;
color: rgb(var(--accent-rgb));
}
.watchlist-detail-stat-label {
font-size: 10px;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.4);
font-weight: 600;
letter-spacing: 0.5px;
}
.watchlist-detail-hero-genres {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
}
.watchlist-detail-genre-tag {
font-size: 10px;
padding: 3px 8px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.6);
text-transform: capitalize;
}
.watchlist-detail-section {
margin-bottom: 24px;
}
.watchlist-detail-section-title {
font-size: 11px;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.4);
font-weight: 600;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
.watchlist-detail-actions {
display: flex;
gap: 12px;
margin-top: 32px;
}
.watchlist-detail-actions button {
flex: 1;
padding: 12px 20px;
border-radius: 8px;
border: none;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease, transform 0.1s ease;
}
.watchlist-detail-actions button:active {
transform: scale(0.98);
}
.watchlist-detail-settings-btn {
background: rgba(var(--accent-rgb), 0.15);
color: rgb(var(--accent-rgb));
}
.watchlist-detail-settings-btn:hover {
background: rgba(var(--accent-rgb), 0.25);
}
.watchlist-detail-remove-btn {
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.7);
}
.watchlist-detail-remove-btn:hover {
background: rgba(255, 59, 48, 0.2);
color: #ff3b30;
}
/* Live activity classes (replaces inline styles) */
.watchlist-live-activity {
display: flex;
gap: 15px;
margin-top: 15px;
padding: 15px;
background: #2a2a2a;
border-radius: 8px;
border: 1px solid #444;
justify-content: center;
align-items: flex-start;
}
.watchlist-live-activity-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.watchlist-live-activity-artist-img {
width: 80px;
height: 80px;
border-radius: 50%;
border: 2px solid rgb(var(--accent-rgb));
object-fit: cover;
background: #1a1a1a;
}
.watchlist-live-activity-album-img {
width: 80px;
height: 80px;
border-radius: 6px;
border: 2px solid #ffc107;
object-fit: cover;
background: #1a1a1a;
}
.watchlist-live-activity-label {
font-size: 11px;
font-weight: bold;
color: #fff;
text-align: center;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.watchlist-live-activity-feed {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 250px;
max-width: 300px;
}
.watchlist-live-activity-feed-label {
font-size: 10px;
color: #b3b3b3;
text-transform: uppercase;
}
.watchlist-live-activity-feed-label-orange {
font-size: 10px;
color: #ff9800;
text-transform: uppercase;
}
.watchlist-live-activity-track {
font-size: 11px;
font-weight: bold;
color: rgb(var(--accent-light-rgb));
margin-bottom: 8px;
}
.watchlist-live-addition-item {
display: flex;
gap: 6px;
align-items: center;
padding: 3px;
background: #1a1a1a;
border-radius: 4px;
}
.watchlist-live-addition-item img {
width: 24px;
height: 24px;
border-radius: 3px;
object-fit: cover;
}
.watchlist-live-addition-item-info {
flex: 1;
overflow: hidden;
}
.watchlist-live-addition-item-track {
font-weight: bold;
font-size: 10px;
color: rgb(var(--accent-light-rgb));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.watchlist-live-addition-item-artist {
font-size: 9px;
color: #b3b3b3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.watchlist-live-addition-empty {
color: #666;
font-size: 10px;
}
.watchlist-scan-completion {
text-align: center;
padding: 15px;
background: #2a2a2a;
border-radius: 8px;
border: 1px solid #444;
}
.watchlist-scan-completion-message {
font-size: 14px;
color: rgb(var(--accent-light-rgb));
margin-bottom: 10px;
}
/* Watchlist Search */
.watchlist-search-container {
padding: 0 32px;