diff --git a/webui/static/script.js b/webui/static/script.js index c32222d..6c4060d 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -5601,6 +5601,65 @@ let currentModalPlaylistId = null; // PHASE 2: Local cancelled track management (GUI PARITY) let cancelledTracks = new Set(); // Track cancelled track indices like GUI's cancelled_tracks +const TRACK_RENDER_BATCH_SIZE = 100; + +function applyProgressiveTrackRendering(playlistId, totalTrackCount) { + if (totalTrackCount <= TRACK_RENDER_BATCH_SIZE) return; + + const modal = document.getElementById(`download-missing-modal-${playlistId}`); + if (!modal) return; + + const tbody = document.getElementById(`download-tracks-tbody-${playlistId}`); + if (!tbody) return; + + const rows = tbody.querySelectorAll('tr[data-track-index]'); + if (rows.length <= TRACK_RENDER_BATCH_SIZE) return; + + // Hide rows beyond first batch + for (let i = TRACK_RENDER_BATCH_SIZE; i < rows.length; i++) { + rows[i].classList.add('hidden'); + } + + let revealedCount = TRACK_RENDER_BATCH_SIZE; + + // Append indicator into .download-tracks-title + const titleEl = modal.querySelector('.download-tracks-title'); + if (titleEl) { + const indicator = document.createElement('span'); + indicator.className = 'track-render-indicator'; + indicator.id = `track-render-indicator-${playlistId}`; + indicator.textContent = `Showing ${revealedCount} of ${totalTrackCount} tracks`; + titleEl.appendChild(indicator); + } + + // Scroll listener on table container + const container = modal.querySelector('.download-tracks-table-container'); + if (!container) return; + + container.addEventListener('scroll', function onScroll() { + const scrollBottom = container.scrollHeight - container.scrollTop - container.clientHeight; + if (scrollBottom > 200) return; + if (revealedCount >= rows.length) return; + + const nextEnd = Math.min(revealedCount + TRACK_RENDER_BATCH_SIZE, rows.length); + for (let i = revealedCount; i < nextEnd; i++) { + rows[i].classList.remove('hidden'); + } + revealedCount = nextEnd; + + const indicator = document.getElementById(`track-render-indicator-${playlistId}`); + if (indicator) { + indicator.textContent = revealedCount >= rows.length + ? `Showing all ${totalTrackCount} tracks` + : `Showing ${revealedCount} of ${totalTrackCount} tracks`; + } + + if (revealedCount >= rows.length) { + container.removeEventListener('scroll', onScroll); + } + }); +} + async function openDownloadMissingModal(playlistId) { showLoadingOverlay('Loading playlist...'); @@ -5765,6 +5824,7 @@ async function openDownloadMissingModal(playlistId) { `; + applyProgressiveTrackRendering(playlistId, tracks.length); modal.style.display = 'flex'; hideLoadingOverlay(); } @@ -6123,6 +6183,7 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam `; + applyProgressiveTrackRendering(virtualPlaylistId, spotifyTracks.length); modal.style.display = 'flex'; hideLoadingOverlay(); } @@ -7326,6 +7387,7 @@ async function openDownloadMissingWishlistModal(category = null) { `; + applyProgressiveTrackRendering(playlistId, tracks.length); modal.style.display = 'flex'; hideLoadingOverlay(); WishlistModalState.setVisible(); // Track that new wishlist modal is now visible @@ -14989,6 +15051,7 @@ async function openDownloadMissingModalForTidal(virtualPlaylistId, playlistName, `; + applyProgressiveTrackRendering(virtualPlaylistId, spotifyTracks.length); modal.style.display = 'flex'; hideLoadingOverlay(); } @@ -22079,6 +22142,7 @@ async function openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlis `; + applyProgressiveTrackRendering(virtualPlaylistId, spotifyTracks.length); modal.style.display = 'flex'; hideLoadingOverlay(); diff --git a/webui/static/style.css b/webui/static/style.css index f1b8c2c..8b7f8c3 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -10326,6 +10326,14 @@ body { text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); } +.track-render-indicator { + font-size: 11px; + font-weight: 400; + color: #888888; + margin-left: auto; + white-space: nowrap; +} + .download-tracks-table-container { flex: 1; overflow-y: auto;