// J621-style upload + real-time autosave for the "create project" form. // Each selected file is uploaded individually (XHR + FormData) to /api/uploads/ // and stored under media/uploads/user_/. Typed fields autosave to the // ProjectDraft via /api/draft/ (debounced, no explicit Save Draft button). (function () { const form = document.getElementById('project-form'); if (!form) return; const csrf = getCookie('csrftoken'); const csrfHeader = csrf ? { 'X-CSRFToken': csrf } : {}; const savedUploads = JSON.parse( (document.getElementById('packs-draft-uploads') || { textContent: '[]' }).textContent ); // ----- Multi-upload (per-file XHR, thumbnails) ----- const zones = {}; document.querySelectorAll('.upload-zone').forEach((zone) => { const kind = zone.dataset.upload; zones[kind] = { zone, drop: zone.querySelector('.drop-zone'), input: zone.querySelector('input[type=file]'), preview: zone.querySelector('.upload-preview, #media-preview'), }; if (kind === 'media') zones[kind].batch = document.getElementById('media-batch'); }); // Re-render the pending uploads that survived from a previous session. savedUploads.forEach((u) => { const z = zones[u.kind]; if (z && !z.current) renderUpload(u, z); }); function renderUpload(upload, z) { if (z.kind === 'thumbnail' || z.kind === 'version') { z.preview.innerHTML = ''; z.preview.appendChild(previewCard(upload)); z.current = upload; } else { z.preview.appendChild(previewCard(upload)); } } function previewCard(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', () => deleteUpload(upload.uuid, card)); card.appendChild(remove); return card; } function uploadFile(file, kind, z) { const fd = new FormData(); fd.append('file', file); fd.append('kind', 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 (xhr.status !== 200) { try { alert(JSON.parse(xhr.responseText).error || 'Upload failed.'); } catch (e) { alert('Upload failed.'); } return; } const data = JSON.parse(xhr.responseText); renderUpload(data, z); }; xhr.onerror = function () { z.busy = false; alert('Network error during upload.'); }; xhr.send(fd); } function deleteUpload(uuid, card) { 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); }; xhr.onerror = function () { alert('Network error.'); }; xhr.send(); } Object.keys(zones).forEach((kind) => { const z = zones[kind]; 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), kind, z); } }); z.input.addEventListener('change', () => { handleFiles(Array.from(z.input.files), kind, z); z.input.value = ''; }); }); // Prevent the browser opening files dropped outside a zone. ['dragover', 'drop'].forEach((evt) => window.addEventListener(evt, (e) => e.preventDefault())); let batchTotal = 0; let batchDone = 0; function handleFiles(files, kind, z) { // Thumbnail/version are single slots: take one file, replace any // current upload, and ignore further drops while one is in flight. if (kind === 'thumbnail' || kind === 'version') { if (z.busy) return; const file = files[0]; if (z.current) deleteUpload(z.current.uuid, null); z.busy = true; uploadFile(file, kind, z); return; } batchTotal += files.length; updateBatch(); files.forEach((file) => uploadFile(file, kind, z)); } function updateBatch() { const z = zones.media; if (!z || !z.batch) return; if (batchTotal === 0) { z.batch.hidden = true; return; } z.batch.hidden = false; document.getElementById('media-batch-status').textContent = 'Uploading ' + batchDone + ' of ' + batchTotal + ' files'; if (batchDone >= batchTotal && batchTotal > 0) { setTimeout(() => { z.batch.hidden = true; batchTotal = 0; batchDone = 0; }, 2500); } } // ----- Real-time draft autosave ----- const autosaveStatus = document.getElementById('autosave-status'); const FIELDS = ['title', 'summary', 'category', 'description', 'caption', 'version_name', 'changelog']; function setStatus(text, state) { autosaveStatus.innerHTML = text; autosaveStatus.className = 'autosave-status ' + (state || ''); } let saveTimer = null; let lastSavedAt = null; function scheduleSave() { setStatus(' Saving…', 'saving'); clearTimeout(saveTimer); saveTimer = setTimeout(saveDraft, 600); } function saveDraft() { const data = {}; FIELDS.forEach((name) => { const el = document.getElementById('id_' + name); if (el) data[name] = el.value; }); const tags = document.getElementById('id_tags'); if (tags) data.tags = tags.value; const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/draft/'); xhr.setRequestHeader('Content-Type', 'application/json'); for (const k in csrfHeader) xhr.setRequestHeader(k, csrfHeader[k]); xhr.onload = function () { if (xhr.status === 200) { const r = JSON.parse(xhr.responseText); lastSavedAt = r.updated_at; setStatus(' Saved just now', 'saved'); } else { setStatus(' Save failed', 'error'); } }; xhr.onerror = function () { setStatus(' Save failed', 'error'); }; xhr.send(JSON.stringify({ data })); } FIELDS.forEach((name) => { const el = document.getElementById('id_' + name); if (el) { el.addEventListener('input', scheduleSave); if (el.tagName === 'SELECT') el.addEventListener('change', scheduleSave); } }); document.addEventListener('tags-changed', scheduleSave); // Autosave is always live; indicate it once the page settles. setStatus(' Autosave on', 'idle'); setTimeout(() => setStatus(' Saved just now', 'saved'), 2000); 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; } })();