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.
pull/165/head
Broque Thomas 2 months ago
parent fabec1e455
commit f1fe72ceb2

Binary file not shown.

Binary file not shown.

@ -13724,6 +13724,9 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
print(f"🔄 [Force Download] Force download mode enabled for batch {batch_id} - treating all tracks as missing")
for i, track_data in enumerate(tracks_json):
# Use original table index if provided (for partial track selection),
# otherwise fall back to enumeration index
track_index = track_data.get('_original_index', i)
track_name = track_data.get('name', '')
artists = track_data.get('artists', [])
found, confidence = False, 0.0
@ -13749,7 +13752,7 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
break
analysis_results.append({
'track_index': i, 'track': track_data, 'found': found, 'confidence': confidence
'track_index': track_index, 'track': track_data, 'found': found, 'confidence': confidence
})
# WISHLIST REMOVAL: If track is found in database, check if it should be removed from wishlist

@ -1141,6 +1141,10 @@
font-size: 13px;
}
.track-selection-count {
font-size: 10px;
}
.download-tracks-table {
table-layout: auto;
}
@ -1154,17 +1158,38 @@
text-overflow: ellipsis;
}
/* Checkbox column sizing on mobile */
.track-select-header,
.track-select-cell {
width: auto;
padding: 6px 4px !important;
}
.track-select-header input[type="checkbox"],
.track-select-cell input[type="checkbox"] {
width: 14px;
height: 14px;
}
/* Hide # and Duration columns on mobile */
.download-tracks-table th:nth-child(1),
.download-tracks-table td.track-number {
display: none;
}
.download-tracks-table th:nth-child(4),
.download-tracks-table td.track-duration {
display: none;
}
/* Hide matching th headers — different positions depending on checkbox column */
.download-tracks-table:has(.track-select-header) th:nth-child(2),
.download-tracks-table:not(:has(.track-select-header)) th:nth-child(1) {
display: none;
}
.download-tracks-table:has(.track-select-header) th:nth-child(5) {
display: none;
}
.track-name {
width: auto;
max-width: 120px;
@ -1810,11 +1835,15 @@
}
/* Hide Library Match column too at 480px */
.download-tracks-table th:nth-child(5),
.download-tracks-table td.track-match-status {
display: none;
}
.download-tracks-table:has(.track-select-header) th:nth-child(6),
.download-tracks-table:not(:has(.track-select-header)) th:nth-child(4) {
display: none;
}
.track-name {
max-width: 100px;
}

@ -6007,11 +6007,17 @@ async function openDownloadMissingModal(playlistId) {
<div class="download-tracks-section">
<div class="download-tracks-header">
<h3 class="download-tracks-title">📋 Track Analysis & Download Status</h3>
<span class="track-selection-count" id="track-selection-count-${playlistId}">${tracks.length} / ${tracks.length} tracks selected</span>
</div>
<div class="download-tracks-table-container">
<table class="download-tracks-table">
<thead>
<tr>
<th class="track-select-header">
<input type="checkbox" class="track-select-all"
id="select-all-${playlistId}" checked
onchange="toggleAllTrackSelections('${playlistId}', this.checked)">
</th>
<th>#</th>
<th>Track</th>
<th>Artist</th>
@ -6024,6 +6030,11 @@ async function openDownloadMissingModal(playlistId) {
<tbody id="download-tracks-tbody-${playlistId}">
${tracks.map((track, index) => `
<tr data-track-index="${index}">
<td class="track-select-cell">
<input type="checkbox" class="track-select-cb"
data-track-index="${index}" checked
onchange="updateTrackSelectionCount('${playlistId}')">
</td>
<td class="track-number">${index + 1}</td>
<td class="track-name" title="${escapeHtml(track.name)}">${escapeHtml(track.name)}</td>
<td class="track-artist" title="${escapeHtml(formatArtists(track.artists))}">${escapeHtml(formatArtists(track.artists))}</td>
@ -6394,11 +6405,17 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam
<div class="download-tracks-section">
<div class="download-tracks-header">
<h3 class="download-tracks-title">📋 Track Analysis & Download Status</h3>
<span class="track-selection-count" id="track-selection-count-${virtualPlaylistId}">${spotifyTracks.length} / ${spotifyTracks.length} tracks selected</span>
</div>
<div class="download-tracks-table-container">
<table class="download-tracks-table">
<thead>
<tr>
<th class="track-select-header">
<input type="checkbox" class="track-select-all"
id="select-all-${virtualPlaylistId}" checked
onchange="toggleAllTrackSelections('${virtualPlaylistId}', this.checked)">
</th>
<th>#</th>
<th>Track</th>
<th>Artist</th>
@ -6411,6 +6428,11 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam
<tbody id="download-tracks-tbody-${virtualPlaylistId}">
${spotifyTracks.map((track, index) => `
<tr data-track-index="${index}">
<td class="track-select-cell">
<input type="checkbox" class="track-select-cb"
data-track-index="${index}" checked
onchange="updateTrackSelectionCount('${virtualPlaylistId}')">
</td>
<td class="track-number">${index + 1}</td>
<td class="track-name" title="${escapeHtml(track.name)}">${escapeHtml(track.name)}</td>
<td class="track-artist" title="${escapeHtml(formatArtists(track.artists))}">${escapeHtml(formatArtists(track.artists))}</td>
@ -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,
<div class="download-tracks-section">
<div class="download-tracks-header">
<h3 class="download-tracks-title">📋 Track Analysis & Download Status</h3>
<span class="track-selection-count" id="track-selection-count-${virtualPlaylistId}">${spotifyTracks.length} / ${spotifyTracks.length} tracks selected</span>
</div>
<div class="download-tracks-table-container">
<table class="download-tracks-table">
<thead>
<tr>
<th class="track-select-header">
<input type="checkbox" class="track-select-all"
id="select-all-${virtualPlaylistId}" checked
onchange="toggleAllTrackSelections('${virtualPlaylistId}', this.checked)">
</th>
<th>#</th>
<th>Track</th>
<th>Artist</th>
@ -15423,6 +15533,11 @@ async function openDownloadMissingModalForTidal(virtualPlaylistId, playlistName,
<tbody id="download-tracks-tbody-${virtualPlaylistId}">
${spotifyTracks.map((track, index) => `
<tr data-track-index="${index}">
<td class="track-select-cell">
<input type="checkbox" class="track-select-cb"
data-track-index="${index}" checked
onchange="updateTrackSelectionCount('${virtualPlaylistId}')">
</td>
<td class="track-number">${index + 1}</td>
<td class="track-name" title="${escapeHtml(track.name)}">${escapeHtml(track.name)}</td>
<td class="track-artist" title="${escapeHtml(formatArtists(track.artists))}">${escapeHtml(formatArtists(track.artists))}</td>
@ -22498,11 +22613,17 @@ async function openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlis
<div class="download-tracks-section">
<div class="download-tracks-header">
<h3 class="download-tracks-title">📋 Track Analysis & Download Status</h3>
<span class="track-selection-count" id="track-selection-count-${virtualPlaylistId}">${spotifyTracks.length} / ${spotifyTracks.length} tracks selected</span>
</div>
<div class="download-tracks-table-container">
<table class="download-tracks-table">
<thead>
<tr>
<th class="track-select-header">
<input type="checkbox" class="track-select-all"
id="select-all-${virtualPlaylistId}" checked
onchange="toggleAllTrackSelections('${virtualPlaylistId}', this.checked)">
</th>
<th>#</th>
<th>Track Name</th>
<th>Artist(s)</th>
@ -22515,6 +22636,11 @@ async function openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlis
<tbody id="download-tracks-tbody-${virtualPlaylistId}">
${spotifyTracks.map((track, index) => `
<tr data-track-index="${index}">
<td class="track-select-cell">
<input type="checkbox" class="track-select-cb"
data-track-index="${index}" checked
onchange="updateTrackSelectionCount('${virtualPlaylistId}')">
</td>
<td class="track-number">${index + 1}</td>
<td class="track-name" title="${escapeHtml(track.name)}">${escapeHtml(track.name)}</td>
<td class="track-artist" title="${escapeHtml(formatArtists(track.artists))}">${escapeHtml(formatArtists(track.artists))}</td>

@ -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;

Loading…
Cancel
Save