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.
This commit is contained in:
parent
fabec1e455
commit
f1fe72ceb2
6 changed files with 210 additions and 12 deletions
BIN
database/music_library.db-shm
Normal file
BIN
database/music_library.db-shm
Normal file
Binary file not shown.
BIN
database/music_library.db-wal
Normal file
BIN
database/music_library.db-wal
Normal file
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…
Reference in a new issue