diff --git a/website/package-lock.json b/website/package-lock.json index a2215cd5..ef3543aa 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -21,6 +21,7 @@ "dexie": "^4.0.8", "gpx": "file:../gpx", "immer": "^10.1.1", + "jszip": "^3.10.1", "lucide-static": "^0.460.0", "lucide-svelte": "^0.460.1", "mapbox-gl": "^3.9.1", @@ -3502,8 +3503,7 @@ "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==", - "dev": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "node_modules/create-ecdh": { "version": "4.0.4", @@ -5054,6 +5054,12 @@ "node": ">=18.0.0" } }, + "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/immer": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", @@ -5125,8 +5131,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/intl-messageformat": { "version": "10.5.14", @@ -5327,8 +5332,7 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/isexe": { "version": "2.0.0", @@ -5405,6 +5409,54 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "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/jszip/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/jszip/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/jszip/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/jszip/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/kdbush": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", @@ -5454,6 +5506,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", @@ -6688,8 +6749,7 @@ "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==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/protocol-buffers-schema": { "version": "3.6.0", @@ -7365,8 +7425,7 @@ "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==", - "dev": true + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, "node_modules/sha.js": { "version": "2.4.11", diff --git a/website/package.json b/website/package.json index 8dbe9eb0..d8903dfa 100644 --- a/website/package.json +++ b/website/package.json @@ -63,6 +63,7 @@ "dexie": "^4.0.8", "gpx": "file:../gpx", "immer": "^10.1.1", + "jszip": "^3.10.1", "lucide-static": "^0.460.0", "lucide-svelte": "^0.460.1", "mapbox-gl": "^3.9.1", diff --git a/website/src/lib/stores.ts b/website/src/lib/stores.ts index 70d6111c..bfd03494 100644 --- a/website/src/lib/stores.ts +++ b/website/src/lib/stores.ts @@ -23,6 +23,7 @@ import { } from '$lib/components/file-list/FileList'; import type { RoutingControls } from '$lib/components/toolbar/tools/routing/RoutingControls'; import { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte'; +import JSZip from 'jszip'; const { fileOrder } = settings; @@ -406,26 +407,52 @@ export function updateSelectionFromKey(down: boolean, shift: boolean) { } } -async function exportFiles(fileIds: string[], exclude: string[]) { - for (let fileId of fileIds) { - let file = getFile(fileId); +async function exportFilesAsZip(fileIds: string[], exclude: string[]) { + const zip = new JSZip(); + for (const fileId of fileIds) { + const file = getFile(fileId); if (file) { - exportFile(file, exclude); - await new Promise((resolve) => setTimeout(resolve, 200)); + const gpx = buildGPX(file, exclude); + zip.file(file.metadata.name + '.gpx', gpx); + } + } + if (Object.keys(zip.files).length > 0) { + const content = await zip.generateAsync({ type: 'blob' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(content); + link.download = 'gpx-export.zip'; + link.style.display = 'none'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(link.href); + } +} + +async function exportFiles(fileIds: string[], exclude: string[]) { + if (fileIds.length > 1) { + await exportFilesAsZip(fileIds, exclude) + } else { + const firstFileId = fileIds.at(0); + if (firstFileId != null) { + const file = getFile(firstFileId); + if (file) { + exportFile(file, exclude); + } } } } -export function exportSelectedFiles(exclude: string[]) { - let fileIds: string[] = []; +export async function exportSelectedFiles(exclude: string[]) { + const fileIds: string[] = []; applyToOrderedSelectedItemsFromFile(async (fileId, level, items) => { fileIds.push(fileId); }); - exportFiles(fileIds, exclude); + await exportFiles(fileIds, exclude); } -export function exportAllFiles(exclude: string[]) { - exportFiles(get(fileOrder), exclude); +export async function exportAllFiles(exclude: string[]) { + await exportFiles(get(fileOrder), exclude); } export function exportFile(file: GPXFile, exclude: string[]) {