284 lines
11 KiB
JavaScript
284 lines
11 KiB
JavaScript
(() => {
|
|
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 <label for="file-input" class="link">browse</label>';
|
|
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 = '<div class="file-list-header">' +
|
|
'<span>' + selectedFiles.length + ' file' + (selectedFiles.length > 1 ? 's' : '') +
|
|
' (' + formatSize(totalSize) + ')</span>' +
|
|
'<button class="file-list-clear link" type="button">Clear all</button>' +
|
|
'</div>' +
|
|
selectedFiles.map((f, i) =>
|
|
'<div class="file-list-item">' +
|
|
'<span class="file-list-name" title="' + escapeAttr(f.name) + '">' + escapeHtml(f.name) + '</span>' +
|
|
'<span class="file-list-size">' + formatSize(f.size) + '</span>' +
|
|
'<button class="file-list-remove" data-index="' + i + '" type="button" title="Remove">×</button>' +
|
|
'</div>'
|
|
).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 <label for="file-input" class="link">browse</label>';
|
|
}
|
|
|
|
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();
|
|
|
|
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 : ''
|
|
});
|
|
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 <label for="file-input" class="link">browse</label>';
|
|
}
|
|
|
|
function uploadOne(file, opts) {
|
|
return new Promise((resolve, reject) => {
|
|
const formData = new FormData();
|
|
if (opts.slug) formData.append('slug', opts.slug);
|
|
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');
|
|
|
|
resultList.innerHTML = results.map(data =>
|
|
'<div class="result-item">' +
|
|
'<a href="' + escapeAttr(data.url) + '" target="_blank" class="result-item-name">' + escapeHtml(data.filename) + '</a>' +
|
|
'<span class="result-item-url">' + escapeHtml(data.url) + '</span>' +
|
|
'</div>'
|
|
).join('');
|
|
|
|
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, '<').replace(/>/g, '>');
|
|
}
|
|
})();
|