- commit
- c656e4458f032c5baeeed9bdd279bd2ee726ce43
- parent
- c49b9072ae7ffef6d956cecd1ead029556b304a9
- author
- Dami <dami.etoile@gmail.com>
| M | docs/ | 2 | +- |
| M | internal/ | 73 | +++++++++++++++++++++++++----- |
| M | internal/ | 1 | + |
| M | scripts/ | 1 | - |
@@ -4,7 +4,7 @@ Use **Files** to search your uploads, import a public HTTPS URL or Blossom URI,
44 55 ## Uploading66 7-The **Upload** panel on the Files page stores files or a folder. Choose the files, check **folder** to select a whole folder, and check **encrypt** to encrypt in the browser before sending. Plain uploads store each file as it is and refresh the listing. **Import from URL** stores a copy of a public HTTPS URL or Blossom URI.7+The **Upload** panel on the Files page stores files or a folder. Choose files, choose a folder, or drop either onto the panel, and check **encrypt** to encrypt in the browser before sending. Plain uploads store each file as it is and refresh the listing. **Import from URL** stores a copy of a public HTTPS URL or Blossom URI.88 99 ## Encrypted files1010 @@ -151,13 +151,23 @@
151151 if (this.bound) return;152152 this.bound = true;153153 this.innerHTML =154- '<form><label>File or folder <input type="file" name="file" multiple required></label><label><input type="checkbox" name="folder"> folder</label><label><input type="checkbox" name="encrypt"> encrypt</label><button>Upload</button></form><p data-controls hidden><button type="button" data-cancel disabled>Cancel</button> <button type="button" data-retry disabled>Retry</button></p><label data-share hidden>Share link <input data-share-url readonly></label><output role="status"></output>';154+ '<form><label>Files <input type="file" name="file" multiple></label><input type="file" name="folder" webkitdirectory multiple hidden><p data-drop><button type="button" data-pick-folder>Choose folder</button> or drop files or a folder here</p><label><input type="checkbox" name="encrypt"> encrypt</label><button>Upload</button></form><p data-controls hidden><button type="button" data-cancel disabled>Cancel</button> <button type="button" data-retry disabled>Retry</button></p><label data-share hidden>Share link <input data-share-url readonly></label><output role="status"></output>';155155 this.out = this.querySelector("output");156156 const form = this.querySelector("form");157- const input = form.elements.namedItem("file");158- form.elements.namedItem("folder").addEventListener("change", event => {159- if (event.currentTarget.checked) input.setAttribute("webkitdirectory", "");160- else input.removeAttribute("webkitdirectory");157+ const files = form.elements.namedItem("file");158+ const folder = form.elements.namedItem("folder");159+ files.addEventListener("change", () => this.choose([...files.files]));160+ folder.addEventListener("change", () => this.choose([...folder.files]));161+ this.querySelector("[data-pick-folder]").addEventListener("click", () => folder.click());162+ this.addEventListener("dragover", event => {163+ event.preventDefault();164+ this.dataset.over = "";165+ });166+ this.addEventListener("dragleave", () => delete this.dataset.over);167+ this.addEventListener("drop", event => {168+ event.preventDefault();169+ delete this.dataset.over;170+ this.dropped(event.dataTransfer).catch(error => this.say(error.message, true));161171 });162172 form.addEventListener("submit", event => {163173 event.preventDefault();@@ -166,6 +176,49 @@
166176 this.querySelector("[data-cancel]").addEventListener("click", () => this.controller?.abort());167177 this.querySelector("[data-retry]").addEventListener("click", () => this.run().catch(() => {}));168178 }179+ // choose records a selection. A folder is recognized from the relative180+ // paths that folder pickers and folder drops attach to their files.181+ choose(files) {182+ const folder = files.some(file => (file.webkitRelativePath || "").includes("/"));183+ this.chosen = {files, folder};184+ if (files.length)185+ this.say(186+ folder187+ ? files.length + " files in a folder selected."188+ : files.length + (files.length === 1 ? " file selected." : " files selected.")189+ );190+ }191+ async dropped(transfer) {192+ const entries = [...(transfer.items || [])].map(item => item.webkitGetAsEntry?.()).filter(Boolean);193+ if (!entries.length) return this.choose([...transfer.files]);194+ const files = [];195+ const walk = (entry, path) =>196+ new Promise((resolve, reject) => {197+ if (files.length > MAX_FILES) return reject(Error("Choose a folder with at most 10,000 files."));198+ if (entry.isFile) {199+ entry.file(file => {200+ if (path) Object.defineProperty(file, "webkitRelativePath", {value: path + file.name});201+ files.push(file);202+ resolve();203+ }, reject);204+ return;205+ }206+ const reader = entry.createReader();207+ const next = () =>208+ reader.readEntries(async batch => {209+ if (!batch.length) return resolve();210+ try {211+ for (const child of batch) await walk(child, path + entry.name + "/");212+ } catch (error) {213+ return reject(error);214+ }215+ next();216+ }, reject);217+ next();218+ });219+ for (const entry of entries) await walk(entry, "");220+ this.choose(files);221+ }169222 disconnectedCallback() {170223 this.controller?.abort();171224 }@@ -183,12 +236,8 @@
183236 async run(form) {184237 if (this.busy) return;185238 if (form) {186- const files = [...form.elements.namedItem("file").files];187- this.selection = {188- files,189- folder: Boolean(form.elements.namedItem("folder").checked),190- encrypt: Boolean(form.elements.namedItem("encrypt").checked)191- };239+ if (!this.chosen) this.choose([...form.elements.namedItem("file").files]);240+ this.selection = {...this.chosen, encrypt: Boolean(form.elements.namedItem("encrypt").checked)};192241 this.pending = null;193242 }194243 if (!this.selection?.files.length) throw Error("Choose a file or folder first.");@@ -221,7 +270,7 @@
221270 } finally {222271 this.busy = false;223272 this.controls(false, completed);224- if (completed) this.selection = null;273+ if (completed) this.selection = this.chosen = null;225274 }226275 }227276 async storePlain(files) {@@ -115,6 +115,7 @@ rpc-form label input,rpc-form label select,signed-form label input,nostr-compose
115115 file-upload label[data-share]{display:block}file-upload input[data-share-url]{width:100%}116116 file-upload label:has([type=checkbox]),#panel file-upload label:has([type=checkbox]){display:inline-block;margin:8px 14px 8px 0}file-upload label:has([type=checkbox]) input,#panel file-upload label:has([type=checkbox]) input{display:inline-block;width:auto;min-width:0;margin:0 4px 0 0;vertical-align:middle}117117 #panel file-upload [hidden],file-upload [hidden],file-tools [hidden]{display:none}118+file-upload p[data-drop]{margin:6px 0;color:var(--muted);font-size:12px}file-upload[data-over]{outline:1px dashed var(--accent);outline-offset:4px}118119 #panel file-upload,#panel file-mirror{border:0;padding:0;max-width:none}#panel details{margin-top:8px}#panel details>summary{cursor:pointer;color:var(--muted);font-size:12px}119120 rpc-form label:has(textarea),publish-list label:has(textarea),nostr-compose label:has(textarea){display:block}120121 rpc-form output,signed-form output,publish-list output,nostr-compose output,file-tools output,file-upload output,file-workspace-root output,private-services output{display:block;font:12px var(--mono);color:var(--muted);margin-top:4px;overflow-wrap:anywhere}@@ -108,7 +108,6 @@ const workspaceServer = t => {
108108 upload.connectedCallback();109109 const form = upload.querySelector("form");110110 form.elements.namedItem("file").files = files;111- form.elements.namedItem("folder").checked = true;112111 form.elements.namedItem("encrypt").checked = true;113112 return upload.run(form);114113 };