From ac3c89493d14783f9ee67f58a4733e1a85ec6334 Mon Sep 17 00:00:00 2001 From: Sin Date: Tue, 23 Jun 2026 21:02:27 +0100 Subject: [PATCH] Fix list/tree view: select mode, parts visibility, song actions - Preserve expanded artist groups across re-renders (was collapsing all groups whenever select mode toggled) - Add select checkbox + ring highlight to tree rows, matching grid - Add capture-phase select guard on tree clicks so rows/chips toggle selection instead of falling through to play - Always show favorite/save-for-later/overflow-menu buttons on tree rows instead of hover-only (matches grid card behaviour) - Always show arrangement chips on tree rows (no longer hidden below the sm breakpoint) Signed-off-by: Sin --- static/v3/songs.js | 48 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/static/v3/songs.js b/static/v3/songs.js index ed86acd..b3688c8 100644 --- a/static/v3/songs.js +++ b/static/v3/songs.js @@ -710,6 +710,10 @@ async function loadTree() { const host = document.getElementById('v3-songs-tree'); if (!host) return; + // Capture expanded groups BEFORE the "Loading…" wipe below, so a reload + // (e.g. toggling select mode) restores them instead of collapsing all. + const openArtists = new Set( + [...host.querySelectorAll('details[open]')].map((d) => d.getAttribute('data-artist'))); host.innerHTML = '

Loading…

'; // Page through ALL artists — the endpoint clamps size to 100, so a // single request would silently truncate libraries with >100 artists. @@ -726,18 +730,33 @@ if (!artists.length) { host.innerHTML = '

Nothing here.

'; return; } artists.forEach((a) => (a.albums || []).forEach((al) => (al.songs || []).forEach((s) => { state.songsById[cardKey(s)] = s; }))); host.innerHTML = artists.map((a) => - '
' + + '
' + '' + esc(a.name) + '' + esc(a.song_count) + '' + '
' + (a.albums || []).map((al) => '
' + esc(al.name || 'Unknown') + '
' + - (al.songs || []).map((s) => { const k = cardKey(s); const fl = fmtLabel(s); const chips = arrChipsHtml(s); return ( - '
' + - '' + + (al.songs || []).map((s) => { + const k = cardKey(s); const fl = fmtLabel(s); const chips = arrChipsHtml(s); const sel = state.selected.has(k); + // Display-only checkbox (pointer-events-none); the row's + // capture-phase select handler (render()) owns the toggle. + const checkbox = state.selectMode + ? '' + : ''; + return ( + '
' + + checkbox + + '' + '' + esc(s.title) + '' + - (chips ? '' : '') + + (chips ? '' + chips + '' : '') + (fl ? '' + fl + '' : '') + accuracyBadge(k, 'tree') + - '' + + // Same fav / save-for-later / overflow-menu cluster as the grid + // card. Always shown (like the arrangement chips), not hover- + // revealed. wireCards() binds all three for any [data-fn]. + '
' + + '' + + '' + + '' + + '
' + '
'); }).join('') + '
').join('') + '
').join(''); wireCards(host); } @@ -945,6 +964,21 @@ e.stopImmediatePropagation(); toggleSelect(card.getAttribute('data-fn'), card); }, true); + + // Same bulletproof guard for the list/tree view. Without it, clicking a + // song row (or its arrangement chip) in select mode falls through to the + // per-card play handler and starts playback instead of selecting. The + // group headers sit OUTSIDE any [data-fn], so closest() is null + // for them and their native expand/collapse is left untouched. + const treeEl = byId('v3-songs-tree'); + if (treeEl) treeEl.addEventListener('click', (e) => { + if (!state.selectMode) return; + const card = e.target.closest('[data-fn]'); + if (!card || !treeEl.contains(card)) return; + e.preventDefault(); + e.stopImmediatePropagation(); + toggleSelect(card.getAttribute('data-fn'), card); + }, true); const setView = (v) => { state.view = v; byId('v3-songs-grid-btn').className = 'px-3 py-2 text-sm ' + (v === 'grid' ? 'bg-fb-primary text-white' : 'text-fb-textDim'); @@ -1097,4 +1131,4 @@ if (active && active.id === 'v3-songs') applyScoreRefresh(); }); } -})(); +})(); \ No newline at end of file