From 6e7948b642dbd2e19c6701028953c87a49186c47 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Mon, 1 Jun 2026 09:31:20 -0700 Subject: [PATCH] Mirrored playlist modal: revamp + fix header clipping on long playlists The preview modal looked amateur and its header/footer clipped on long playlists (wolf39's 316-track "Road trip" showed neither title nor buttons). Root cause of the clip: .mm-list (the scroll area) was a flex child with flex:1 but no min-height:0. Flex items default to min-height:auto, so the list refused to shrink below its content, the modal blew past max-height, and overflow:hidden + vertical centering pushed the header off the top and the footer off the bottom. Now the list has min-height:0 and the hero + action bar are flex-shrink:0, so they stay pinned and the list scrolls. Visual revamp to match the rest of the app, using data already returned by /api/mirrored-playlists/ (image_url on the playlist and each track): - Hero uses real artwork (playlist cover -> first track art -> gradient fallback) with a blurred art backdrop + darkening overlay, replacing the emoji-in-a-box. Eyebrow + large title + meta line (source pill, owner, track count, total runtime, mirrored-ago). - Track rows gain per-track album thumbnails, two-line title/artist, album, duration, and a sticky column header. Missing art falls back to a gradient tile via onerror (no broken-image icons). - Cleaner action bar: primary Discover, secondary Auto-Sync, ghost Edit/ Close, quiet danger-outline Delete. Old .mirrored-modal-* / .mirrored-track-* / .mirrored-btn-* classes removed from style.css and replaced with the new .mm-* set; the _escJs escaping in the footer buttons (apostrophe fix) is preserved. --- webui/static/stats-automations.js | 84 +++--- webui/static/style.css | 422 +++++++++++++++++------------- 2 files changed, 288 insertions(+), 218 deletions(-) diff --git a/webui/static/stats-automations.js b/webui/static/stats-automations.js index e16397b7..b5f2612b 100644 --- a/webui/static/stats-automations.js +++ b/webui/static/stats-automations.js @@ -829,52 +829,74 @@ async function openMirroredPlaylistModal(playlistId) { const tracks = data.tracks || []; const source = data.source || 'unknown'; const sourceRef = getMirroredSourceRef(data); - const sourceIcons = { spotify: '🎵', tidal: '🌊', youtube: '▶', beatport: '🎛' }; + const sourceIcons = { spotify: '🎵', spotify_public: '🎵', tidal: '🌊', youtube: '▶', beatport: '🎛', deezer: '🎧', qobuz: '♫' }; + const sourceLabels = { spotify: 'Spotify', spotify_public: 'Spotify', tidal: 'Tidal', youtube: 'YouTube', beatport: 'Beatport', deezer: 'Deezer', qobuz: 'Qobuz' }; const sourceIcon = sourceIcons[source] || '📋'; + const srcLabel = sourceLabels[source] || source; + + // Hero artwork: playlist cover → first track with art → gradient fallback. + const heroArt = data.image_url || (tracks.find(t => t.image_url) || {}).image_url || ''; + + // Total runtime for the meta line. + const totalMs = tracks.reduce((sum, t) => sum + (t.duration_ms || 0), 0); + const totalMin = Math.round(totalMs / 60000); + const totalLabel = totalMin >= 60 ? `${Math.floor(totalMin / 60)} hr ${totalMin % 60} min` : `${totalMin} min`; const trackRows = tracks.map(t => { const dur = t.duration_ms ? `${Math.floor(t.duration_ms / 60000)}:${String(Math.floor((t.duration_ms % 60000) / 1000)).padStart(2, '0')}` : ''; - return `
- ${t.position} - ${_esc(t.track_name)} - ${_esc(t.artist_name)} - ${_esc(t.album_name)} - ${dur} + const art = t.image_url + ? `` + : ``; + return `
+ ${t.position} + ${art} +
+ ${_esc(t.track_name)} + ${_esc(t.artist_name)} +
+ ${_esc(t.album_name || '')} + ${dur}
`; }).join(''); + const heroCover = heroArt + ? `
` + : `
${sourceIcon}
`; + overlay.innerHTML = `
-
-
-
${sourceIcon}
-
-

${_esc(data.name)}

-
- ${_esc(source)} - ${tracks.length} tracks - · - Mirrored ${timeAgo(data.updated_at || data.mirrored_at)} +
+ ${heroArt ? `
` : ''} +
+ ${heroCover} +
+ Mirrored Playlist +

${_esc(data.name)}

+
+ ${_esc(srcLabel)} + ${data.owner ? `${_esc(data.owner)}·` : ''} + ${tracks.length} tracks + ${totalMs ? `·${totalLabel}` : ''} + · + Mirrored ${timeAgo(data.updated_at || data.mirrored_at)}
- × +
-
-
- #TrackArtistAlbumTime +
+
+ #TitleAlbumTime
- ${trackRows} + ${trackRows || '
No tracks in this mirror yet.
'}
-