Fresh playlists built from your listening — open one to see the tracks
+
+
+
+
+
diff --git a/webui/static/discover.js b/webui/static/discover.js
index e1d658c2..05ca9d07 100644
--- a/webui/static/discover.js
+++ b/webui/static/discover.js
@@ -4278,8 +4278,12 @@ async function loadPersonalizedPopularPicks() {
}
personalizedPopularPicks = data.tracks;
- renderCompactPlaylist(container, data.tracks);
- container.closest('.discover-section').style.display = 'block';
+ _upsertMixCard({
+ key: 'popular_picks', title: 'Popular Picks',
+ subtitle: 'Popular tracks from artists you love',
+ tracks: data.tracks, syncKey: 'popular_picks',
+ });
+ _collapseOldMixSection(container);
} catch (error) {
console.error('Error loading popular picks:', error);
@@ -4301,8 +4305,12 @@ async function loadPersonalizedHiddenGems() {
}
personalizedHiddenGems = data.tracks;
- renderCompactPlaylist(container, data.tracks);
- container.closest('.discover-section').style.display = 'block';
+ _upsertMixCard({
+ key: 'hidden_gems', title: 'Hidden Gems',
+ subtitle: 'Deeper cuts you might have missed',
+ tracks: data.tracks, syncKey: 'hidden_gems',
+ });
+ _collapseOldMixSection(container);
} catch (error) {
console.error('Error loading hidden gems:', error);
@@ -4327,8 +4335,14 @@ async function loadPersonalizedListeningMix() {
}
personalizedListeningMix = data.tracks;
- renderCompactPlaylist(container, data.tracks);
- container.closest('.discover-section').style.display = 'block';
+ // Spotify-style: collapse this mix into a card in the "Your Mixes" shelf; its
+ // track list + actions live in the modal you open from the card (#discover redesign).
+ _upsertMixCard({
+ key: 'listening_mix', title: 'Your Listening Mix',
+ subtitle: 'From artists matched to your listening',
+ tracks: data.tracks, syncKey: 'listening_mix',
+ });
+ _collapseOldMixSection(container);
} catch (error) {
console.error('Error loading listening mix:', error);
@@ -4410,6 +4424,115 @@ function renderCompactPlaylist(container, tracks) {
container.innerHTML = html;
}
+// ── Your Mixes shelf (#discover redesign) ───────────────────────────────────
+// Each mix is ONE card (a 2x2 mosaic cover from its top tracks); clicking it opens
+// the track list + actions in a modal. The old per-mix full-width tables collapse
+// into these cards (the table now lives inside the opened mix, where it belongs).
+const _discoverMixRegistry = {};
+
+function _buildMixCard(mix) {
+ const covers = [];
+ for (const t of mix.tracks) {
+ const c = t.album_cover_url;
+ if (c && !covers.includes(c)) covers.push(c);
+ if (covers.length >= 4) break;
+ }
+ while (covers.length < 4) covers.push('/static/placeholder-album.png');
+ const mosaic = covers.map(c => ``).join('');
+ return `
+
+
+ ${mosaic}
+
▶
+
+
${_esc(mix.title)}
+
${mix.tracks.length} tracks
+
+ `;
+}
+
+// Register/refresh a mix's card in the "Your Mixes" shelf and reveal the section.
+function _upsertMixCard(mix) {
+ _discoverMixRegistry[mix.key] = mix;
+ const shelf = document.getElementById('your-mixes-grid');
+ if (!shelf) return;
+ shelf.innerHTML = Object.values(_discoverMixRegistry).map(_buildMixCard).join('');
+ const section = document.getElementById('your-mixes-section');
+ if (section) section.style.display = 'block';
+}
+
+// Collapse a now-redundant per-mix table section out of view, and strip its sync-status +
+// sync-button so their ids can't shadow the modal's (the modal owns the live sync display
+// now). Keeps the container so each loader's "already loaded?" guard still works on refresh.
+function _collapseOldMixSection(container) {
+ const section = container.closest('.discover-section');
+ if (!section) return;
+ section.querySelectorAll('.discover-sync-status, [id$="-sync-btn"]').forEach(el => el.remove());
+ section.style.display = 'none';
+}
+
+function openMixModalByKey(key) {
+ const mix = _discoverMixRegistry[key];
+ if (mix) openMixModal(mix);
+}
+
+function openMixModal(mix) {
+ document.getElementById('mix-modal-overlay')?.remove();
+ const overlay = document.createElement('div');
+ overlay.id = 'mix-modal-overlay';
+ overlay.className = 'modal-overlay';
+ overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
+ // statusBase mirrors startDiscoverPlaylistSync's id convention (underscores → hyphens) so
+ // the sync's live-progress updates land on THIS modal's status elements (the old hidden
+ // section's duplicates are stripped in _collapseOldMixSection so there's no id clash).
+ const base = mix.syncKey ? mix.syncKey.replace(/_/g, '-') : '';
+ // Download opens its own modal beneath this one — close this first so it's interactable.
+ const actions = mix.syncKey ? `
+
+ ` : '';
+ const syncStatus = mix.syncKey ? `
+