From 1a9b726354841e8cc3ebeccc413adc92f6d07168 Mon Sep 17 00:00:00 2001 From: Sharif Date: Tue, 20 Oct 2026 07:25:12 +0600 Subject: [PATCH] Add batch cleaning with per-file review and ZIP download Up to 50 images at once; single-image flow unchanged. Includes a dependency-free stored-ZIP writer. Co-Authored-By: Claude Sonnet 5.5 --- src/lib/Batch.svelte | 281 ++++++++++++++++++++++++++++++++++++++++ src/lib/Dropzone.svelte | 39 ++++-- src/lib/Tool.svelte | 18 ++- src/lib/faq-data.js | 4 + src/lib/zip.js | 90 +++++++++++++ 5 files changed, 421 insertions(+), 11 deletions(-) create mode 100644 src/lib/Batch.svelte create mode 100644 src/lib/zip.js diff --git a/src/lib/Batch.svelte b/src/lib/Batch.svelte new file mode 100644 index 0000000..d8ecd31 --- /dev/null +++ b/src/lib/Batch.svelte @@ -0,0 +1,281 @@ + + +
+
+

+ {#if stage === "scanning"} + Scanning {scanned} of {items.length}… + {:else if stage === "review"} + {items.length} images ready, {dirty} with hidden data + {:else if stage === "cleaning"} + Cleaning {done} of {items.length}… + {:else} + {succeeded.length} of {items.length} cleaned + {/if} +

+ +
+ +
    + {#each items as item (item.id)} +
  • +
    + {item.file.name} + {formatBytes(item.file.size)} +
    + +
    + {#if item.state === "pending"} + Waiting + {:else if item.state === "cleaning"} + Cleaning + {:else if item.state === "done"} + Clean + + Download + + {:else if item.state === "failed"} + Could not re-encode + {:else if item.clean} + Already clean + {:else} + {#each item.labels.slice(0, 3) as label} + {label.label} + {/each} + {#if item.labels.length > 3} + +{item.labels.length - 3} more + {/if} + {/if} +
    +
  • + {/each} +
+ +
+ {#if stage === "done"} + + + {:else} + + + {/if} +
+
+ + diff --git a/src/lib/Dropzone.svelte b/src/lib/Dropzone.svelte index 38690da..49321ec 100644 --- a/src/lib/Dropzone.svelte +++ b/src/lib/Dropzone.svelte @@ -11,15 +11,33 @@ let fileInput; let depth = 0; - function offer(file) { - if (!file) return; - if (!isAccepted(file)) { + const MAX_FILES = 50; + + function offer(list) { + const incoming = [...(list || [])].filter(Boolean); + if (incoming.length === 0) return; + + const accepted = incoming.filter(isAccepted); + if (accepted.length === 0) { + const type = incoming[0].type; dispatch("reject", { - message: `Untrace cannot read ${file.type || "that file type"}. Choose a JPEG, PNG, TIFF, HEIC or AVIF image.`, + message: `Untrace cannot read ${type || "that file type"}. Choose JPEG, PNG, TIFF, HEIC or AVIF images.`, }); return; } - dispatch("select", { file }); + + const skipped = incoming.length - accepted.length; + const capped = accepted.slice(0, MAX_FILES); + const over = accepted.length - capped.length; + if (skipped > 0 || over > 0) { + dispatch("reject", { + message: + over > 0 + ? `Only the first ${MAX_FILES} images were added.` + : `Skipped ${skipped} file${skipped === 1 ? "" : "s"} Untrace cannot read.`, + }); + } + dispatch("select", { files: capped }); } function onDrop(event) { @@ -27,7 +45,7 @@ depth = 0; dragging = false; if (busy) return; - offer(event.dataTransfer?.files?.[0]); + offer(event.dataTransfer?.files); } function onDragEnter(event) { @@ -44,7 +62,7 @@ } function onChange(event) { - offer(event.target.files?.[0]); + offer(event.target.files); // Reset so choosing the same file twice still fires a change event. event.target.value = ""; } @@ -54,7 +72,7 @@ const item = [...(event.clipboardData?.items || [])].find((entry) => entry.type.startsWith("image/") ); - if (item) offer(item.getAsFile()); + if (item) offer([item.getAsFile()]); } @@ -77,10 +95,10 @@ > - {dragging ? "Drop to scan it" : "Drop an image, or choose a file"} + {dragging ? "Drop to scan" : "Drop images, or choose files"} - Nothing is uploaded. The image is read and rewritten inside this tab. + Nothing is uploaded. Add one image or up to 50 at once. JPEG @@ -98,6 +116,7 @@ 1) { + batchFiles = event.detail.files; + return; + } + file = event.detail.files[0]; previewUrl = URL.createObjectURL(file); metadata = null; stage = "scanning"; @@ -75,6 +82,7 @@ function reset() { release(); + batchFiles = null; file = null; metadata = null; stage = "idle"; @@ -86,6 +94,13 @@

Clean an image

+ {#if batchFiles} + dispatch("notify", e.detail)} + on:reset={reset} + /> + {:else}
    {#each steps as label, index}
  1. @@ -179,6 +194,7 @@ {/if} {/if} + {/if}