diff --git a/js/side-panel.js b/js/side-panel.js index 8ba2527..323e85f 100644 --- a/js/side-panel.js +++ b/js/side-panel.js @@ -61,14 +61,16 @@ export class SidePanelManager { return this.currentView === view && this.panel.classList.contains('active'); } - refresh(view, renderControlsCallback, renderContentCallback) { + refresh(view, renderControlsCallback, renderContentCallback, options = {}) { if (this.isActive(view)) { if (renderControlsCallback) { this.controlsElement.innerHTML = ''; renderControlsCallback(this.controlsElement); } if (renderContentCallback) { - this.contentElement.innerHTML = ''; + if (!options.noClear) { + this.contentElement.innerHTML = ''; + } renderContentCallback(this.contentElement); } } diff --git a/js/ui-interactions.js b/js/ui-interactions.js index 95cd7bc..6ae0213 100644 --- a/js/ui-interactions.js +++ b/js/ui-interactions.js @@ -75,6 +75,15 @@ export function initializeUIInteractions(player, api, ui) { } let draggedQueueIndex = null; + let queueStartIndex = 0; + let queueEndIndex = 1000; + let isQueueRendering = false; + let topObserver = null; + let bottomObserver = null; + const QUEUE_VIRTUALIZATION_THRESHOLD = 1500; + const QUEUE_MAX_RENDERED = 1000; + const QUEUE_CHUNK_SIZE = 200; + const ESTIMATED_ITEM_HEIGHT = 58; // Sidebar mobile hamburgerBtn.addEventListener('click', () => { @@ -232,66 +241,221 @@ export function initializeUIInteractions(player, api, ui) { } }; - const renderQueueContent = (container) => { + const renderQueueItemHTML = (track, index) => { + const isPlaying = index === player.currentQueueIndex; + const isBlocked = contentBlockingSettings?.shouldHideTrack(track); + const trackTitle = getTrackTitle(track); + const trackArtists = getTrackArtists(track, { fallback: 'Unknown' }); + const qualityBadge = createQualityBadgeHTML(track); + const blockedTitle = isBlocked + ? `title="Blocked: ${contentBlockingSettings.isTrackBlocked(track.id) ? 'Track blocked' : contentBlockingSettings.isArtistBlocked(track.artist?.id) ? 'Artist blocked' : 'Album blocked'}"` + : ''; + + const isVideo = track.type === 'video'; + const coverUrl = + isVideo && track.imageId + ? api.getVideoCoverUrl(track.imageId) + : api.getCoverUrl(track.album?.cover); + + return ` +
+ `; + }; + + const attachQueueListeners = (container) => { + if (container._queueListenersAttached) return; + + container.addEventListener('click', async (e) => { + const item = e.target.closest('.queue-track-item'); + if (!item) return; + + const index = parseInt(item.dataset.queueIndex); + const removeBtn = e.target.closest('.queue-remove-btn'); + if (removeBtn) { + e.stopPropagation(); + player.removeFromQueue(index); + refreshQueuePanel(); + return; + } + + const likeBtn = e.target.closest('.queue-like-btn'); + if (likeBtn && likeBtn.dataset.action === 'toggle-like') { + e.stopPropagation(); + const track = player.getCurrentQueue()[index]; + if (track) { + const added = await db.toggleFavorite('track', track); + syncManager.syncLibraryItem('track', track, added); + + likeBtn.classList.toggle('active', added); + likeBtn.innerHTML = added + ? SVG_HEART.replace('class="heart-icon"', 'class="heart-icon filled"') + : SVG_HEART; + + showNotification( + added ? `Added to Liked: ${track.title}` : `Removed from Liked: ${track.title}` + ); + } + return; + } + + if (item.classList.contains('blocked')) return; + + player.playAtIndex(index); + refreshQueuePanel(); + }); + + container.addEventListener('contextmenu', async (e) => { + const item = e.target.closest('.queue-track-item'); + if (!item) return; + + e.preventDefault(); + const index = parseInt(item.dataset.queueIndex); + const contextMenu = document.getElementById('context-menu'); + if (contextMenu) { + const track = player.getCurrentQueue()[index]; + if (track) { + const isLiked = await db.isFavorite('track', track.id); + const likeItem = contextMenu.querySelector('li[data-action="toggle-like"]'); + if (likeItem) { + likeItem.textContent = isLiked ? 'Unlike' : 'Like'; + } + + const trackMixItem = contextMenu.querySelector('li[data-action="track-mix"]'); + if (trackMixItem) { + const hasMix = track.mixes && track.mixes.TRACK_MIX; + trackMixItem.style.display = hasMix ? 'block' : 'none'; + } + + positionMenu(contextMenu, e.clientX, e.clientY); + contextMenu._contextTrack = track; + } + } + }); + + container.addEventListener('dragstart', (e) => { + const item = e.target.closest('.queue-track-item'); + if (item) { + draggedQueueIndex = parseInt(item.dataset.queueIndex); + item.style.opacity = '0.5'; + } + }); + + container.addEventListener('dragend', (e) => { + const item = e.target.closest('.queue-track-item'); + if (item) { + item.style.opacity = '1'; + } + }); + + container.addEventListener('dragover', (e) => { + e.preventDefault(); + }); + + container.addEventListener('drop', (e) => { + e.preventDefault(); + const item = e.target.closest('.queue-track-item'); + if (item && draggedQueueIndex !== null) { + const index = parseInt(item.dataset.queueIndex); + if (draggedQueueIndex !== index) { + player.moveInQueue(draggedQueueIndex, index); + refreshQueuePanel(); + } + } + }); + + container._queueListenersAttached = true; + }; + + const renderQueueContent = (container, isUpdate = false) => { const currentQueue = player.getCurrentQueue(); if (currentQueue.length === 0) { container.innerHTML = '