From 5330223ce3703ea318f9f72c40507389cfc938e4 Mon Sep 17 00:00:00 2001 From: kamtschatka Date: Fri, 22 Nov 2024 15:17:51 +0100 Subject: [PATCH] Allow Epub generation from multiple bookmarks #295 switched epub generator added URL filtering changed the behavior to only create TOC for more than 1 bookmark added bulk edit operation + translations --- apps/web/app/api/epub/route.ts | 20 ++- .../dashboard/BulkBookmarksAction.tsx | 13 ++ .../dashboard/bookmarks/BookmarkOptions.tsx | 42 +++--- apps/web/lib/i18n/locales/de/translation.json | 1 + apps/web/lib/i18n/locales/en/translation.json | 1 + apps/web/lib/i18n/locales/fr/translation.json | 1 + apps/web/lib/i18n/locales/zh/translation.json | 1 + apps/web/package.json | 3 +- pnpm-lock.yaml | 127 ++++++++++++++++-- 9 files changed, 171 insertions(+), 38 deletions(-) diff --git a/apps/web/app/api/epub/route.ts b/apps/web/app/api/epub/route.ts index da81bc84..a3c93f01 100644 --- a/apps/web/app/api/epub/route.ts +++ b/apps/web/app/api/epub/route.ts @@ -1,5 +1,5 @@ import { getServerAuthSession } from "@/server/auth"; -import epub, { Chapter } from "@epubkit/epub-gen-memory"; +import epub, { Chapter } from "@kamtschatka/epub-gen-memory"; import { and, eq, inArray } from "drizzle-orm"; import { db } from "@hoarder/db"; @@ -50,9 +50,17 @@ export async function GET(request: Request) { }); const title = getTitle(chapters); + // If there is only 1 bookmark, we can skip the table of contents + const tocInTOC = chapters.length > 1; const generatedEpub = await epub( - { title, ignoreFailedDownloads: true }, + { + title, + ignoreFailedDownloads: true, + tocInTOC, + version: 3, + urlValidator, + }, chapters, ); @@ -65,6 +73,14 @@ export async function GET(request: Request) { }); } +function urlValidator(url: string): boolean { + const urlParsed = new URL(url); + if (urlParsed.protocol != "http:" && urlParsed.protocol != "https:") { + return true; + } + return ["localhost", "127.0.0.1", "0.0.0.0"].includes(urlParsed.hostname); +} + function createFilename(): string { const date = new Date(); const year = date.getFullYear(); diff --git a/apps/web/components/dashboard/BulkBookmarksAction.tsx b/apps/web/components/dashboard/BulkBookmarksAction.tsx index eb0e0cec..0d6bb687 100644 --- a/apps/web/components/dashboard/BulkBookmarksAction.tsx +++ b/apps/web/components/dashboard/BulkBookmarksAction.tsx @@ -12,6 +12,7 @@ import useBulkActionsStore from "@/lib/bulkActions"; import { useTranslation } from "@/lib/i18n/client"; import { CheckCheck, + Download, FileDown, Hash, Link, @@ -223,6 +224,18 @@ export default function BulkBookmarksAction() { isPending: recrawlBookmarkMutator.isPending, hidden: !isBulkEditEnabled, }, + { + name: t("actions.download_epub"), + icon: ( + "assetId=" + bookmark.id).join("&")}`} + > + + + ), + isPending: false, + hidden: !isBulkEditEnabled, + }, { name: t("actions.refresh"), icon: , diff --git a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx index 6df981fb..be65b17c 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx @@ -188,28 +188,28 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { )} {bookmark.content.type === BookmarkTypes.LINK && ( - <> - { - navigator.clipboard.writeText( - (bookmark.content as ZBookmarkedLink).url, - ); - toast({ - description: t("toasts.bookmarks.clipboard_copied"), - }); - }} - > - - {t("actions.copy_link")} - + <> + { + navigator.clipboard.writeText( + (bookmark.content as ZBookmarkedLink).url, + ); + toast({ + description: t("toasts.bookmarks.clipboard_copied"), + }); + }} + > + + {t("actions.copy_link")} + - - - - Download as EPUB - - - + + + + {t("actions.download_epub")} + + + )} setTagModalIsOpen(true)}> diff --git a/apps/web/lib/i18n/locales/de/translation.json b/apps/web/lib/i18n/locales/de/translation.json index 40a167ad..fc4e5188 100644 --- a/apps/web/lib/i18n/locales/de/translation.json +++ b/apps/web/lib/i18n/locales/de/translation.json @@ -39,6 +39,7 @@ "delete": "Löschen", "refresh": "Aktualisieren", "download_full_page_archive": "Vollständiges Seitenarchiv herunterladen", + "download_epub": "EPUB herunterladen", "edit_tags": "Tags bearbeiten", "add_to_list": "Zur Liste hinzufügen", "select_all": "Alle auswählen", diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json index 530d489a..dc4c692c 100644 --- a/apps/web/lib/i18n/locales/en/translation.json +++ b/apps/web/lib/i18n/locales/en/translation.json @@ -39,6 +39,7 @@ "delete": "Delete", "refresh": "Refresh", "download_full_page_archive": "Download Full Page Archive", + "download_epub": "Download EPUB", "edit_tags": "Edit Tags", "add_to_list": "Add to List", "select_all": "Select All", diff --git a/apps/web/lib/i18n/locales/fr/translation.json b/apps/web/lib/i18n/locales/fr/translation.json index d369ad44..a972490e 100644 --- a/apps/web/lib/i18n/locales/fr/translation.json +++ b/apps/web/lib/i18n/locales/fr/translation.json @@ -39,6 +39,7 @@ "delete": "Supprimer", "refresh": "Rafraîchir", "download_full_page_archive": "Télécharger l'archive de la page complète", + "download_epub": "Télécharger EPUB", "edit_tags": "Modifier les tags", "add_to_list": "Ajouter à la liste", "select_all": "Tout sélectionner", diff --git a/apps/web/lib/i18n/locales/zh/translation.json b/apps/web/lib/i18n/locales/zh/translation.json index 663933ec..cb67dea7 100644 --- a/apps/web/lib/i18n/locales/zh/translation.json +++ b/apps/web/lib/i18n/locales/zh/translation.json @@ -39,6 +39,7 @@ "delete": "删除", "refresh": "刷新", "download_full_page_archive": "下载完整页面归档", + "download_epub": "下载EPUB", "edit_tags": "编辑标签", "add_to_list": "添加到列表", "select_all": "全选", diff --git a/apps/web/package.json b/apps/web/package.json index 0f8a5903..e3aaed73 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -18,12 +18,12 @@ "@auth/drizzle-adapter": "^1.4.2", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", - "@epubkit/epub-gen-memory": "1.0.10-aplha.8", "@hoarder/db": "workspace:^0.1.0", "@hoarder/shared": "workspace:^0.1.0", "@hoarder/shared-react": "workspace:^0.1.0", "@hoarder/trpc": "workspace:^0.1.0", "@hookform/resolvers": "^3.3.4", + "@kamtschatka/epub-gen-memory": "^1.0.0", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -51,7 +51,6 @@ "csv-parse": "^5.5.6", "dayjs": "^1.11.10", "drizzle-orm": "^0.33.0", - "epub-gen-memory": "^1.0.10", "fastest-levenshtein": "^1.0.16", "i18next": "^23.16.5", "i18next-resources-to-backend": "^1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a272d71..a3c19661 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -484,6 +484,9 @@ importers: '@hookform/resolvers': specifier: ^3.3.4 version: 3.3.4(react-hook-form@7.50.1(react@18.2.0)) + '@kamtschatka/epub-gen-memory': + specifier: ^1.0.0 + version: 1.0.0 '@radix-ui/react-collapsible': specifier: ^1.0.3 version: 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -2987,6 +2990,10 @@ packages: '@jridgewell/trace-mapping@0.3.23': resolution: {integrity: sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==} + '@kamtschatka/epub-gen-memory@1.0.0': + resolution: {integrity: sha512-xspE1ObMhm7ngvFgQC336dUV25kN9Cktn2Ya429KO+cif8VCpnckmPxgF0hYSttTwz/LnB+8EQiyxCNxmd+v0g==} + engines: {node: '>=10.0.0'} + '@keyvhq/core@2.1.1': resolution: {integrity: sha512-wVnnVFWmtAvQP8v/Ugm8KSl4glrVZjb5uqVc1n5tbGzj45lZhG7F/YxCJ6qHGDfBtDEw5cp1nJ2qImdmaG/JEQ==} engines: {node: '>= 16'} @@ -4741,6 +4748,7 @@ packages: acorn-import-assertions@1.9.0: resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + deprecated: package has been renamed to acorn-import-attributes peerDependencies: acorn: ^8 @@ -6134,6 +6142,9 @@ packages: dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + diacritics@1.3.0: + resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -6406,6 +6417,10 @@ packages: entities@2.0.3: resolution: {integrity: sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==} + entities@3.0.1: + resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} + engines: {node: '>=0.12'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -7640,6 +7655,9 @@ packages: htmlparser2@6.1.0: resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + htmlparser2@7.2.0: + resolution: {integrity: sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==} + htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} @@ -7778,6 +7796,9 @@ packages: engines: {node: '>=16.x'} hasBin: true + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@9.0.21: resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} @@ -8409,6 +8430,9 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -8461,6 +8485,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lighthouse-logger@1.4.2: resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} @@ -8641,6 +8668,9 @@ packages: lodash.escaperegexp@4.1.2: resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} @@ -9756,6 +9786,10 @@ packages: resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==} deprecated: This package is no longer supported. + ow@0.28.2: + resolution: {integrity: sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==} + engines: {node: '>=12'} + p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -9835,6 +9869,9 @@ packages: resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} engines: {node: '>=14.16'} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} @@ -12523,6 +12560,10 @@ packages: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true + vali-date@1.0.0: + resolution: {integrity: sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==} + engines: {node: '>=0.10.0'} + valid-url@1.0.9: resolution: {integrity: sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==} @@ -16836,6 +16877,25 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + '@kamtschatka/epub-gen-memory@1.0.0': + dependencies: + abort-controller: 3.0.0 + css-select: 4.3.0 + diacritics: 1.3.0 + dom-serializer: 1.4.1 + domhandler: 4.3.1 + domutils: 2.8.0 + ejs: 3.1.9 + htmlparser2: 7.2.0 + jszip: 3.10.1 + mime: 2.6.0 + node-fetch: 2.7.0 + ow: 0.28.2 + slugify: 1.6.6 + transitivePeerDependencies: + - encoding + dev: false + '@keyvhq/core@2.1.1': dependencies: json-buffer: 3.0.1 @@ -20077,7 +20137,6 @@ snapshots: electron-to-chromium: 1.5.41 node-releases: 2.0.18 update-browserslist-db: 1.1.1(browserslist@4.24.0) - dev: false bser@2.1.1: dependencies: @@ -20254,8 +20313,7 @@ snapshots: caniuse-lite@1.0.30001589: {} - caniuse-lite@1.0.30001669: - dev: false + caniuse-lite@1.0.30001669: {} canvas@2.11.2: dependencies: @@ -21239,6 +21297,9 @@ snapshots: wrappy: 1.0.2 dev: false + diacritics@1.3.0: + dev: false + didyoumean@1.2.2: {} diff-sequences@29.6.3: @@ -21519,8 +21580,7 @@ snapshots: electron-to-chromium@1.4.681: {} - electron-to-chromium@1.5.41: - dev: false + electron-to-chromium@1.5.41: {} emoji-mart@5.5.2: dev: false @@ -21572,6 +21632,9 @@ snapshots: entities@2.0.3: dev: false + entities@3.0.1: + dev: false + entities@4.5.0: {} env-editor@0.4.2: @@ -21758,8 +21821,7 @@ snapshots: escalade@3.1.2: {} - escalade@3.2.0: - dev: false + escalade@3.2.0: {} escape-goat@4.0.0: dev: false @@ -23506,6 +23568,14 @@ snapshots: entities: 2.0.3 dev: false + htmlparser2@7.2.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 3.0.1 + dev: false + htmlparser2@8.0.2: dependencies: domelementtype: 2.3.0 @@ -23690,6 +23760,9 @@ snapshots: queue: 6.0.2 dev: false + immediate@3.0.6: + dev: false + immer@9.0.21: dev: false @@ -24449,6 +24522,14 @@ snapshots: object.assign: 4.1.5 object.values: 1.1.7 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + dev: false + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -24504,6 +24585,11 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + dev: false + lighthouse-logger@1.4.2: dependencies: debug: 2.6.9 @@ -24708,6 +24794,9 @@ snapshots: lodash.escaperegexp@4.1.2: dev: false + lodash.isequal@4.5.0: + dev: false + lodash.isplainobject@4.0.6: {} lodash.isstring@4.0.1: @@ -26222,8 +26311,7 @@ snapshots: node-releases@2.0.14: {} - node-releases@2.0.18: - dev: false + node-releases@2.0.18: {} node-stream-zip@1.15.0: dev: false @@ -26538,6 +26626,15 @@ snapshots: os-tmpdir: 1.0.2 dev: false + ow@0.28.2: + dependencies: + '@sindresorhus/is': 4.6.0 + callsites: 3.1.0 + dot-prop: 6.0.1 + lodash.isequal: 4.5.0 + vali-date: 1.0.0 + dev: false + p-cancelable@2.1.1: dev: false @@ -26636,6 +26733,9 @@ snapshots: semver: 7.6.3 dev: false + pako@1.0.11: + dev: false + param-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -26823,8 +26923,7 @@ snapshots: picocolors@1.0.0: {} - picocolors@1.1.1: - dev: false + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -30000,7 +30099,6 @@ snapshots: browserslist: 4.24.0 escalade: 3.2.0 picocolors: 1.1.1 - dev: false update-notifier@6.0.2: dependencies: @@ -30132,6 +30230,9 @@ snapshots: uuid@8.3.2: dev: false + vali-date@1.0.0: + dev: false + valid-url@1.0.9: dev: false @@ -30395,7 +30496,7 @@ snapshots: '@webassemblyjs/wasm-parser': 1.11.6 acorn: 8.11.3 acorn-import-assertions: 1.9.0(acorn@8.11.3) - browserslist: 4.23.0 + browserslist: 4.24.0 chrome-trace-event: 1.0.3 enhanced-resolve: 5.15.0 es-module-lexer: 1.4.1