diff --git a/index.html b/index.html
index dc76d98..b51f916 100644
--- a/index.html
+++ b/index.html
@@ -2,9 +2,9 @@
-
+
- Plugin testing
+ Plugin Playground
diff --git a/package-lock.json b/package-lock.json
index f057a47..384692f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
"@tailwindcss/vite": "^4.1.14",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "jszip": "^3.10.1",
"lucide-react": "^0.546.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
@@ -283,6 +284,7 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.5.tgz",
"integrity": "sha512-6zeCUxUH+EPF1s+YF/2hPVODeV/7V07YU5x+2tfuRL8MdW6rv5vb2+CBEGTGwBdux0OIERcOS+RzxeK80k2DsQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -326,6 +328,7 @@
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.5.tgz",
"integrity": "sha512-gnOQ+nGLPvDXgIx119JqGalys64lhMdnNQA9TMxhDA4K0Hq5+++OE20Zs5GxiCV9r814xQ2K5WmtofSpHVW6BQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -1067,6 +1070,7 @@
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.9.tgz",
"integrity": "sha512-NwqIN0bdsgzSbZd5JFcC+2ez0XW/XNs8uiV2PDHrqQ4qf/FEasFJG1z6g8JbCN0YlTrHZekVb17X0Fv0qcYJfQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/core-downloads-tracker": "^6.1.9",
@@ -3002,6 +3006,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.66.tgz",
"integrity": "sha512-OYTmMI4UigXeFMF/j4uv0lBBEbongSgptPrHBxqME44h9+yNov+oL6Z3ocJKo0WyXR84sQUNeyIp9MRfckvZpg==",
"devOptional": true,
+ "peer": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -3013,6 +3018,7 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.22.tgz",
"integrity": "sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==",
"devOptional": true,
+ "peer": true,
"dependencies": {
"@types/react": "*"
}
@@ -3085,6 +3091,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz",
"integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==",
"dev": true,
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "7.14.1",
"@typescript-eslint/types": "7.14.1",
@@ -3300,6 +3307,7 @@
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
"integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4393,7 +4401,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/cosmiconfig": {
@@ -4959,6 +4966,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
"dev": true,
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -5834,6 +5842,12 @@
"node": ">=16.x"
}
},
+ "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",
@@ -5883,8 +5897,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/ini": {
"version": "1.3.8",
@@ -6110,7 +6123,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/isexe": {
@@ -6191,6 +6203,48 @@
"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/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/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -6213,6 +6267,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/lightningcss": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
@@ -7084,7 +7147,6 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
- "dev": true,
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": {
@@ -7377,7 +7439,6 @@
"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,
"license": "MIT"
},
"node_modules/prop-types": {
@@ -7567,6 +7628,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -7578,6 +7640,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -7597,6 +7660,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz",
"integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==",
"dev": true,
+ "peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.3",
"use-sync-external-store": "^1.0.0"
@@ -7749,7 +7813,8 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@@ -8182,7 +8247,6 @@
"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,
"license": "MIT"
},
"node_modules/sha.js": {
@@ -8574,6 +8638,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -8645,6 +8710,7 @@
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -8816,7 +8882,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "dev": true,
"license": "MIT"
},
"node_modules/vary": {
@@ -8834,6 +8899,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -8939,6 +9005,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -9333,6 +9400,7 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.5.tgz",
"integrity": "sha512-6zeCUxUH+EPF1s+YF/2hPVODeV/7V07YU5x+2tfuRL8MdW6rv5vb2+CBEGTGwBdux0OIERcOS+RzxeK80k2DsQ==",
"dev": true,
+ "peer": true,
"requires": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -9368,6 +9436,7 @@
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.5.tgz",
"integrity": "sha512-gnOQ+nGLPvDXgIx119JqGalys64lhMdnNQA9TMxhDA4K0Hq5+++OE20Zs5GxiCV9r814xQ2K5WmtofSpHVW6BQ==",
"dev": true,
+ "peer": true,
"requires": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -9756,6 +9825,7 @@
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.9.tgz",
"integrity": "sha512-NwqIN0bdsgzSbZd5JFcC+2ez0XW/XNs8uiV2PDHrqQ4qf/FEasFJG1z6g8JbCN0YlTrHZekVb17X0Fv0qcYJfQ==",
"dev": true,
+ "peer": true,
"requires": {
"@babel/runtime": "^7.26.0",
"@mui/core-downloads-tracker": "^6.1.9",
@@ -10734,6 +10804,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.66.tgz",
"integrity": "sha512-OYTmMI4UigXeFMF/j4uv0lBBEbongSgptPrHBxqME44h9+yNov+oL6Z3ocJKo0WyXR84sQUNeyIp9MRfckvZpg==",
"devOptional": true,
+ "peer": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -10745,6 +10816,7 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.22.tgz",
"integrity": "sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==",
"devOptional": true,
+ "peer": true,
"requires": {
"@types/react": "*"
}
@@ -10801,6 +10873,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz",
"integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==",
"dev": true,
+ "peer": true,
"requires": {
"@typescript-eslint/scope-manager": "7.14.1",
"@typescript-eslint/types": "7.14.1",
@@ -10929,7 +11002,8 @@
"acorn": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
- "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw=="
+ "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==",
+ "peer": true
},
"acorn-jsx": {
"version": "5.3.2",
@@ -11702,8 +11776,7 @@
"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=="
},
"cosmiconfig": {
"version": "7.1.0",
@@ -12131,6 +12204,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
"dev": true,
+ "peer": true,
"requires": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -12738,6 +12812,11 @@
"queue": "6.0.2"
}
},
+ "immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
+ },
"immer": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
@@ -12773,8 +12852,7 @@
"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=="
},
"ini": {
"version": "1.3.8",
@@ -12910,8 +12988,7 @@
"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=="
},
"isexe": {
"version": "2.0.0",
@@ -12974,6 +13051,46 @@
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true
},
+ "jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "requires": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "requires": {
+ "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"
+ }
+ },
+ "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=="
+ },
+ "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==",
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
"keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -12993,6 +13110,14 @@
"type-check": "~0.4.0"
}
},
+ "lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "requires": {
+ "immediate": "~3.0.5"
+ }
+ },
"lightningcss": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
@@ -13521,8 +13646,7 @@
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
- "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
- "dev": true
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"parent-module": {
"version": "1.0.1",
@@ -13717,8 +13841,7 @@
"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=="
},
"prop-types": {
"version": "15.8.1",
@@ -13866,6 +13989,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "peer": true,
"requires": {
"loose-envify": "^1.1.0"
}
@@ -13874,6 +13998,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "peer": true,
"requires": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -13890,6 +14015,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz",
"integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==",
"dev": true,
+ "peer": true,
"requires": {
"@types/use-sync-external-store": "^0.0.3",
"use-sync-external-store": "^1.0.0"
@@ -13969,7 +14095,8 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"redux-thunk": {
"version": "3.1.0",
@@ -14276,8 +14403,7 @@
"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=="
},
"sha.js": {
"version": "2.4.11",
@@ -14539,7 +14665,8 @@
"picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
- "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "peer": true
}
}
},
@@ -14588,7 +14715,8 @@
"typescript": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
- "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg=="
+ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
+ "peer": true
},
"typescript-eslint": {
"version": "7.14.1",
@@ -14699,8 +14827,7 @@
"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=="
},
"vary": {
"version": "1.1.2",
@@ -14712,6 +14839,7 @@
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
+ "peer": true,
"requires": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -14731,7 +14859,8 @@
"picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
- "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "peer": true
}
}
},
diff --git a/package.json b/package.json
index aa63d14..92c4f2e 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"@tailwindcss/vite": "^4.1.14",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "jszip": "^3.10.1",
"lucide-react": "^0.546.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
diff --git a/src/components/parse-novel.tsx b/src/components/parse-novel.tsx
index ceca2ea..5b2bdc5 100644
--- a/src/components/parse-novel.tsx
+++ b/src/components/parse-novel.tsx
@@ -1,5 +1,12 @@
import React, { useState, useEffect } from 'react';
-import { BookOpen, ChevronLeft, ChevronRight, Copy, Zap } from 'lucide-react';
+import {
+ BookOpen,
+ ChevronLeft,
+ ChevronRight,
+ Copy,
+ Zap,
+ Download,
+} from 'lucide-react';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
@@ -14,6 +21,7 @@ import {
} from '@/components/ui/tooltip';
import { useAppStore } from '@/store';
import { Plugin } from '@/types/plugin';
+import { useEpubExport } from '@/hooks/useEpubExport';
type ParseNovelSectionProps = {
onNavigateToParseChapter?: () => void;
@@ -38,6 +46,13 @@ export default function ParseNovelSection({
const [currentPage, setCurrentPage] = useState(1);
const [fetchError, setFetchError] = useState('');
+ const { exportEpub, isExporting } = useEpubExport({
+ plugin: plugin || null,
+ sourceNovel,
+ chapters,
+ novelPath,
+ });
+
const fetchNovelByPath = async (path: string) => {
if (plugin && path.trim()) {
setLoading(true);
@@ -267,24 +282,47 @@ export default function ParseNovelSection({
)}
-
-
-
-
-
- Copy novel path to clipboard
-
-
+
+
+
+
+
+
+ Copy novel path to clipboard
+
+
+
+
+
+
+
+
+ {isExporting
+ ? 'Exporting chapters to EPUB...'
+ : 'Export all chapters as EPUB file'}
+
+
+
+
diff --git a/src/hooks/useEpubExport.ts b/src/hooks/useEpubExport.ts
new file mode 100644
index 0000000..8a8fad0
--- /dev/null
+++ b/src/hooks/useEpubExport.ts
@@ -0,0 +1,151 @@
+import { useState } from 'react';
+import { toast } from 'sonner';
+import { Plugin } from '@/types/plugin';
+import { createEpub, downloadBlob } from '@/lib/epub';
+
+interface UseEpubExportOptions {
+ plugin: Plugin.PluginBase | null;
+ sourceNovel: (Plugin.SourceNovel & { totalPages?: number }) | undefined;
+ chapters: Plugin.ChapterItem[];
+ novelPath: string;
+}
+
+export function useEpubExport({
+ plugin,
+ sourceNovel,
+ chapters,
+ novelPath,
+}: UseEpubExportOptions) {
+ const [isExporting, setIsExporting] = useState(false);
+
+ const exportEpub = async () => {
+ if (!plugin || !sourceNovel || chapters.length === 0) {
+ toast.error('No novel or chapters available to export');
+ return;
+ }
+
+ setIsExporting(true);
+ const toastId = toast.loading('Starting EPUB export...', {
+ description: `Preparing to export ${chapters.length} chapters`,
+ });
+
+ try {
+ const allChapters: Plugin.ChapterItem[] = [];
+
+ if (sourceNovel.totalPages && sourceNovel.totalPages > 1) {
+ toast.loading('Fetching all chapters...', {
+ id: toastId,
+ description: `Found ${sourceNovel.totalPages} pages`,
+ });
+
+ for (let page = 1; page <= sourceNovel.totalPages; page++) {
+ try {
+ const pageResult = await (plugin as Plugin.PagePlugin).parsePage(
+ novelPath,
+ page.toString(),
+ );
+ allChapters.push(...pageResult.chapters);
+
+ toast.loading('Fetching chapters...', {
+ id: toastId,
+ description: `Page ${page}/${sourceNovel.totalPages} - ${allChapters.length} chapters collected`,
+ });
+ } catch (error) {
+ console.error(`Error fetching page ${page}:`, error);
+ }
+ }
+ } else {
+ allChapters.push(...chapters);
+ }
+
+ if (allChapters.length === 0) {
+ toast.error('No chapters found to export', { id: toastId });
+ setIsExporting(false);
+ return;
+ }
+
+ toast.loading('Fetching chapter content...', {
+ id: toastId,
+ description: `0/${allChapters.length} chapters processed`,
+ });
+
+ const chapterContents: Array<{
+ title: string;
+ content: string;
+ path: string;
+ }> = [];
+
+ for (let i = 0; i < allChapters.length; i++) {
+ const chapter = allChapters[i];
+ try {
+ const content = await plugin.parseChapter(chapter.path);
+ chapterContents.push({
+ title: chapter.name,
+ content: content || 'No content available
',
+ path: chapter.path,
+ });
+
+ const progress = Math.round(((i + 1) / allChapters.length) * 100);
+ toast.loading('Fetching chapter content...', {
+ id: toastId,
+ description: `${i + 1}/${allChapters.length} chapters processed (${progress}%)`,
+ });
+ } catch (error) {
+ console.error(`Error fetching chapter ${i + 1}:`, error);
+ chapterContents.push({
+ title: chapter.name,
+ content: `Error: Failed to fetch chapter content
`,
+ path: chapter.path,
+ });
+ }
+ }
+
+ toast.loading('Generating EPUB file...', {
+ id: toastId,
+ description: 'Creating EPUB structure',
+ });
+
+ let coverUrl = sourceNovel.cover;
+ if (coverUrl && plugin.resolveUrl) {
+ coverUrl = plugin.resolveUrl(coverUrl, true);
+ } else if (
+ coverUrl &&
+ !coverUrl.startsWith('http://') &&
+ !coverUrl.startsWith('https://')
+ ) {
+ coverUrl = coverUrl.startsWith('/') ? coverUrl : '/' + coverUrl;
+ }
+
+ const epubBlob = await createEpub(chapterContents, {
+ title: sourceNovel.name,
+ author: sourceNovel.author,
+ description: sourceNovel.summary,
+ cover: coverUrl,
+ language: 'en',
+ });
+
+ const filename = `${sourceNovel.name.replace(/[^a-z0-9]/gi, '_')}.epub`;
+ downloadBlob(epubBlob, filename);
+
+ toast.success('EPUB exported successfully!', {
+ id: toastId,
+ description: `Downloaded ${allChapters.length} chapters as ${filename}`,
+ });
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Failed to export EPUB';
+ toast.error('Export failed', {
+ id: toastId,
+ description: errorMessage,
+ });
+ console.error('Error exporting EPUB:', error);
+ } finally {
+ setIsExporting(false);
+ }
+ };
+
+ return {
+ exportEpub,
+ isExporting,
+ };
+}
diff --git a/src/hooks/useQueryParams.ts b/src/hooks/useQueryParams.ts
deleted file mode 100644
index 8648820..0000000
--- a/src/hooks/useQueryParams.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useCallback } from 'react';
-
-export const useQueryParams = () => {
- const getParam = useCallback((key: string): string | null => {
- const urlParams = new URLSearchParams(window.location.search);
- return urlParams.get(key);
- }, []);
-
- const setParam = useCallback((key: string, value: string) => {
- const url = new URL(window.location.href);
- url.searchParams.set(key, value);
- window.history.pushState({}, '', url);
- }, []);
-
- const removeParam = useCallback((key: string) => {
- const url = new URL(window.location.href);
- url.searchParams.delete(key);
- window.history.pushState({}, '', url);
- }, []);
-
- const getAllParams = useCallback((): Record => {
- const urlParams = new URLSearchParams(window.location.search);
- const result: Record = {};
- urlParams.forEach((value, key) => {
- result[key] = value;
- });
- return result;
- }, []);
-
- return {
- getParam,
- setParam,
- removeParam,
- getAllParams,
- };
-};
diff --git a/src/lib/epub.ts b/src/lib/epub.ts
new file mode 100644
index 0000000..00676e2
--- /dev/null
+++ b/src/lib/epub.ts
@@ -0,0 +1,285 @@
+import JSZip from 'jszip';
+import { Plugin } from '@/types/plugin';
+
+export interface EpubOptions {
+ title: string;
+ author?: string;
+ description?: string;
+ cover?: string;
+ language?: string;
+}
+
+export interface ChapterData {
+ title: string;
+ content: string;
+ path: string;
+}
+
+function sanitizeHtml(html: string): string {
+ // Remove script tags and their content
+ html = html.replace(
+ /