From f1fe72ceb2029b0578495e63e66266471519302a Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Mon, 23 Feb 2026 15:54:58 -0800 Subject: [PATCH] Add track selection UI and backend mapping Add per-track selection checkboxes, select-all control and a selection count to download modals (missing/YouTube/Tidal/artist-album). Implement JS helpers (toggleAllTrackSelections, updateTrackSelectionCount) to manage checkbox state, row dimming, button disabling, and to filter/stamp selected tracks with _original_index before sending to the backend. Update start/add-to-wishlist flows to use only selectedTracks and disable controls once analysis starts. Backend _run_full_missing_tracks_process now reads _original_index to preserve original table indices in analysis results. CSS updates (mobile.css and style.css) add styling for checkbox columns, responsive hiding logic for headers/columns, selection visuals (.track-deselected), and small layout/width tweaks. --- database/music_library.db-shm | Bin 0 -> 32768 bytes database/music_library.db-wal | Bin 0 -> 61832 bytes web_server.py | 5 +- webui/static/mobile.css | 35 ++++++++- webui/static/script.js | 130 +++++++++++++++++++++++++++++++++- webui/static/style.css | 52 ++++++++++++-- 6 files changed, 210 insertions(+), 12 deletions(-) create mode 100644 database/music_library.db-shm create mode 100644 database/music_library.db-wal diff --git a/database/music_library.db-shm b/database/music_library.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..2dca81bb0f1fa2f375993d2b27c93420219e13ec GIT binary patch literal 32768 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

📋 Track Analysis & Download Status

+ ${tracks.length} / ${tracks.length} tracks selected
+ @@ -6024,6 +6030,11 @@ async function openDownloadMissingModal(playlistId) { ${tracks.map((track, index) => ` + @@ -6394,11 +6405,17 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam

📋 Track Analysis & Download Status

+ ${spotifyTracks.length} / ${spotifyTracks.length} tracks selected
+ + # Track Artist
+ + ${index + 1} ${escapeHtml(track.name)} ${escapeHtml(formatArtists(track.artists))}
+ @@ -6411,6 +6428,11 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam ${spotifyTracks.map((track, index) => ` + @@ -7886,9 +7908,34 @@ async function startMissingTracksProcess(playlistId) { forceToggleContainer.style.display = 'none'; } + // Filter tracks based on checkbox selection (if checkboxes exist in this modal) + const tbody = document.getElementById(`download-tracks-tbody-${playlistId}`); + let selectedTracks = process.tracks; + if (tbody) { + const allCbs = tbody.querySelectorAll('.track-select-cb'); + if (allCbs.length > 0) { + // Checkboxes exist — filter to only checked tracks + const checkedCbs = tbody.querySelectorAll('.track-select-cb:checked'); + const selectedIndices = new Set([...checkedCbs].map(cb => parseInt(cb.dataset.trackIndex))); + console.log(`🔲 [Track Selection] Total checkboxes: ${allCbs.length}, Checked: ${checkedCbs.length}`); + console.log(`🔲 [Track Selection] Checked indices:`, [...selectedIndices]); + console.log(`🔲 [Track Selection] process.tracks has ${process.tracks.length} items, first: "${process.tracks[0]?.name}", last: "${process.tracks[process.tracks.length-1]?.name}"`); + // Stamp each selected track with its original table index so the backend + // maps status updates back to the correct modal row + selectedTracks = process.tracks + .map((track, i) => ({ ...track, _original_index: i })) + .filter(track => selectedIndices.has(track._original_index)); + console.log(`🔲 [Track Selection] Filtered to ${selectedTracks.length} tracks:`, selectedTracks.map(t => `[${t._original_index}] ${t.name}`)); + // Disable checkboxes once analysis starts + allCbs.forEach(cb => { cb.disabled = true; }); + } + } + const selectAllCb = document.getElementById(`select-all-${playlistId}`); + if (selectAllCb) selectAllCb.disabled = true; + // Prepare request body - add album/artist context for artist album downloads const requestBody = { - tracks: process.tracks, + tracks: selectedTracks, force_download_all: forceDownloadAll }; @@ -8793,6 +8840,52 @@ async function updateModalWithLiveDownloadProgress() { } } +function toggleAllTrackSelections(playlistId, checked) { + const tbody = document.getElementById(`download-tracks-tbody-${playlistId}`); + if (!tbody) return; + const checkboxes = tbody.querySelectorAll('.track-select-cb'); + checkboxes.forEach(cb => { cb.checked = checked; }); + updateTrackSelectionCount(playlistId); +} + +function updateTrackSelectionCount(playlistId) { + const tbody = document.getElementById(`download-tracks-tbody-${playlistId}`); + if (!tbody) return; + const allCbs = tbody.querySelectorAll('.track-select-cb'); + const checkedCbs = tbody.querySelectorAll('.track-select-cb:checked'); + const total = allCbs.length; + const selected = checkedCbs.length; + + // Update selection count label + const countLabel = document.getElementById(`track-selection-count-${playlistId}`); + if (countLabel) { + countLabel.textContent = `${selected} / ${total} tracks selected`; + } + + // Update select-all checkbox state + const selectAll = document.getElementById(`select-all-${playlistId}`); + if (selectAll) { + selectAll.checked = selected === total; + selectAll.indeterminate = selected > 0 && selected < total; + } + + // Update row dimming + allCbs.forEach(cb => { + const row = cb.closest('tr'); + if (row) row.classList.toggle('track-deselected', !cb.checked); + }); + + // Disable Begin Analysis and Add to Wishlist buttons when 0 selected + const beginBtn = document.getElementById(`begin-analysis-btn-${playlistId}`); + if (beginBtn) { + beginBtn.disabled = selected === 0; + } + const wishlistBtn = document.getElementById(`add-to-wishlist-btn-${playlistId}`); + if (wishlistBtn) { + wishlistBtn.disabled = selected === 0; + } +} + async function cancelAllOperations(playlistId) { const process = activeDownloadProcesses[playlistId]; if (!process) return; @@ -11573,7 +11666,18 @@ async function addModalTracksToWishlist(playlistId) { return; } - const tracks = process.tracks; + // Filter tracks based on checkbox selection (if checkboxes exist in this modal) + const wishlistTbody = document.getElementById(`download-tracks-tbody-${playlistId}`); + let tracks = process.tracks; + if (wishlistTbody) { + const allCbs = wishlistTbody.querySelectorAll('.track-select-cb'); + if (allCbs.length > 0) { + const checkedCbs = wishlistTbody.querySelectorAll('.track-select-cb:checked'); + const selectedIndices = new Set([...checkedCbs].map(cb => parseInt(cb.dataset.trackIndex))); + tracks = process.tracks.filter((_, i) => selectedIndices.has(i)); + } + } + // Get artist/album context if available (for artist album downloads) const artist = process.artist || { name: 'Unknown Artist', id: null }; const album = process.album || process.playlist || { name: 'Playlist', id: playlistId }; @@ -15406,11 +15510,17 @@ async function openDownloadMissingModalForTidal(virtualPlaylistId, playlistName,

📋 Track Analysis & Download Status

+ ${spotifyTracks.length} / ${spotifyTracks.length} tracks selected
+ + # Track Artist
+ + ${index + 1} ${escapeHtml(track.name)} ${escapeHtml(formatArtists(track.artists))}
+ @@ -15423,6 +15533,11 @@ async function openDownloadMissingModalForTidal(virtualPlaylistId, playlistName, ${spotifyTracks.map((track, index) => ` + @@ -22498,11 +22613,17 @@ async function openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlis

📋 Track Analysis & Download Status

+ ${spotifyTracks.length} / ${spotifyTracks.length} tracks selected
+ + # Track Artist
+ + ${index + 1} ${escapeHtml(track.name)} ${escapeHtml(formatArtists(track.artists))}
+ @@ -22515,6 +22636,11 @@ async function openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlis ${spotifyTracks.map((track, index) => ` + diff --git a/webui/static/style.css b/webui/static/style.css index 88815ec3..a2682109 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -10518,6 +10518,9 @@ body { background: linear-gradient(135deg, rgba(40, 40, 40, 0.8) 0%, rgba(30, 30, 30, 0.9) 100%); + display: flex; + align-items: center; + justify-content: space-between; } .download-tracks-title { @@ -10578,16 +10581,16 @@ body { .track-number { color: #888888; font-weight: 500; - width: 5%; - /* 5% for track numbers */ + width: 4%; + /* 4% for track numbers */ text-align: center; } .track-name { font-weight: 600; color: #ffffff; - width: 25%; - /* 25% for track names */ + width: 24%; + /* 24% for track names */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -10633,8 +10636,8 @@ body { .track-download-status { text-align: center; - width: 20%; - /* 20% for download status with progress */ + width: 19%; + /* 19% for download status with progress */ font-weight: 500; overflow: hidden; text-overflow: ellipsis; @@ -10667,6 +10670,43 @@ body { width: auto; } +.track-select-header, +.track-select-cell { + width: 3%; + text-align: center; + padding: 12px 6px !important; +} + +.track-select-header input[type="checkbox"], +.track-select-cell input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: #1db954; +} + +.track-select-cell input[type="checkbox"]:disabled { + cursor: default; + opacity: 0.5; +} + +.track-deselected td:not(.track-select-cell) { + opacity: 0.4; +} + +.track-deselected .track-name, +.track-deselected .track-artist { + text-decoration: line-through; + text-decoration-color: rgba(255, 255, 255, 0.3); +} + +.track-selection-count { + font-size: 12px; + color: #999; + font-weight: 500; + white-space: nowrap; +} + .cancel-track-btn { background: #f44336; color: #ffffff;
+ + # Track Name Artist(s)
+ + ${index + 1} ${escapeHtml(track.name)} ${escapeHtml(formatArtists(track.artists))}