diff --git a/package-lock.json b/package-lock.json index 87e30cd..53b2c6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "@jsquash/jxl": "^1.0.0", "@jsquash/png": "^2.0.0", "@jsquash/webp": "^1.2.0", + "file-saver": "^2.0.5", + "jszip": "^3.10.1", "lucide-react": "^0.344.0", "react": "^18.3.1", "react-dom": "^18.3.1" @@ -1894,6 +1896,12 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2352,6 +2360,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2564,6 +2578,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2589,6 +2609,12 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2655,6 +2681,12 @@ "node": ">=0.12.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2744,6 +2776,18 @@ "node": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2766,6 +2810,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -3009,6 +3062,12 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3267,6 +3326,12 @@ "node": ">= 0.8.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3337,6 +3402,21 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -3443,6 +3523,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -3460,6 +3546,12 @@ "semver": "bin/semver.js" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3502,6 +3594,15 @@ "node": ">=0.10.0" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -3849,8 +3950,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vite": { "version": "5.4.8", diff --git a/package.json b/package.json index fbe2bfa..0c44399 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "@jsquash/jxl": "^1.0.0", "@jsquash/png": "^2.0.0", "@jsquash/webp": "^1.2.0", + "file-saver": "^2.0.5", + "jszip": "^3.10.1", "lucide-react": "^0.344.0", "react": "^18.3.1", "react-dom": "^18.3.1" @@ -35,4 +37,4 @@ "typescript-eslint": "^8.3.0", "vite": "^5.4.2" } -} \ No newline at end of file +} diff --git a/src/App.tsx b/src/App.tsx index a92286f..c276a64 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,25 @@ -import React, { useState, useCallback } from 'react'; -import { Image, Trash2 } from 'lucide-react'; -import { CompressionOptions } from './components/CompressionOptions'; -import { DropZone } from './components/DropZone'; -import { ImageList } from './components/ImageList'; -import { DownloadAll } from './components/DownloadAll'; -import { useImageQueue } from './hooks/useImageQueue'; -import { DEFAULT_QUALITY_SETTINGS } from './utils/formatDefaults'; -import type { ImageFile, OutputType, CompressionOptions as CompressionOptionsType } from './types'; +import React, { useState, useCallback } from "react"; +import { Image, Trash2 } from "lucide-react"; +import { CompressionOptions } from "./components/CompressionOptions"; +import { DropZone } from "./components/DropZone"; +import { ImageList } from "./components/ImageList"; +import { DownloadAll } from "./components/DownloadAll"; +import { useImageQueue } from "./hooks/useImageQueue"; + +import JSZip from "jszip"; +import { saveAs } from "file-saver"; + +import { DEFAULT_QUALITY_SETTINGS } from "./utils/formatDefaults"; +import type { + ImageFile, + OutputType, + CompressionOptions as CompressionOptionsType, +} from "./types"; +import { DownloadZip } from "./components/DownloadZip"; export function App() { const [images, setImages] = useState([]); - const [outputType, setOutputType] = useState('webp'); + const [outputType, setOutputType] = useState("webp"); const [options, setOptions] = useState({ quality: DEFAULT_QUALITY_SETTINGS.webp, }); @@ -19,34 +28,37 @@ export function App() { const handleOutputTypeChange = useCallback((type: OutputType) => { setOutputType(type); - if (type !== 'png') { + if (type !== "png") { setOptions({ quality: DEFAULT_QUALITY_SETTINGS[type] }); } }, []); - const handleFilesDrop = useCallback((newImages: ImageFile[]) => { - // First add all images to state - setImages((prev) => [...prev, ...newImages]); - - // Use requestAnimationFrame to wait for render to complete - requestAnimationFrame(() => { - // Then add to queue after UI has updated - newImages.forEach(image => addToQueue(image.id)); - }); - }, [addToQueue]); + const handleFilesDrop = useCallback( + (newImages: ImageFile[]) => { + // First add all images to state + setImages((prev) => [...prev, ...newImages]); + + // Use requestAnimationFrame to wait for render to complete + requestAnimationFrame(() => { + // Then add to queue after UI has updated + newImages.forEach((image) => addToQueue(image.id)); + }); + }, + [addToQueue] + ); const handleRemoveImage = useCallback((id: string) => { setImages((prev) => { - const image = prev.find(img => img.id === id); + const image = prev.find((img) => img.id === id); if (image?.preview) { URL.revokeObjectURL(image.preview); } - return prev.filter(img => img.id !== id); + return prev.filter((img) => img.id !== id); }); }, []); const handleClearAll = useCallback(() => { - images.forEach(image => { + images.forEach((image) => { if (image.preview) { URL.revokeObjectURL(image.preview); } @@ -70,7 +82,27 @@ export function App() { } }, [images]); - const completedImages = images.filter(img => img.status === 'complete').length; + const handleDownloadAllAsZip = useCallback(async () => { + const completedImages = images.filter((img) => img.status === "complete"); + + // Create a new zip instance + const zip = new JSZip(); + + for (const image of completedImages) { + if (image.blob && image.outputType) { + const fileName = `${image.file.name.split(".")[0]}.${image.outputType}`; + zip.file(fileName, image.blob); + } + } + + zip.generateAsync({ type: "blob" }).then((content) => { + saveAs(content, "images.zip"); + }); + }, [images]); + + const completedImages = images.filter( + (img) => img.status === "complete" + ).length; return (
@@ -96,7 +128,10 @@ export function App() { {completedImages > 0 && ( - +
+ + +
)} void; +} + +export function DownloadZip({ onDownloadAll }: DownloadZipProps) { + return ( + + ); +} \ No newline at end of file