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:
2026-01-20 22:11:53 -07:00
parent cc95608364
commit fb8567a4a9
2 changed files with 65 additions and 19 deletions

View File

@@ -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', () => {