Redesign Watch All Unwatched as polished preview modal

Replaces the fire-and-forget button with a premium modal that shows
exactly which artists will be added before confirming. Features:

- Glassmorphic modal with stat cards, two-column artist grid, search
  filter, collapsible ineligible section, and loading spinner
- Source-aware filtering: only shows artists with the active source's
  ID (Spotify/iTunes/Deezer) as eligible
- Frontend and backend both paginate at 400 to avoid SQLite variable
  limit (SQLITE_MAX_VARIABLE_NUMBER=999) that silently broke queries
  above ~500 artists
- Backend source detection aligned with frontend — uses only the
  active source's ID, falls back to configured metadata source
pull/253/head
Broque Thomas 2 months ago
parent 56305615a4
commit 3c47281ce5

@ -18999,6 +18999,17 @@ def get_version_info():
"title": "What's New in SoulSync",
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
"sections": [
{
"title": "👁️ Watch All Unwatched Preview Modal",
"description": "The Watch All Unwatched button now opens a modal showing exactly which artists will be added",
"features": [
"• Preview list shows all eligible artists with images, track counts, and matched sources",
"• Clear separation of eligible vs ineligible artists (no external ID)",
"• Collapsible section explains why some artists can't be added yet",
"• Confirm before adding — no more silent 'Added 0' surprises",
"• Results summary shown after completion"
]
},
{
"title": "🔧 Fix Watch All Unwatched Skipping Deezer Artists",
"description": "Watch All Unwatched now supports Deezer as an ID source",
@ -34246,34 +34257,36 @@ def watchlist_all_unwatched_library_artists():
database = get_database()
active_source = _get_active_discovery_source()
# Get ALL unwatched artists from library (no pagination)
result = database.get_library_artists(
search_query='',
letter='all',
page=1,
limit=99999,
watchlist_filter='unwatched',
profile_id=get_current_profile_id()
)
unwatched_artists = result.get('artists', [])
# Fetch all unwatched artists in pages (SQLite variable limit safe)
unwatched_artists = []
page = 1
page_size = 400
while True:
result = database.get_library_artists(
search_query='',
letter='all',
page=page,
limit=page_size,
watchlist_filter='unwatched',
profile_id=get_current_profile_id()
)
unwatched_artists.extend(result.get('artists', []))
if not result.get('pagination', {}).get('has_next', False):
break
page += 1
added = 0
skipped_no_id = 0
skipped_already = 0
for artist in unwatched_artists:
# Determine the external ID to use based on active source
# Use only the active source's ID — matches frontend modal filtering
artist_id = None
if active_source == 'spotify' and artist.get('spotify_artist_id'):
artist_id = artist['spotify_artist_id']
elif active_source == 'deezer' and artist.get('deezer_id'):
artist_id = artist['deezer_id']
elif artist.get('spotify_artist_id'):
artist_id = artist['spotify_artist_id']
elif artist.get('itunes_artist_id'):
artist_id = artist['itunes_artist_id']
elif artist.get('deezer_id'):
artist_id = artist['deezer_id']
if active_source == 'spotify':
artist_id = artist.get('spotify_artist_id')
elif active_source == 'itunes':
artist_id = artist.get('itunes_artist_id')
elif active_source == 'deezer':
artist_id = artist.get('deezer_id')
if not artist_id:
skipped_no_id += 1
@ -34288,13 +34301,7 @@ def watchlist_all_unwatched_library_artists():
skipped_already += 1
continue
# Determine source based on which ID field we picked
if artist_id == artist.get('spotify_artist_id'):
src = 'spotify'
elif artist_id == artist.get('deezer_id'):
src = 'deezer'
else:
src = _get_metadata_fallback_source()
src = active_source if active_source in ('spotify', 'itunes', 'deezer') else _get_metadata_fallback_source()
success = database.add_artist_to_watchlist(artist_id, artist_name, profile_id=get_current_profile_id(), source=src)
if success:
added += 1

@ -2484,7 +2484,7 @@
<button class="watchlist-filter-btn active" data-filter="all">All</button>
<button class="watchlist-filter-btn" data-filter="watched">Watched</button>
<button class="watchlist-filter-btn" data-filter="unwatched">Unwatched</button>
<button class="library-watchlist-all-btn hidden" id="library-watchlist-all-btn" onclick="addAllUnwatchedToWatchlist(this)">
<button class="library-watchlist-all-btn hidden" id="library-watchlist-all-btn" onclick="openWatchAllUnwatchedModal()">
<span class="watchlist-all-icon">👁️</span>
<span class="watchlist-all-text">Watch All Unwatched</span>
</button>

@ -3403,6 +3403,7 @@ function closeHelperSearch() {
const WHATS_NEW = {
'2.1': [
// Newest features first
{ title: 'Watch All Preview Modal', desc: 'Watch All Unwatched now opens a modal showing which artists will be added before confirming', page: 'library', selector: '#library-watchlist-all-btn' },
{ title: 'Fix Watch All Unwatched', desc: 'Watch All Unwatched now works for Deezer users — was silently skipping artists with only Deezer IDs' },
{ title: 'Fix Path Mismatch Fixes', desc: 'Library Maintenance path fixes now use fresh config and show error reasons in the toast' },
{ title: 'Fix Wrong Spotify IDs', desc: 'Manual match no longer stores iTunes IDs as Spotify IDs — detects actual provider from results' },

@ -1000,6 +1000,16 @@
padding: 5px 12px;
}
.watch-all-modal { max-width: 95vw; width: 95vw; }
.watch-all-grid { grid-template-columns: 1fr; }
.watch-all-stats { flex-wrap: wrap; gap: 8px; padding: 12px 14px; }
.watch-all-stat-card { padding: 8px 12px; }
.watch-all-stat-value { font-size: 18px; }
.watch-all-cell { padding: 6px 8px; }
.watch-all-cell-img, .watch-all-cell-placeholder { width: 32px; height: 32px; }
.watch-all-header { padding: 16px 18px; }
.watch-all-search-wrap { padding: 8px 14px 4px; }
.library-card-watchlist-btn {
opacity: 1;
}

@ -38975,13 +38975,171 @@ function showLibraryEmpty(show) {
}
}
async function addAllUnwatchedToWatchlist(btn) {
if (btn.classList.contains('adding') || btn.classList.contains('all-added')) return;
async function openWatchAllUnwatchedModal() {
if (document.getElementById('watch-all-modal-overlay')) return;
btn.classList.add('adding');
const textSpan = btn.querySelector('.watchlist-all-text');
const originalText = textSpan.textContent;
textSpan.textContent = 'Adding...';
const sourceIdField = currentMusicSourceName === 'Apple Music' ? 'itunes_artist_id'
: currentMusicSourceName === 'Deezer' ? 'deezer_id' : 'spotify_artist_id';
const sourceName = currentMusicSourceName || 'Spotify';
const overlay = document.createElement('div');
overlay.id = 'watch-all-modal-overlay';
overlay.className = 'modal-overlay';
overlay.onclick = (e) => { if (e.target === overlay) closeWatchAllUnwatchedModal(); };
overlay.innerHTML = `
<div class="watch-all-modal">
<div class="watch-all-header">
<div class="watch-all-header-content">
<div class="watch-all-header-icon">&#128065;</div>
<div>
<h2 class="watch-all-title">Watch All Unwatched</h2>
<p class="watch-all-subtitle">Add unwatched artists with ${_esc(sourceName)} IDs to your watchlist</p>
</div>
</div>
<button class="watch-all-close" onclick="closeWatchAllUnwatchedModal()">&times;</button>
</div>
<div class="watch-all-body">
<div class="watch-all-loading-state">
<div class="watch-all-loading-spinner"></div>
<div class="watch-all-loading-text">Loading unwatched artists...</div>
<div class="watch-all-loading-count" id="watch-all-load-count"></div>
</div>
</div>
<div class="watch-all-footer">
<button class="watch-all-btn watch-all-btn-cancel" onclick="closeWatchAllUnwatchedModal()">Cancel</button>
<button class="watch-all-btn watch-all-btn-primary" id="watch-all-confirm-btn" disabled>Watch All</button>
</div>
</div>
`;
document.body.appendChild(overlay);
// Fetch all unwatched artists paginated (SQLite variable limit safe)
try {
const eligible = [];
const ineligible = [];
let page = 1;
const pageSize = 400;
const countEl = document.getElementById('watch-all-load-count');
while (true) {
if (!document.getElementById('watch-all-modal-overlay')) return;
if (countEl) countEl.textContent = `${eligible.length + ineligible.length} artists loaded...`;
const params = new URLSearchParams({ search: '', letter: 'all', page, limit: pageSize, watchlist: 'unwatched' });
const response = await fetch(`/api/library/artists?${params}`);
const data = await response.json();
if (!data.success) throw new Error(data.error || 'Failed to load artists');
for (const a of (data.artists || [])) {
if (a[sourceIdField]) eligible.push(a);
else ineligible.push(a);
}
if (!data.pagination.has_next) break;
page++;
}
_renderWatchAllModalContent(overlay, eligible, ineligible, sourceName);
} catch (error) {
console.error('Error loading unwatched artists:', error);
const body = overlay.querySelector('.watch-all-body');
if (body) body.innerHTML = `<div class="watch-all-empty-state"><div class="watch-all-empty-icon">&#9888;</div><div>Failed to load artists</div><a href="#" onclick="closeWatchAllUnwatchedModal(); openWatchAllUnwatchedModal(); return false;" class="watch-all-retry-link">Retry</a></div>`;
}
}
function _renderWatchAllModalContent(overlay, eligible, ineligible, sourceName) {
const body = overlay.querySelector('.watch-all-body');
const confirmBtn = overlay.querySelector('#watch-all-confirm-btn');
if (eligible.length === 0 && ineligible.length === 0) {
body.innerHTML = '<div class="watch-all-empty-state"><div class="watch-all-empty-icon">&#127925;</div><div>No unwatched artists found</div></div>';
return;
}
// Store data for search filtering
overlay._watchAllEligible = eligible;
overlay._watchAllIneligible = ineligible;
let html = '';
// Summary bar (sticky)
html += '<div class="watch-all-stats">';
html += `<div class="watch-all-stat-card eligible"><div class="watch-all-stat-value">${eligible.length}</div><div class="watch-all-stat-label">Ready to watch</div></div>`;
html += `<div class="watch-all-stat-card ineligible"><div class="watch-all-stat-value">${ineligible.length}</div><div class="watch-all-stat-label">No ${_esc(sourceName)} ID</div></div>`;
html += `<div class="watch-all-stat-card total"><div class="watch-all-stat-value">${eligible.length + ineligible.length}</div><div class="watch-all-stat-label">Total unwatched</div></div>`;
html += '</div>';
// Search filter
if (eligible.length > 10) {
html += '<div class="watch-all-search-wrap"><input type="text" class="watch-all-search" id="watch-all-search" placeholder="Search artists..." oninput="_filterWatchAllList(this.value)"></div>';
}
// Eligible grid
if (eligible.length > 0) {
html += '<div class="watch-all-section-label">Artists to be watched</div>';
html += '<div class="watch-all-grid" id="watch-all-eligible-grid">';
html += _buildWatchAllRows(eligible, false);
html += '</div>';
}
// Ineligible section
if (ineligible.length > 0) {
html += `<div class="watch-all-ineligible">
<div class="watch-all-ineligible-header" onclick="this.parentElement.classList.toggle('expanded')">
<div class="watch-all-ineligible-label">
<span class="watch-all-ineligible-icon">&#9888;</span>
<span>${ineligible.length} artist${ineligible.length !== 1 ? 's' : ''} without ${_esc(sourceName)} ID</span>
</div>
<span class="watch-all-chevron">&#9660;</span>
</div>
<div class="watch-all-ineligible-body">
<div class="watch-all-ineligible-hint">These artists haven't been matched to ${_esc(sourceName)} yet. The background enrichment worker will match them over time.</div>
<div class="watch-all-grid" id="watch-all-ineligible-grid">${_buildWatchAllRows(ineligible, true)}</div>
</div>
</div>`;
}
if (eligible.length === 0) {
html += `<div class="watch-all-empty-state"><div class="watch-all-empty-icon">&#128268;</div><div>None of your unwatched artists have a ${_esc(sourceName)} ID yet</div><div class="watch-all-empty-hint">The background enrichment worker will match them over time.</div></div>`;
}
body.innerHTML = html;
if (eligible.length > 0 && confirmBtn) {
confirmBtn.textContent = `Watch All (${eligible.length})`;
confirmBtn.disabled = false;
confirmBtn.onclick = () => _confirmWatchAllUnwatched(overlay, eligible.length);
}
}
function _buildWatchAllRows(artists, dimmed) {
let html = '';
for (const a of artists) {
const img = a.image_url
? `<img src="${_esc(a.image_url)}" alt="" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex'" loading="lazy"><div class="watch-all-cell-placeholder" style="display:none">&#127925;</div>`
: `<div class="watch-all-cell-placeholder">&#127925;</div>`;
html += `<div class="watch-all-cell${dimmed ? ' dimmed' : ''}" data-name="${_esc(a.name.toLowerCase())}">
<div class="watch-all-cell-img">${img}</div>
<div class="watch-all-cell-name" title="${_esc(a.name)}">${_esc(a.name)}</div>
<div class="watch-all-cell-meta">${a.track_count || 0} tracks</div>
</div>`;
}
return html;
}
function _filterWatchAllList(query) {
const q = query.toLowerCase().trim();
document.querySelectorAll('#watch-all-eligible-grid .watch-all-cell').forEach(cell => {
cell.style.display = !q || cell.dataset.name.includes(q) ? '' : 'none';
});
}
async function _confirmWatchAllUnwatched(overlay, expectedCount) {
const confirmBtn = overlay.querySelector('#watch-all-confirm-btn');
const cancelBtn = overlay.querySelector('.watch-all-btn-cancel');
if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.textContent = 'Adding...'; }
if (cancelBtn) cancelBtn.disabled = true;
try {
const response = await fetch('/api/library/watchlist-all-unwatched', {
@ -38991,43 +39149,36 @@ async function addAllUnwatchedToWatchlist(btn) {
const data = await response.json();
if (data.success) {
btn.classList.remove('adding');
btn.classList.add('all-added');
let resultText = `Added ${data.added}`;
if (data.skipped_no_id > 0) {
resultText += ` (${data.skipped_no_id} unmatched)`;
}
textSpan.textContent = resultText;
if (data.added > 0) {
showToast(data.message, 'success');
// Reload the library to reflect changes
setTimeout(() => {
loadLibraryArtists();
}, 1500);
} else if (data.skipped_no_id > 0) {
showToast(`No artists could be added — ${data.skipped_no_id} don't have matching IDs yet. Background workers will match them over time.`, 'warning');
} else {
showToast('No unwatched artists found', 'info');
}
const body = overlay.querySelector('.watch-all-body');
body.innerHTML = `<div class="watch-all-results">
<div class="watch-all-results-icon">&#10003;</div>
<div class="watch-all-results-title">Added ${data.added} artist${data.added !== 1 ? 's' : ''} to watchlist</div>
${data.skipped_already > 0 ? `<div class="watch-all-results-detail">${data.skipped_already} already watched</div>` : ''}
${data.skipped_no_id > 0 ? `<div class="watch-all-results-detail">${data.skipped_no_id} skipped (no external ID)</div>` : ''}
</div>`;
// Reset button after a delay
setTimeout(() => {
btn.classList.remove('all-added');
textSpan.textContent = originalText;
}, 5000);
if (confirmBtn) confirmBtn.style.display = 'none';
if (cancelBtn) { cancelBtn.disabled = false; cancelBtn.textContent = 'Close'; }
overlay.dataset.needsRefresh = 'true';
} else {
throw new Error(data.error || 'Failed to add artists');
}
} catch (error) {
console.error('Error bulk adding unwatched artists:', error);
btn.classList.remove('adding');
textSpan.textContent = originalText;
console.error('Error in watch all:', error);
if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.textContent = `Watch All (${expectedCount})`; }
if (cancelBtn) cancelBtn.disabled = false;
showToast('Failed to add artists to watchlist', 'error');
}
}
function closeWatchAllUnwatchedModal() {
const overlay = document.getElementById('watch-all-modal-overlay');
if (!overlay) return;
const needsRefresh = overlay.dataset.needsRefresh === 'true';
overlay.remove();
if (needsRefresh) loadLibraryArtists();
}
async function toggleLibraryCardWatchlist(btn, artist) {
if (btn.disabled) return;
btn.disabled = true;

@ -20560,6 +20560,194 @@ body.helper-mode-active #dashboard-activity-feed:hover {
display: none;
}
/* ── Watch All Unwatched Modal ──────────────────────────── */
.watch-all-modal {
width: 880px; max-width: 95vw; max-height: 85vh;
background: linear-gradient(135deg, rgba(20,20,20,0.97) 0%, rgba(12,12,12,0.99) 100%);
backdrop-filter: blur(20px) saturate(1.2);
border-radius: 20px;
border: 1px solid rgba(255,255,255,0.1);
border-top: 1px solid rgba(255,255,255,0.15);
box-shadow: 0 20px 60px rgba(0,0,0,0.6), 0 8px 32px rgba(0,0,0,0.4),
0 0 40px rgba(var(--accent-rgb), 0.08), inset 0 1px 0 rgba(255,255,255,0.1);
display: flex; flex-direction: column; overflow: hidden;
}
.watch-all-header {
display: flex; align-items: center; justify-content: space-between;
padding: 20px 24px;
background: linear-gradient(90deg, rgba(var(--accent-rgb), 0.06) 0%, transparent 60%);
border-bottom: 1px solid rgba(var(--accent-rgb), 0.12);
}
.watch-all-header-content { display: flex; align-items: center; gap: 14px; }
.watch-all-header-icon { font-size: 28px; opacity: 0.9; }
.watch-all-title {
font-size: 20px; font-weight: 700; color: #fff; letter-spacing: -0.3px;
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
}
.watch-all-subtitle { font-size: 13px; color: rgba(255,255,255,0.45); margin-top: 2px; }
.watch-all-close {
background: rgba(255,255,255,0.08); border: none; color: rgba(255,255,255,0.5);
width: 36px; height: 36px; border-radius: 50%; font-size: 20px; cursor: pointer;
display: flex; align-items: center; justify-content: center; transition: all 0.2s;
}
.watch-all-close:hover { background: rgba(255,255,255,0.15); color: #fff; transform: scale(1.1); }
.watch-all-body {
flex: 1; overflow-y: auto; padding: 0;
scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.15) transparent;
}
.watch-all-body::-webkit-scrollbar { width: 6px; }
.watch-all-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }
.watch-all-footer {
display: flex; justify-content: flex-end; gap: 10px;
padding: 16px 24px;
border-top: 1px solid rgba(255,255,255,0.06);
background: rgba(0,0,0,0.2);
}
.watch-all-btn {
padding: 10px 24px; border-radius: 10px; font-size: 14px; font-weight: 600;
border: none; cursor: pointer; transition: all 0.2s;
}
.watch-all-btn-cancel {
background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.7);
}
.watch-all-btn-cancel:hover { background: rgba(255,255,255,0.12); color: #fff; }
.watch-all-btn-primary {
background: rgb(var(--accent-rgb)); color: #fff;
box-shadow: 0 4px 12px rgba(var(--accent-rgb), 0.3);
}
.watch-all-btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(var(--accent-rgb), 0.4);
}
.watch-all-btn-primary:disabled { opacity: 0.4; cursor: default; }
/* Loading state */
.watch-all-loading-state {
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 64px 24px; gap: 16px;
}
.watch-all-loading-spinner {
width: 36px; height: 36px; border-radius: 50%;
border: 3px solid rgba(255,255,255,0.1); border-top-color: rgb(var(--accent-rgb));
animation: watch-all-spin 0.8s linear infinite;
}
@keyframes watch-all-spin { to { transform: rotate(360deg); } }
.watch-all-loading-text { color: rgba(255,255,255,0.6); font-size: 14px; }
.watch-all-loading-count { color: rgba(255,255,255,0.3); font-size: 13px; }
/* Stats cards */
.watch-all-stats {
display: flex; gap: 12px; padding: 16px 20px;
position: sticky; top: 0; z-index: 2;
background: linear-gradient(135deg, rgba(20,20,20,0.98) 0%, rgba(16,16,16,0.98) 100%);
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.watch-all-stat-card {
flex: 1; padding: 12px 16px; border-radius: 12px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
text-align: center; transition: all 0.2s;
}
.watch-all-stat-card:hover { border-color: rgba(255,255,255,0.1); background: rgba(255,255,255,0.05); }
.watch-all-stat-card.eligible { border-color: rgba(var(--accent-rgb), 0.2); }
.watch-all-stat-card.eligible .watch-all-stat-value { color: rgb(var(--accent-rgb)); }
.watch-all-stat-card.ineligible .watch-all-stat-value { color: rgba(255,180,80,0.8); }
.watch-all-stat-value { font-size: 22px; font-weight: 700; color: #fff; }
.watch-all-stat-label { font-size: 11px; color: rgba(255,255,255,0.4); margin-top: 2px; text-transform: uppercase; letter-spacing: 0.5px; }
/* Search */
.watch-all-search-wrap { padding: 8px 20px 4px; position: sticky; top: 78px; z-index: 2; background: rgba(16,16,16,0.95); }
.watch-all-search {
width: 100%; padding: 10px 16px; border-radius: 10px;
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08);
color: #fff; font-size: 14px; outline: none; transition: border-color 0.2s;
box-sizing: border-box;
}
.watch-all-search:focus { border-color: rgba(var(--accent-rgb), 0.4); }
.watch-all-search::placeholder { color: rgba(255,255,255,0.3); }
/* Section labels */
.watch-all-section-label {
padding: 12px 20px 6px; font-size: 11px; font-weight: 600;
color: rgba(255,255,255,0.35); text-transform: uppercase; letter-spacing: 1px;
}
/* Artist grid — two columns */
.watch-all-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 2px;
padding: 4px 12px 12px;
}
.watch-all-cell {
display: flex; align-items: center; gap: 10px;
padding: 8px 12px; border-radius: 10px;
transition: background 0.15s;
}
.watch-all-cell:hover { background: rgba(255,255,255,0.04); }
.watch-all-cell.dimmed { opacity: 0.4; }
.watch-all-cell-img {
width: 38px; height: 38px; flex-shrink: 0; border-radius: 50%; overflow: hidden;
background: rgba(255,255,255,0.06);
}
.watch-all-cell-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.watch-all-cell-placeholder {
width: 38px; height: 38px; border-radius: 50%;
background: rgba(255,255,255,0.06);
display: flex; align-items: center; justify-content: center; font-size: 16px;
}
.watch-all-cell-name {
font-size: 13px; font-weight: 500; color: #fff;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
flex: 1; min-width: 0;
}
.watch-all-cell-meta { font-size: 11px; color: rgba(255,255,255,0.3); white-space: nowrap; }
/* Ineligible collapsible section */
.watch-all-ineligible {
border-top: 1px solid rgba(255,255,255,0.06); margin-top: 8px;
}
.watch-all-ineligible-header {
display: flex; justify-content: space-between; align-items: center;
padding: 12px 20px; cursor: pointer; transition: background 0.15s;
}
.watch-all-ineligible-header:hover { background: rgba(255,255,255,0.03); }
.watch-all-ineligible-label { display: flex; align-items: center; gap: 8px; font-size: 13px; color: rgba(255,180,80,0.7); }
.watch-all-ineligible-icon { font-size: 14px; }
.watch-all-chevron { font-size: 10px; color: rgba(255,255,255,0.3); transition: transform 0.25s; }
.watch-all-ineligible.expanded .watch-all-chevron { transform: rotate(180deg); }
.watch-all-ineligible-body {
max-height: 0; overflow: hidden; transition: max-height 0.35s ease;
}
.watch-all-ineligible.expanded .watch-all-ineligible-body { max-height: 400px; overflow-y: auto; }
.watch-all-ineligible-hint {
padding: 8px 20px 4px; font-size: 12px; color: rgba(255,255,255,0.3);
border-left: 3px solid rgba(255,180,80,0.3); margin: 0 20px 8px; padding-left: 12px;
}
/* Results state */
.watch-all-results {
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 64px 24px; gap: 8px;
}
.watch-all-results-icon {
width: 64px; height: 64px; border-radius: 50%;
background: rgba(var(--accent-rgb), 0.12); color: rgb(var(--accent-rgb));
display: flex; align-items: center; justify-content: center;
font-size: 32px; margin-bottom: 8px;
box-shadow: 0 0 24px rgba(var(--accent-rgb), 0.15);
}
.watch-all-results-title { font-size: 20px; font-weight: 600; color: #fff; }
.watch-all-results-detail { font-size: 13px; color: rgba(255,255,255,0.4); }
/* Empty / error state */
.watch-all-empty-state {
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 64px 24px; gap: 8px; color: rgba(255,255,255,0.5); font-size: 14px;
}
.watch-all-empty-icon { font-size: 36px; margin-bottom: 4px; opacity: 0.5; }
.watch-all-empty-hint { font-size: 12px; color: rgba(255,255,255,0.3); margin-top: 4px; }
.watch-all-retry-link { color: rgb(var(--accent-rgb)); text-decoration: none; margin-top: 8px; }
.watch-all-retry-link:hover { text-decoration: underline; }
.alphabet-selector {
overflow-x: auto;
-webkit-overflow-scrolling: touch;

Loading…
Cancel
Save