(() => { const dropzone = document.getElementById('dropzone'); const fileInput = document.getElementById('file-input'); const fileListEl = document.getElementById('file-list'); const options = document.getElementById('options'); const slugGroup = document.getElementById('slug-group'); const uploadBtn = document.getElementById('upload-btn'); const progressSection = document.getElementById('progress-section'); const progressOverall = document.getElementById('progress-overall'); const progressFilename = document.getElementById('progress-filename'); const progressFill = document.getElementById('progress-fill'); const progressText = document.getElementById('progress-text'); const result = document.getElementById('result'); const resultHeading = document.getElementById('result-heading'); const resultSingle = document.getElementById('result-single'); const resultUrl = document.getElementById('result-url'); const copyBtn = document.getElementById('copy-btn'); const resultDelete = document.getElementById('result-delete'); const resultExpiry = document.getElementById('result-expiry'); const resultMulti = document.getElementById('result-multi'); const resultList = document.getElementById('result-list'); const copyAllBtn = document.getElementById('copy-all-btn'); const errorDiv = document.getElementById('error'); let selectedFiles = []; let uploading = false; // Drag and drop dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.classList.add('drag-over'); }); dropzone.addEventListener('dragleave', () => { dropzone.classList.remove('drag-over'); }); dropzone.addEventListener('drop', (e) => { e.preventDefault(); dropzone.classList.remove('drag-over'); if (e.dataTransfer.files.length > 0) { addFiles(e.dataTransfer.files); } }); dropzone.addEventListener('click', () => { if (!uploading) fileInput.click(); }); fileInput.addEventListener('change', () => { if (fileInput.files.length > 0) { addFiles(fileInput.files); } fileInput.value = ''; }); function addFiles(fileListObj) { for (const f of fileListObj) { // Skip duplicates by name+size if (!selectedFiles.some(s => s.name === f.name && s.size === f.size)) { selectedFiles.push(f); } } renderFileList(); result.classList.add('hidden'); errorDiv.classList.add('hidden'); } function removeFile(index) { selectedFiles.splice(index, 1); renderFileList(); } function renderFileList() { if (selectedFiles.length === 0) { fileListEl.classList.add('hidden'); options.classList.add('hidden'); dropzone.querySelector('p').innerHTML = 'Drop files here or '; return; } fileListEl.classList.remove('hidden'); options.classList.remove('hidden'); // Hide custom slug for multi-file uploads slugGroup.classList.toggle('hidden', selectedFiles.length > 1); if (selectedFiles.length > 1) { document.getElementById('slug').value = ''; } const totalSize = selectedFiles.reduce((sum, f) => sum + f.size, 0); fileListEl.innerHTML = '
' + '' + selectedFiles.length + ' file' + (selectedFiles.length > 1 ? 's' : '') + ' (' + formatSize(totalSize) + ')' + '' + '
' + selectedFiles.map((f, i) => '
' + '' + escapeHtml(f.name) + '' + '' + formatSize(f.size) + '' + '' + '
' ).join(''); fileListEl.querySelector('.file-list-clear').addEventListener('click', () => { selectedFiles = []; renderFileList(); }); fileListEl.querySelectorAll('.file-list-remove').forEach(btn => { btn.addEventListener('click', () => removeFile(parseInt(btn.dataset.index))); }); dropzone.querySelector('p').innerHTML = 'Drop more files or '; } uploadBtn.addEventListener('click', () => { if (selectedFiles.length === 0 || uploading) return; uploadAll(); }); async function uploadAll() { uploading = true; const files = [...selectedFiles]; const results = []; const errors = []; options.classList.add('hidden'); fileListEl.classList.add('hidden'); dropzone.classList.add('hidden'); progressSection.classList.remove('hidden'); errorDiv.classList.add('hidden'); result.classList.add('hidden'); const expires = document.getElementById('expires').value; const password = document.getElementById('password').value; const slug = document.getElementById('slug').value.trim(); // Generate batch ID for multi-file uploads const batchId = files.length > 1 ? nanoid(8) : ''; for (let i = 0; i < files.length; i++) { const file = files[i]; if (files.length > 1) { progressOverall.textContent = 'File ' + (i + 1) + ' of ' + files.length; } else { progressOverall.textContent = ''; } progressFilename.textContent = file.name; progressFill.style.width = '0%'; progressText.textContent = '0%'; try { const data = await uploadOne(file, { expires, password, slug: files.length === 1 ? slug : '', batchId }); results.push(data); } catch (err) { errors.push({ name: file.name, error: err.message }); } } progressSection.classList.add('hidden'); uploading = false; if (results.length > 0) { showResults(results); } if (errors.length > 0) { showError(errors.map(e => e.name + ': ' + e.error).join('\n')); } // Reset selectedFiles = []; dropzone.classList.remove('hidden'); dropzone.querySelector('p').innerHTML = 'Drop files here or '; } function uploadOne(file, opts) { return new Promise((resolve, reject) => { const formData = new FormData(); if (opts.slug) formData.append('slug', opts.slug); if (opts.batchId) formData.append('batch_id', opts.batchId); formData.append('file', file); if (opts.expires) formData.append('expires_in', opts.expires); if (opts.password) formData.append('password', opts.password); const xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const pct = Math.round((e.loaded / e.total) * 100); progressFill.style.width = pct + '%'; progressText.textContent = pct + '% — ' + formatSize(e.loaded) + ' / ' + formatSize(e.total); } }); xhr.addEventListener('load', () => { if (xhr.status === 201) { resolve(JSON.parse(xhr.responseText)); } else if (xhr.status === 429) { // Rate limited — retry after a short delay setTimeout(() => { uploadOne(file, opts).then(resolve, reject); }, 1500); } else { reject(new Error(xhr.responseText || 'Upload failed')); } }); xhr.addEventListener('error', () => { reject(new Error('Network error')); }); xhr.open('POST', '/upload'); xhr.send(formData); }); } function showResults(results) { result.classList.remove('hidden'); if (results.length === 1) { resultHeading.textContent = 'Uploaded!'; resultSingle.classList.remove('hidden'); resultMulti.classList.add('hidden'); const data = results[0]; resultUrl.value = data.url; resultDelete.textContent = 'Delete: curl -X DELETE -H "Authorization: Bearer ' + data.delete_token + '" ' + data.delete_url; resultExpiry.textContent = data.expires_at ? 'Expires: ' + new Date(data.expires_at).toLocaleString() : ''; } else { resultHeading.textContent = results.length + ' files uploaded!'; resultSingle.classList.add('hidden'); resultMulti.classList.remove('hidden'); // Show batch URL if available const batchUrl = results[0] && results[0].batch_url; let html = ''; if (batchUrl) { html += '
' + '' + '' + '
'; } html += results.map(data => '
' + '' + escapeHtml(data.filename) + '' + '' + escapeHtml(data.url) + '' + '
' ).join(''); resultList.innerHTML = html; if (batchUrl) { document.getElementById('copy-batch-btn').onclick = () => { document.getElementById('batch-url').select(); navigator.clipboard.writeText(batchUrl).then(() => { document.getElementById('copy-batch-btn').textContent = 'Copied!'; setTimeout(() => { document.getElementById('copy-batch-btn').textContent = 'Copy'; }, 2000); }); }; } copyAllBtn.onclick = () => { const urls = results.map(d => d.url).join('\n'); navigator.clipboard.writeText(urls).then(() => { copyAllBtn.textContent = 'Copied!'; setTimeout(() => { copyAllBtn.textContent = 'Copy all URLs'; }, 2000); }); }; } } function showError(msg) { errorDiv.textContent = msg; errorDiv.classList.remove('hidden'); } copyBtn.addEventListener('click', () => { resultUrl.select(); navigator.clipboard.writeText(resultUrl.value).then(() => { copyBtn.textContent = 'Copied!'; setTimeout(() => { copyBtn.textContent = 'Copy'; }, 2000); }); }); function formatSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; } function escapeHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } function escapeAttr(s) { return s.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); } function nanoid(size) { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-'; const bytes = crypto.getRandomValues(new Uint8Array(size)); let id = ''; for (let i = 0; i < size; i++) id += alphabet[bytes[i] & 63]; return id; } })();