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( + /)<[^<]*)*<\/script>/gi, + '', + ); + + // Remove style tags but keep inline styles + html = html.replace(/)<[^<]*)*<\/style>/gi, ''); + + // Ensure images have proper attributes + html = html.replace(/]*)>/gi, (match, attrs) => { + if (!attrs.includes('alt=')) { + return ``; + } + return match; + }); + + return html; +} + +function generateUUID(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +async function fetchCoverImage(coverUrl?: string): Promise<{ + data: ArrayBuffer | null; + mimeType: string; + extension: string; +}> { + if (!coverUrl) { + return { data: null, mimeType: '', extension: '' }; + } + + try { + let url = coverUrl; + if (!coverUrl.startsWith('http://') && !coverUrl.startsWith('https://')) { + url = coverUrl.startsWith('/') ? coverUrl : '/' + coverUrl; + } + + const response = await fetch(url); + + if (!response.ok) { + return { data: null, mimeType: '', extension: '' }; + } + + const arrayBuffer = await response.arrayBuffer(); + const contentType = response.headers.get('content-type') || 'image/jpeg'; + + let extension = 'jpg'; + if (contentType.includes('png')) extension = 'png'; + else if (contentType.includes('gif')) extension = 'gif'; + else if (contentType.includes('webp')) extension = 'webp'; + + if (extension === 'jpg') { + const urlLower = coverUrl.toLowerCase(); + if (urlLower.includes('.png')) extension = 'png'; + else if (urlLower.includes('.gif')) extension = 'gif'; + else if (urlLower.includes('.webp')) extension = 'webp'; + } + + return { + data: arrayBuffer, + mimeType: contentType, + extension, + }; + } catch (error) { + console.error('Error fetching cover image:', error); + return { data: null, mimeType: '', extension: '' }; + } +} + +export async function createEpub( + chapters: ChapterData[], + options: EpubOptions, +): Promise { + const zip = new JSZip(); + const uuid = generateUUID(); + const now = new Date().toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; + + const coverImage = await fetchCoverImage(options.cover); + const hasCover = coverImage.data !== null; + + zip.file('mimetype', 'application/epub+zip'); + + zip.file( + 'META-INF/container.xml', + ` + + + + +`, + ); + + if (hasCover && coverImage.data) { + zip.file(`OEBPS/cover.${coverImage.extension}`, coverImage.data); + } + + const manifestItems = [ + '', + '', + ]; + + if (hasCover) { + manifestItems.push( + ``, + '', + ); + } + + manifestItems.push( + ...chapters.map( + (_, i) => + ``, + ), + ); + + const spineItems = hasCover + ? ['', ''] + : ['']; + + spineItems.push( + ...chapters.map((_, i) => ``), + ); + + const contentOpf = ` + + + urn:uuid:${uuid} + ${escapeXml(options.title)} + ${options.author ? `${escapeXml(options.author)}` : ''} + ${options.description ? `${escapeXml(options.description)}` : ''} + ${options.language || 'en'} + ${now} + ${now} + ${hasCover ? '' : ''} + + + ${manifestItems.join('\n ')} + + + ${spineItems.join('\n ')} + +`; + + zip.file('OEBPS/content.opf', contentOpf); + + const tocNcx = ` + + + + + + + + + ${escapeXml(options.title)} + + + ${chapters + .map( + (chapter, i) => ` + + + ${escapeXml(chapter.title)} + + + `, + ) + .join('')} + +`; + + zip.file('OEBPS/toc.ncx', tocNcx); + + const navXhtml = ` + + + + Navigation + + + + + +`; + + zip.file('OEBPS/nav.xhtml', navXhtml); + + if (hasCover) { + const coverXhtml = ` + + + + Cover + + + + + Cover + +`; + zip.file('OEBPS/cover.xhtml', coverXhtml); + } + + chapters.forEach((chapter, i) => { + const sanitizedContent = sanitizeHtml(chapter.content); + const chapterXhtml = ` + + + + ${escapeXml(chapter.title)} + + + + +

${escapeXml(chapter.title)}

+ ${sanitizedContent} + +`; + + zip.file(`OEBPS/chapter${i + 1}.xhtml`, chapterXhtml); + }); + + const blob = await zip.generateAsync({ + type: 'blob', + mimeType: 'application/epub+zip', + }); + return blob; +} + +function escapeXml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +export function downloadBlob(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +}