feat: Add multiple file/folder upload and enhanced drag-and-drop support
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
This commit is contained in:
@@ -87,9 +87,10 @@ function human(bytes){
|
||||
return (bytes/Math.pow(k,i)).toFixed(1)+' '+sizes[i];
|
||||
}
|
||||
|
||||
function addItem(file){
|
||||
function addItem(file, relativePath){
|
||||
const id = (crypto && crypto.randomUUID) ? crypto.randomUUID() : (Math.random().toString(36).slice(2));
|
||||
const it = { id, file, name: file.name, size: file.size, status: 'queued', progress: 0, relativePath: file.webkitRelativePath || '' };
|
||||
const resolvedPath = (relativePath !== undefined) ? relativePath : (file.webkitRelativePath || '');
|
||||
const it = { id, file, name: file.name, size: file.size, status: 'queued', progress: 0, relativePath: resolvedPath };
|
||||
items.unshift(it);
|
||||
render();
|
||||
}
|
||||
@@ -318,6 +319,7 @@ async function uploadChunked(next){
|
||||
// --- DOM refs ---
|
||||
const dz = document.getElementById('dropzone');
|
||||
const fi = document.getElementById('fileInput');
|
||||
const foi = document.getElementById('folderInput');
|
||||
const btnMobilePick = document.getElementById('btnMobilePick');
|
||||
const btnClearFinished = document.getElementById('btnClearFinished');
|
||||
const btnClearAll = document.getElementById('btnClearAll');
|
||||
@@ -380,10 +382,37 @@ if (btnPing) btnPing.onclick = async () => {
|
||||
// --- Drag & drop (no click-to-open on touch) ---
|
||||
['dragenter','dragover'].forEach(ev => dz.addEventListener(ev, e=>{ e.preventDefault(); dz.classList.add('border-blue-500','bg-blue-50','dark:bg-blue-900','dark:bg-opacity-20'); }));
|
||||
['dragleave','drop'].forEach(ev => dz.addEventListener(ev, e=>{ e.preventDefault(); dz.classList.remove('border-blue-500','bg-blue-50','dark:bg-blue-900','dark:bg-opacity-20'); }));
|
||||
dz.addEventListener('drop', (e)=>{
|
||||
dz.addEventListener('drop', async (e)=>{
|
||||
e.preventDefault();
|
||||
const files = Array.from(e.dataTransfer.files || []);
|
||||
files.forEach(addItem);
|
||||
const filesAndPaths = [];
|
||||
|
||||
const traverseFileTree = async (entry) => {
|
||||
if (!entry) return;
|
||||
if (entry.isFile) {
|
||||
return new Promise(resolve => {
|
||||
entry.file(file => {
|
||||
filesAndPaths.push({ file, path: entry.fullPath ? entry.fullPath.substring(1) : file.name });
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} else if (entry.isDirectory) {
|
||||
const reader = entry.createReader();
|
||||
const entries = await new Promise(resolve => reader.readEntries(resolve));
|
||||
for (const subEntry of entries) {
|
||||
await traverseFileTree(subEntry);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (e.dataTransfer.items && e.dataTransfer.items.length > 0 && e.dataTransfer.items[0].webkitGetAsEntry) {
|
||||
const promises = Array.from(e.dataTransfer.items).map(item => traverseFileTree(item.webkitGetAsEntry()));
|
||||
await Promise.all(promises);
|
||||
filesAndPaths.forEach(fp => addItem(fp.file, fp.path));
|
||||
} else {
|
||||
// Fallback for browsers without directory drop support
|
||||
Array.from(e.dataTransfer.files).forEach(file => addItem(file));
|
||||
}
|
||||
|
||||
render();
|
||||
runQueue();
|
||||
});
|
||||
@@ -413,20 +442,27 @@ fi.addEventListener('click', (e) => {
|
||||
// prevent bubbling to parents (extra safety)
|
||||
e.stopPropagation();
|
||||
});
|
||||
if (foi) {
|
||||
foi.addEventListener('click', (e) => { e.stopPropagation(); });
|
||||
}
|
||||
|
||||
fi.onchange = () => {
|
||||
const onFilesSelected = (inputEl) => {
|
||||
if (!inputEl) return;
|
||||
// Suppress any stray clicks for a short window after the picker closes
|
||||
suppressClicksUntil = Date.now() + 800;
|
||||
|
||||
const files = Array.from(fi.files || []);
|
||||
files.forEach(addItem);
|
||||
const files = Array.from(inputEl.files || []);
|
||||
files.forEach(file => addItem(file));
|
||||
render();
|
||||
runQueue();
|
||||
|
||||
// Reset a bit later so selecting the same items again still triggers 'change'
|
||||
setTimeout(() => { try { fi.value = ''; } catch {} }, 500);
|
||||
setTimeout(() => { try { inputEl.value = ''; } catch {} }, 500);
|
||||
};
|
||||
|
||||
fi.onchange = () => onFilesSelected(fi);
|
||||
if (foi) foi.onchange = () => onFilesSelected(foi);
|
||||
|
||||
// If you want the whole dropzone clickable on desktop only, enable this:
|
||||
if (!isTouch) {
|
||||
dz.addEventListener('click', () => {
|
||||
|
||||
Reference in New Issue
Block a user