// J621-style multi-upload for the gallery "add media" page. Files are // uploaded one at a time to /api/uploads/ (kind=media) and previewed; on // submit the pending uploads are moved into the project's gallery. (function () { const zone = document.querySelector('.upload-zone[data-upload="media"]'); if (!zone) return; const csrf = getCookie('csrftoken'); const csrfHeader = csrf ? { 'X-CSRFToken': csrf } : {}; const preview = document.getElementById('media-preview'); const batch = document.getElementById('media-batch'); const input = document.getElementById('media-input'); const drop = zone.querySelector('.drop-zone'); const pending = JSON.parse( (document.getElementById('packs-pending-uploads') || { textContent: '[]' }).textContent ); pending.forEach(renderCard); function renderCard(upload) { const card = document.createElement('div'); card.className = 'file-preview-card'; card.dataset.uuid = upload.uuid; if ((upload.content_type || '').startsWith('image/')) { const img = document.createElement('img'); img.src = upload.url; img.alt = upload.filename; card.appendChild(img); } else if ((upload.content_type || '').startsWith('video/')) { const video = document.createElement('video'); video.src = upload.url; video.muted = true; video.autoplay = true; video.loop = true; card.appendChild(video); } else { const icon = document.createElement('div'); icon.className = 'file-preview-icon'; icon.innerHTML = ''; card.appendChild(icon); } const name = document.createElement('span'); name.className = 'file-preview-name'; name.textContent = upload.filename; card.appendChild(name); const remove = document.createElement('button'); remove.type = 'button'; remove.className = 'file-preview-remove'; remove.innerHTML = ''; remove.addEventListener('click', () => { const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/uploads/' + upload.uuid + '/delete/'); for (const k in csrfHeader) xhr.setRequestHeader(k, csrfHeader[k]); xhr.onload = () => { if (card.parentNode) card.parentNode.removeChild(card); }; xhr.onerror = () => alert('Network error.'); xhr.send(); }); card.appendChild(remove); preview.appendChild(card); } function uploadFile(file) { const fd = new FormData(); fd.append('file', file); fd.append('kind', 'media'); const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/uploads/'); for (const k in csrfHeader) xhr.setRequestHeader(k, csrfHeader[k]); xhr.onload = function () { if (xhr.status !== 200) { try { alert(JSON.parse(xhr.responseText).error || 'Upload failed.'); } catch (e) { alert('Upload failed.'); } } else { renderCard(JSON.parse(xhr.responseText)); } batchDone++; updateBatch(); }; xhr.onerror = function () { alert('Network error during upload.'); batchDone++; updateBatch(); }; xhr.send(fd); } let batchTotal = 0; let batchDone = 0; function updateBatch() { if (batchTotal === 0) { batch.hidden = true; return; } batch.hidden = false; document.getElementById('media-batch-status').textContent = 'Uploading ' + batchDone + ' of ' + batchTotal + ' files'; if (batchDone >= batchTotal && batchTotal > 0) { setTimeout(() => { batch.hidden = true; batchTotal = 0; batchDone = 0; }, 2500); } } function handleFiles(files) { batchTotal += files.length; updateBatch(); files.forEach((file) => uploadFile(file)); } drop.addEventListener('click', () => input.click()); drop.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); input.click(); } }); ['dragenter', 'dragover'].forEach((evt) => drop.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); drop.classList.add('dragover'); })); ['dragleave', 'drop'].forEach((evt) => drop.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); drop.classList.remove('dragover'); })); drop.addEventListener('drop', (e) => { if (e.dataTransfer && e.dataTransfer.files.length) { handleFiles(Array.from(e.dataTransfer.files)); } }); input.addEventListener('change', () => { handleFiles(Array.from(input.files)); input.value = ''; }); ['dragover', 'drop'].forEach((evt) => window.addEventListener(evt, (e) => e.preventDefault())); function getCookie(name) { let cookieValue = null; if (document.cookie && document.cookie !== '') { const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].trim(); if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } })();