// Shared J621-style uploader for UGC pages. // Wires every `.upload-zone[data-upload]` on the page: dropzone click/drag, // per-file XHR to /api/uploads/, preview cards, removal, batch progress. // Single slots (thumbnail/version) hold one file; media is multi. (function () { 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; } function init(options) { const opts = options || {}; const pending = opts.pending || []; const csrf = getCookie('csrftoken'); const csrfHeader = csrf ? { 'X-CSRFToken': csrf } : {}; const zones = {}; document.querySelectorAll('.upload-zone[data-upload]').forEach((zone) => { const kind = zone.dataset.upload; zones[kind] = { zone, kind, single: kind === 'thumbnail' || kind === 'version', drop: zone.querySelector('.drop-zone'), input: zone.querySelector('input[type=file]'), preview: zone.querySelector('.upload-preview, #media-preview'), batch: zone.querySelector('.upload-batch'), current: null, busy: false, batchTotal: 0, batchDone: 0, }; }); // Render uploads that survived from a previous session. pending.forEach((u) => { const z = zones[u.kind]; if (z) addCard(u, z); }); function previewCard(upload, z) { const card = document.createElement('div'); card.className = 'file-preview-card'; card.dataset.uuid = upload.uuid; const ct = upload.content_type || ''; if (ct.startsWith('image/')) { const img = document.createElement('img'); img.src = upload.url; img.alt = upload.filename || ''; card.appendChild(img); } else if (ct.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); if (upload.removable !== false) { const remove = document.createElement('button'); remove.type = 'button'; remove.className = 'file-preview-remove'; remove.innerHTML = ''; remove.addEventListener('click', () => deleteUpload(upload.uuid, card, z)); card.appendChild(remove); } return card; } function addCard(upload, z) { if (z.single) { z.preview.innerHTML = ''; z.preview.appendChild(previewCard(upload, z)); if (upload.removable !== false) z.current = upload; } else { z.preview.appendChild(previewCard(upload, z)); } } function uploadFile(file, z) { const fd = new FormData(); fd.append('file', file); fd.append('kind', z.kind); const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/uploads/'); for (const k in csrfHeader) xhr.setRequestHeader(k, csrfHeader[k]); xhr.onload = function () { z.busy = false; if (!z.single) { z.batchDone++; updateBatch(z); } if (xhr.status !== 200) { try { alert(JSON.parse(xhr.responseText).error || 'Upload failed.'); } catch (e) { alert('Upload failed.'); } return; } addCard(JSON.parse(xhr.responseText), z); }; xhr.onerror = function () { z.busy = false; if (!z.single) { z.batchDone++; updateBatch(z); } alert('Network error during upload.'); }; xhr.send(fd); } function deleteUpload(uuid, card, z) { const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/uploads/' + uuid + '/delete/'); for (const k in csrfHeader) xhr.setRequestHeader(k, csrfHeader[k]); xhr.onload = function () { if (card && card.parentNode) card.parentNode.removeChild(card); if (z && z.current && z.current.uuid === uuid) z.current = null; if (opts.onRemove) opts.onRemove(uuid); }; xhr.onerror = function () { alert('Network error.'); }; xhr.send(); } function handleFiles(files, z) { // Single slots: take one file, replace the current upload, and // ignore further drops while one is in flight. if (z.single) { if (z.busy) return; const file = files[0]; if (z.current) deleteUpload(z.current.uuid, null, z); z.busy = true; uploadFile(file, z); return; } z.batchTotal += files.length; updateBatch(z); files.forEach((file) => uploadFile(file, z)); } function updateBatch(z) { if (!z.batch) return; if (z.batchTotal === 0) { z.batch.hidden = true; return; } z.batch.hidden = false; const status = z.batch.querySelector('.upload-batch-status'); if (status) { status.textContent = 'Uploading ' + z.batchDone + ' of ' + z.batchTotal + ' files'; } if (z.batchDone >= z.batchTotal && z.batchTotal > 0) { setTimeout(() => { z.batch.hidden = true; z.batchTotal = 0; z.batchDone = 0; }, 2500); } } Object.keys(zones).forEach((kind) => { const z = zones[kind]; if (!z.drop || !z.input) return; z.drop.addEventListener('click', () => z.input.click()); z.drop.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); z.input.click(); } }); ['dragenter', 'dragover'].forEach((evt) => z.drop.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); z.drop.classList.add('dragover'); })); ['dragleave', 'drop'].forEach((evt) => z.drop.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); z.drop.classList.remove('dragover'); })); z.drop.addEventListener('drop', (e) => { if (e.dataTransfer && e.dataTransfer.files.length) { handleFiles(Array.from(e.dataTransfer.files), z); } }); z.input.addEventListener('change', () => { handleFiles(Array.from(z.input.files), z); z.input.value = ''; }); }); // Prevent the browser opening files dropped outside a zone. ['dragover', 'drop'].forEach((evt) => window.addEventListener(evt, (e) => e.preventDefault())); if (opts.onReady) opts.onReady(); } window.PacksUploads = { init: init }; })();