feat: Add EPUB Export Support
This commit is contained in:
+2
-2
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/public/icon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Plugin testing</title>
|
||||
<title>Plugin Playground</title>
|
||||
<link href="/src/index.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Generated
+157
-28
@@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,6 +282,7 @@ export default function ParseNovelSection({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
@@ -285,6 +301,28 @@ export default function ParseNovelSection({
|
||||
<p>Copy novel path to clipboard</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2 bg-transparent"
|
||||
onClick={exportEpub}
|
||||
disabled={isExporting || chapters.length === 0}
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
{isExporting ? 'Exporting...' : 'Export EPUB'}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{isExporting
|
||||
? 'Exporting chapters to EPUB...'
|
||||
: 'Export all chapters as EPUB file'}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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 || '<p>No content available</p>',
|
||||
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: `<p>Error: Failed to fetch chapter content</p>`,
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -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<string, string> => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const result: Record<string, string> = {};
|
||||
urlParams.forEach((value, key) => {
|
||||
result[key] = value;
|
||||
});
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
getParam,
|
||||
setParam,
|
||||
removeParam,
|
||||
getAllParams,
|
||||
};
|
||||
};
|
||||
+285
@@ -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\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
|
||||
'',
|
||||
);
|
||||
|
||||
// Remove style tags but keep inline styles
|
||||
html = html.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '');
|
||||
|
||||
// Ensure images have proper attributes
|
||||
html = html.replace(/<img([^>]*)>/gi, (match, attrs) => {
|
||||
if (!attrs.includes('alt=')) {
|
||||
return `<img${attrs} alt="">`;
|
||||
}
|
||||
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<Blob> {
|
||||
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',
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||||
<rootfiles>
|
||||
<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
|
||||
</rootfiles>
|
||||
</container>`,
|
||||
);
|
||||
|
||||
if (hasCover && coverImage.data) {
|
||||
zip.file(`OEBPS/cover.${coverImage.extension}`, coverImage.data);
|
||||
}
|
||||
|
||||
const manifestItems = [
|
||||
'<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>',
|
||||
'<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml"/>',
|
||||
];
|
||||
|
||||
if (hasCover) {
|
||||
manifestItems.push(
|
||||
`<item id="cover-image" href="cover.${coverImage.extension}" media-type="${coverImage.mimeType}"/>`,
|
||||
'<item id="cover" href="cover.xhtml" media-type="application/xhtml+xml"/>',
|
||||
);
|
||||
}
|
||||
|
||||
manifestItems.push(
|
||||
...chapters.map(
|
||||
(_, i) =>
|
||||
`<item id="chapter${i + 1}" href="chapter${i + 1}.xhtml" media-type="application/xhtml+xml"/>`,
|
||||
),
|
||||
);
|
||||
|
||||
const spineItems = hasCover
|
||||
? ['<itemref idref="cover"/>', '<itemref idref="nav"/>']
|
||||
: ['<itemref idref="nav"/>'];
|
||||
|
||||
spineItems.push(
|
||||
...chapters.map((_, i) => `<itemref idref="chapter${i + 1}"/>`),
|
||||
);
|
||||
|
||||
const contentOpf = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="bookid" version="3.0">
|
||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
|
||||
<dc:identifier id="bookid">urn:uuid:${uuid}</dc:identifier>
|
||||
<dc:title>${escapeXml(options.title)}</dc:title>
|
||||
${options.author ? `<dc:creator>${escapeXml(options.author)}</dc:creator>` : ''}
|
||||
${options.description ? `<dc:description>${escapeXml(options.description)}</dc:description>` : ''}
|
||||
<dc:language>${options.language || 'en'}</dc:language>
|
||||
<dc:date>${now}</dc:date>
|
||||
<meta property="dcterms:modified">${now}</meta>
|
||||
${hasCover ? '<meta name="cover" content="cover-image"/>' : ''}
|
||||
</metadata>
|
||||
<manifest>
|
||||
${manifestItems.join('\n ')}
|
||||
</manifest>
|
||||
<spine toc="ncx">
|
||||
${spineItems.join('\n ')}
|
||||
</spine>
|
||||
</package>`;
|
||||
|
||||
zip.file('OEBPS/content.opf', contentOpf);
|
||||
|
||||
const tocNcx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
|
||||
<head>
|
||||
<meta name="dtb:uid" content="urn:uuid:${uuid}"/>
|
||||
<meta name="dtb:depth" content="1"/>
|
||||
<meta name="dtb:totalPageCount" content="0"/>
|
||||
<meta name="dtb:maxPageNumber" content="0"/>
|
||||
</head>
|
||||
<docTitle>
|
||||
<text>${escapeXml(options.title)}</text>
|
||||
</docTitle>
|
||||
<navMap>
|
||||
${chapters
|
||||
.map(
|
||||
(chapter, i) => `
|
||||
<navPoint id="navpoint-${i + 1}" playOrder="${i + 1}">
|
||||
<navLabel>
|
||||
<text>${escapeXml(chapter.title)}</text>
|
||||
</navLabel>
|
||||
<content src="chapter${i + 1}.xhtml"/>
|
||||
</navPoint>`,
|
||||
)
|
||||
.join('')}
|
||||
</navMap>
|
||||
</ncx>`;
|
||||
|
||||
zip.file('OEBPS/toc.ncx', tocNcx);
|
||||
|
||||
const navXhtml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
|
||||
<head>
|
||||
<title>Navigation</title>
|
||||
<meta charset="UTF-8"/>
|
||||
</head>
|
||||
<body>
|
||||
<nav epub:type="toc">
|
||||
<h1>Table of Contents</h1>
|
||||
<ol>
|
||||
${chapters.map((chapter, i) => `<li><a href="chapter${i + 1}.xhtml">${escapeXml(chapter.title)}</a></li>`).join('\n ')}
|
||||
</ol>
|
||||
</nav>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
zip.file('OEBPS/nav.xhtml', navXhtml);
|
||||
|
||||
if (hasCover) {
|
||||
const coverXhtml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>Cover</title>
|
||||
<meta charset="UTF-8"/>
|
||||
<style type="text/css">
|
||||
body { margin: 0; padding: 0; text-align: center; }
|
||||
img { max-width: 100%; height: auto; display: block; margin: 0 auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<img src="cover.${coverImage.extension}" alt="Cover"/>
|
||||
</body>
|
||||
</html>`;
|
||||
zip.file('OEBPS/cover.xhtml', coverXhtml);
|
||||
}
|
||||
|
||||
chapters.forEach((chapter, i) => {
|
||||
const sanitizedContent = sanitizeHtml(chapter.content);
|
||||
const chapterXhtml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>${escapeXml(chapter.title)}</title>
|
||||
<meta charset="UTF-8"/>
|
||||
<style type="text/css">
|
||||
body { font-family: serif; line-height: 1.6; margin: 1em; }
|
||||
h1, h2, h3 { margin-top: 1em; margin-bottom: 0.5em; }
|
||||
p { margin: 0.5em 0; text-align: justify; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${escapeXml(chapter.title)}</h1>
|
||||
${sanitizedContent}
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
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, '"')
|
||||
.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);
|
||||
}
|
||||
Reference in New Issue
Block a user