From 5c7d6759606cab0a8dda74ac32fb562ee7d9e9f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= <127139797+balazs-szucs@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:10:43 +0100 Subject: [PATCH] deps(embedPDF): Bump codebase to embedPDF v2.3.0 and adjust codebase for new features (#5567) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Signed-off-by: Balázs Szücs Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> --- build.gradle | 2 +- frontend/package-lock.json | 398 ++++++++++-------- frontend/package.json | 43 +- .../viewer/ActiveDocumentContext.tsx | 64 +++ .../components/viewer/AnnotationAPIBridge.tsx | 112 +++-- .../components/viewer/BookmarkAPIBridge.tsx | 3 +- .../components/viewer/CustomSearchLayer.tsx | 27 +- .../viewer/DocumentPermissionsAPIBridge.tsx | 63 +++ .../viewer/DocumentReadyWrapper.tsx | 56 +++ .../core/components/viewer/EmbedPdfViewer.tsx | 15 - .../src/core/components/viewer/LinkLayer.tsx | 386 ++++++++++------- .../core/components/viewer/LocalEmbedPDF.tsx | 205 +++++---- .../viewer/LocalEmbedPDFWithAnnotations.tsx | 162 ++++--- .../core/components/viewer/PanAPIBridge.tsx | 50 ++- .../components/viewer/RedactionAPIBridge.tsx | 22 +- .../viewer/RedactionPendingTracker.tsx | 24 +- .../viewer/RedactionSelectionMenu.tsx | 91 ++-- .../components/viewer/RotateAPIBridge.tsx | 43 +- .../components/viewer/ScrollAPIBridge.tsx | 41 +- .../components/viewer/SearchAPIBridge.tsx | 122 +++++- .../components/viewer/SelectionAPIBridge.tsx | 12 +- .../components/viewer/SpreadAPIBridge.tsx | 38 +- .../core/components/viewer/ZoomAPIBridge.tsx | 85 +++- .../components/viewer/useActiveDocumentId.ts | 9 + frontend/src/core/contexts/ViewerContext.tsx | 34 ++ .../src/core/contexts/viewer/viewerActions.ts | 41 +- .../src/core/contexts/viewer/viewerBridges.ts | 35 ++ .../tools/annotate/useAnnotationSelection.ts | 6 +- 28 files changed, 1407 insertions(+), 782 deletions(-) create mode 100644 frontend/src/core/components/viewer/ActiveDocumentContext.tsx create mode 100644 frontend/src/core/components/viewer/DocumentPermissionsAPIBridge.tsx create mode 100644 frontend/src/core/components/viewer/DocumentReadyWrapper.tsx create mode 100644 frontend/src/core/components/viewer/useActiveDocumentId.ts diff --git a/build.gradle b/build.gradle index 404639a4f..2366c6a92 100644 --- a/build.gradle +++ b/build.gradle @@ -537,4 +537,4 @@ tasks.register('buildRestartHelper', Jar) { doLast { println "restart-helper.jar created at: ${destinationDirectory.get()}/restart-helper.jar" } -} \ No newline at end of file +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f54207ac2..79bb4fbe8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,27 +11,28 @@ "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.7.7", "@dnd-kit/core": "^6.3.1", - "@embedpdf/core": "^1.5.0", - "@embedpdf/engines": "^1.5.0", - "@embedpdf/plugin-annotation": "^1.5.0", - "@embedpdf/plugin-bookmark": "^1.5.0", - "@embedpdf/plugin-export": "^1.5.0", - "@embedpdf/plugin-history": "^1.5.0", - "@embedpdf/plugin-interaction-manager": "^1.5.0", - "@embedpdf/plugin-loader": "^1.5.0", - "@embedpdf/plugin-pan": "^1.5.0", - "@embedpdf/plugin-print": "^1.5.0", - "@embedpdf/plugin-redaction": "^1.5.0", - "@embedpdf/plugin-render": "^1.5.0", - "@embedpdf/plugin-rotate": "^1.5.0", - "@embedpdf/plugin-scroll": "^1.5.0", - "@embedpdf/plugin-search": "^1.5.0", - "@embedpdf/plugin-selection": "^1.5.0", - "@embedpdf/plugin-spread": "^1.5.0", - "@embedpdf/plugin-thumbnail": "^1.5.0", - "@embedpdf/plugin-tiling": "^1.5.0", - "@embedpdf/plugin-viewport": "^1.5.0", - "@embedpdf/plugin-zoom": "^1.5.0", + "@embedpdf/core": "^2.3.0", + "@embedpdf/engines": "^2.3.0", + "@embedpdf/models": "^2.3.0", + "@embedpdf/plugin-annotation": "^2.3.0", + "@embedpdf/plugin-bookmark": "^2.3.0", + "@embedpdf/plugin-document-manager": "^2.3.0", + "@embedpdf/plugin-export": "^2.3.0", + "@embedpdf/plugin-history": "^2.3.0", + "@embedpdf/plugin-interaction-manager": "^2.3.0", + "@embedpdf/plugin-pan": "^2.3.0", + "@embedpdf/plugin-print": "^2.3.0", + "@embedpdf/plugin-redaction": "^2.3.0", + "@embedpdf/plugin-render": "^2.3.0", + "@embedpdf/plugin-rotate": "^2.3.0", + "@embedpdf/plugin-scroll": "^2.3.0", + "@embedpdf/plugin-search": "^2.3.0", + "@embedpdf/plugin-selection": "^2.3.0", + "@embedpdf/plugin-spread": "^2.3.0", + "@embedpdf/plugin-thumbnail": "^2.3.0", + "@embedpdf/plugin-tiling": "^2.3.0", + "@embedpdf/plugin-viewport": "^2.3.0", + "@embedpdf/plugin-zoom": "^2.3.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@iconify/react": "^6.0.2", @@ -552,13 +553,13 @@ } }, "node_modules/@embedpdf/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.5.0.tgz", - "integrity": "sha512-Yrh9XoVaT8cUgzgqpJ7hx5wg6BqQrCFirqqlSwVb+Ly9oNn4fZbR9GycIWmzJOU5XBnaOJjXfQSaDyoNP0woNA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-2.3.0.tgz", + "integrity": "sha512-aPD7lNSCOLc5Nos9xGA3qAT5jFZdrTT7IVcpxtM1BOKa1FI0XmotJ8vgzcRxH/FLwUASC4xwR9QxzTKp2aLsZQ==", "license": "MIT", "dependencies": { - "@embedpdf/engines": "1.5.0", - "@embedpdf/models": "1.5.0" + "@embedpdf/engines": "2.3.0", + "@embedpdf/models": "2.3.0" }, "peerDependencies": { "preact": "^10.26.4", @@ -569,13 +570,20 @@ } }, "node_modules/@embedpdf/engines": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-1.5.0.tgz", - "integrity": "sha512-/GzhjHFHWfOaX7vjgFJX/pyq668wYjoda1bZ9MpwF/EF000Wwy2Q0AOhprjldPFz8ASKjwKwqsXmaqrK99yOAQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-2.3.0.tgz", + "integrity": "sha512-QxNY58E2HgNgnbsTt5TnDUNvKoyabkf5IniGsiN5+rx6f4SFDpCnz3h1VJxNReWDyn9e16QlkUfgXX0qQWd3iQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0", - "@embedpdf/pdfium": "1.5.0" + "@embedpdf/fonts-arabic": "1.0.0", + "@embedpdf/fonts-hebrew": "1.0.0", + "@embedpdf/fonts-jp": "1.0.0", + "@embedpdf/fonts-kr": "1.0.0", + "@embedpdf/fonts-latin": "1.0.0", + "@embedpdf/fonts-sc": "1.0.0", + "@embedpdf/fonts-tc": "1.0.0", + "@embedpdf/models": "2.3.0", + "@embedpdf/pdfium": "2.3.0" }, "peerDependencies": { "preact": "^10.26.4", @@ -585,64 +593,125 @@ "vue": ">=3.2.0" } }, + "node_modules/@embedpdf/fonts-arabic": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@embedpdf/fonts-arabic/-/fonts-arabic-1.0.0.tgz", + "integrity": "sha512-SnGvQb+LwPZQO2WjjvlmXrJZolJUfLYbLZQSaYUw1vrQyMyJKT4LewvJGG+hZ+Yz2fz7OMIQ+4Gc98mGODZtOg==", + "license": "OFL-1.1" + }, + "node_modules/@embedpdf/fonts-hebrew": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@embedpdf/fonts-hebrew/-/fonts-hebrew-1.0.0.tgz", + "integrity": "sha512-5HVAKGL7VqPeTxxADDrSqAFBxfmAXdP8fIqrPwJIKkqdK2643bOer8CqnnpO3/nPoFhkzxhttWMB9BGiqSW62w==", + "license": "OFL-1.1" + }, + "node_modules/@embedpdf/fonts-jp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@embedpdf/fonts-jp/-/fonts-jp-1.0.0.tgz", + "integrity": "sha512-BY2tv/mcICUUKf+M/bizf3RU65PMqKClJ/e5o9mgMibxyML0OQvEDwYMRPODQkKgJKXCO3ScHmVvcmXp6kt+fA==", + "license": "OFL-1.1" + }, + "node_modules/@embedpdf/fonts-kr": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@embedpdf/fonts-kr/-/fonts-kr-1.0.0.tgz", + "integrity": "sha512-bh88HXSvOBS581kgmihWY7Ijp9hBsvlmXogFG5LSNx9UBAobRcakZiFMGieRBc06hUSkpo7WhjaFM/z/SfQ8dQ==", + "license": "OFL-1.1" + }, + "node_modules/@embedpdf/fonts-latin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@embedpdf/fonts-latin/-/fonts-latin-1.0.0.tgz", + "integrity": "sha512-LLYysdr8O6sRNzhmW3PbF3AeA8xnqvOi4XLFfIfNlW5uEZ+qsJdcfd78Q78sFJMhlaOAYFMziMMsnOzmx463rA==", + "license": "OFL-1.1" + }, + "node_modules/@embedpdf/fonts-sc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@embedpdf/fonts-sc/-/fonts-sc-1.0.0.tgz", + "integrity": "sha512-ETXl7XCwaQLSSvMO3EUDwMNqtL64kX2LlFxarTRi/NsIGGOIxUurGfKtrkmtnKHrWy1jAJSt6oxK2uJhvdvQIw==", + "license": "OFL-1.1" + }, + "node_modules/@embedpdf/fonts-tc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@embedpdf/fonts-tc/-/fonts-tc-1.0.0.tgz", + "integrity": "sha512-rGZJbVD6DYS5BbXdpEMnWkpVF0Knar+bsiyb2o3+YRx7O8eyFubEBQUSUInirQk69HA6fc3GhYCg7TyC/oD76Q==", + "license": "OFL-1.1" + }, "node_modules/@embedpdf/models": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-1.5.0.tgz", - "integrity": "sha512-x/1li3jdag+IzfZkcfRLKLqASLep4v6dgVi3z0JArwaicFra8k1IY2xaVTrwcZyx7pRb/rxvoO9yLHW0Y34NFw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-2.3.0.tgz", + "integrity": "sha512-YAH3YdXl/UOhVcvMPd6mtU+tJ3veh24Q5swRDfuWUsJ3L2CcAG2P+4pjj4EAwvWUQcmN/HlVOjVQL0PkbkytKw==", "license": "MIT" }, "node_modules/@embedpdf/pdfium": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-1.5.0.tgz", - "integrity": "sha512-PI32t2U4ThZC907n2Iwr8E5WqmC574G83u3V9ysNFl29N9kasrY9RiLSzU4W/yQvXPjIbpQHBsbMKXLjCFBI9w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-2.3.0.tgz", + "integrity": "sha512-AIWHDDG24we1r8sWVO9Uae6V2ISXji2gIkZS3+CjtYowaBCpMTSu4QEQRnjQam2EWrEMVIJOXwBfx11TZKrxWA==", "license": "MIT" }, "node_modules/@embedpdf/plugin-annotation": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-1.5.0.tgz", - "integrity": "sha512-mxEPI6xYwOGaf9fYfoywuj6nwA10eHFPBuN066MzwphDk6DOHJGZ3Vq8zNQBXh20c/Lb25PL718D7MZWxZLUHg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-2.3.0.tgz", + "integrity": "sha512-TIN/OiDTg5tCNsebp1SWnS6aa7nnDvRrrZe3jx7Sg5IMEiZc6P3z+0aOjJtvoz0cp3Xi7Bb0PQsTLwo+bdfpVg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0", - "@embedpdf/utils": "1.5.0" + "@embedpdf/models": "2.3.0", + "@embedpdf/utils": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", - "@embedpdf/plugin-history": "1.5.0", - "@embedpdf/plugin-interaction-manager": "1.5.0", - "@embedpdf/plugin-selection": "1.5.0", + "@embedpdf/core": "2.3.0", + "@embedpdf/plugin-history": "2.3.0", + "@embedpdf/plugin-interaction-manager": "2.3.0", + "@embedpdf/plugin-selection": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-bookmark": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-bookmark/-/plugin-bookmark-1.5.0.tgz", - "integrity": "sha512-s3C9PtVesy5X8Ds/C9TEElFiqfKGRklG/uNPTROpNoolfpi0h7qX2xqqh/9+FzKH2nHjVcPB7Pp432v16h7eRA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-bookmark/-/plugin-bookmark-2.3.0.tgz", + "integrity": "sha512-7XO2NntgRb/Jk1XN/EOf7+yVaOPVVFvBuF0xlCqnz2BGAnMNrTn8QE73FtluJBgNhuK9LwDT2C4W+BTD2gd59Q==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0" + "@embedpdf/models": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", + "@embedpdf/core": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", + "vue": ">=3.2.0" + } + }, + "node_modules/@embedpdf/plugin-document-manager": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-document-manager/-/plugin-document-manager-2.3.0.tgz", + "integrity": "sha512-hdKaWU1sjlLgXo2iWF4N734lklCfSO5Tj1xqk+0omxOpnVL1Ed5fzFO2N584pMkfFn1xo9Y2JPHSUtCdzF7/EQ==", + "license": "MIT", + "dependencies": { + "@embedpdf/models": "2.3.0" + }, + "peerDependencies": { + "@embedpdf/core": "2.3.0", + "preact": "^10.26.4", + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-export": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-1.5.0.tgz", - "integrity": "sha512-luk68mNW9l2X31qk4b02phKaqDl9aDXUAgHVz1EWrgwXQ3Oz9WEdu60utYARYDiepDo3Caadll8RwctYSf/anA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-2.3.0.tgz", + "integrity": "sha512-Xa048lKnc1jehWbaWv5qER1RVIHhHqt+JhgzAlqFSURXmzowbUzVEDBZ7fYImXRkpqp+ZeyBhWfZ60DBNE55Cw==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0" + "@embedpdf/models": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", + "@embedpdf/core": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -651,31 +720,15 @@ } }, "node_modules/@embedpdf/plugin-history": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.5.0.tgz", - "integrity": "sha512-p7PTNNaIr4gH3jLwX+eLJe1DeUXgi21kVGN6SRx/pocH8esg4jqoOeD/YiRRZoZnPOiy0jBXVhkPkwSmY7a2hQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-2.3.0.tgz", + "integrity": "sha512-+fr/kjK2Z9BiC53IMlUZvWjkD6iilcI3XCUKQPXRgS5MDAuwpVlgdAtc+3VAMlG3IddElxVFdvvxRO9R89k5Mg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0" + "@embedpdf/models": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", - "preact": "^10.26.4", - "react": ">=16.8.0", - "react-dom": ">=16.8.0", - "vue": ">=3.2.0" - } - }, - "node_modules/@embedpdf/plugin-interaction-manager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.5.0.tgz", - "integrity": "sha512-ckHgTfvkW6c5Ta7Mc+Dl9C2foVnvEpqEJ84wyBnqrU0OWbe/jsiPhyKBVeartMGqNI/kVfaQTXupyrKhekAVmg==", - "license": "MIT", - "dependencies": { - "@embedpdf/models": "1.5.0" - }, - "peerDependencies": { - "@embedpdf/core": "1.5.0", + "@embedpdf/core": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -683,16 +736,16 @@ "vue": ">=3.2.0" } }, - "node_modules/@embedpdf/plugin-loader": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.5.0.tgz", - "integrity": "sha512-P4YpIZfaW69etYIjphyaL4cGl2pB14h3OdTE0tRQ2pZYZHFLTvlt4q9B3PVSdhlSrHK5nob7jfLGon2U7xCslg==", + "node_modules/@embedpdf/plugin-interaction-manager": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-2.3.0.tgz", + "integrity": "sha512-1/tDLPoQm6skNe/WOd6QD7SA0XRKphbJHi/s9XY4fhGgBvlD5XHFrYxtmrsaheYjqIBFtAWWZ3m5lAXRaO/igA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0" + "@embedpdf/models": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", + "@embedpdf/core": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -701,17 +754,17 @@ } }, "node_modules/@embedpdf/plugin-pan": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-1.5.0.tgz", - "integrity": "sha512-EMQ08dHqLkZmFVuLOO6h3AAinFPQoA1r6OlL9z+p0sswq31JAgd4X7+xjYIpI01z/V3+cTzPHzp7qwob5E4tbA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-2.3.0.tgz", + "integrity": "sha512-5yGxLpn28PHKCYx3tjzeVir7D5vHZ0Fk9HJRJr4K+Uqbg8pYFavb9tseXzPE4FcqpejqZo2DZyfo54ErQFXEyQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0" + "@embedpdf/models": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", - "@embedpdf/plugin-interaction-manager": "1.5.0", - "@embedpdf/plugin-viewport": "1.5.0", + "@embedpdf/core": "2.3.0", + "@embedpdf/plugin-interaction-manager": "2.3.0", + "@embedpdf/plugin-viewport": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -720,15 +773,15 @@ } }, "node_modules/@embedpdf/plugin-print": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-print/-/plugin-print-1.5.0.tgz", - "integrity": "sha512-rjorvNxAZfO9X4cFZVU9fHnldMWqMceJGmr3mH+yj7KdHePvNDDP+omyZyZKtxlUZENaeDI2h6k5z0GbhBz6sQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-print/-/plugin-print-2.3.0.tgz", + "integrity": "sha512-LNxvXm3rZkRXXC41IArBDiwPLzSflmBmxxi+L+91xvw8n/FWUeXfWwQn7oQEAGq9Ha/3pEVHTls48QSFZN0mhg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0" + "@embedpdf/models": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", + "@embedpdf/core": "2.3.0", "preact": "^10.26.4", "react": ">=18.0.0", "react-dom": ">=18.0.0", @@ -737,34 +790,35 @@ } }, "node_modules/@embedpdf/plugin-redaction": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-1.5.0.tgz", - "integrity": "sha512-txiukr5UKAGvJzl6dVBmmIT1v3r/t4e2qYm1hqU2faGgNCa2dwk79x9mDBlvWwxlJXCDFuFE+7Ps9/nU6qmU2w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-redaction/-/plugin-redaction-2.3.0.tgz", + "integrity": "sha512-un6AQL5Pqcm9v1tCV9Mb3NeowsGUtlCT/198k4nd+SWOMWNsbuFqI+rWOGV3auqXRGSzKj0gnt29t8aaeLpLeA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0", - "@embedpdf/utils": "1.5.0" + "@embedpdf/models": "2.3.0", + "@embedpdf/utils": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", - "@embedpdf/plugin-interaction-manager": "1.5.0", - "@embedpdf/plugin-selection": "1.5.0", + "@embedpdf/core": "2.3.0", + "@embedpdf/plugin-interaction-manager": "2.3.0", + "@embedpdf/plugin-selection": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-render": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.5.0.tgz", - "integrity": "sha512-ywwSj0ByrlkvrJIHKRzqxARkOZriki8VJUC+T4MV8fGyF4CzvCRJyKlPktahFz+VxhoodqTh7lBCib68dH+GvA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-2.3.0.tgz", + "integrity": "sha512-UyQncK5NTokuEVISUcxPOXpZP4SItn4MjfeEaPsTXJkSRjHL4g3mU3iWy0nXJMCOT10OB+5m7qQ0/KkF4f+b5w==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0" + "@embedpdf/models": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", + "@embedpdf/core": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -773,15 +827,15 @@ } }, "node_modules/@embedpdf/plugin-rotate": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-1.5.0.tgz", - "integrity": "sha512-5EmBCsq0VfrE3xWY6ofuVm8S6aK95EbAycRIk1wczcmTdvpsuXZ6P2ZaECUgYMcpZ6uAg4/kGf8X8VVZuCihSQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-2.3.0.tgz", + "integrity": "sha512-vibDXHA0L2LlMrmkSuanmdtUpc2JPBuQybiGwf9F4wlleKN3f7uSWxZsHdVAxWdzsaG+/26QTGl75otZLnVuig==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0" + "@embedpdf/models": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", + "@embedpdf/core": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -790,16 +844,16 @@ } }, "node_modules/@embedpdf/plugin-scroll": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.5.0.tgz", - "integrity": "sha512-RNmTZCZ8X1mA8cw9M7TMDuhO9GtkOalGha2bBL3En3D1IlDRS7PzNNMSMV7eqT7OQICSTltlpJ8p8Qi5esvL/Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-2.3.0.tgz", + "integrity": "sha512-8pdaSY9QuqdX22Ykw2jKn07Rx6FIsDdj/O0+mlbccY/ISofj9WEFNeQgnOY64OUTDyurJYqpYvq6QqvgbGLs+A==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0" + "@embedpdf/models": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", - "@embedpdf/plugin-viewport": "1.5.0", + "@embedpdf/core": "2.3.0", + "@embedpdf/plugin-viewport": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -808,16 +862,15 @@ } }, "node_modules/@embedpdf/plugin-search": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-1.5.0.tgz", - "integrity": "sha512-TB5b0H8Iobx/azVUBIlG2ClaKtf0y3/Xi3E/iB8BwvkIE2+g6EGfp8IMXIn8WDXST6bbvJEP31Ab0Ilp6SVkiw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-2.3.0.tgz", + "integrity": "sha512-VNXmNf7fIIRWGVwf2kIUeUeLkUTJlq9AGjUO2TyuYJTWTsmfT4LEqPDDpwC6NDVFhzWE6xwbb3bxvY/9bqBMzw==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0" + "@embedpdf/models": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", - "@embedpdf/plugin-loader": "1.5.0", + "@embedpdf/core": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -826,17 +879,17 @@ } }, "node_modules/@embedpdf/plugin-selection": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.5.0.tgz", - "integrity": "sha512-zrxLBAZQoPswDuf9q9DrYaQc6B0Ysc2U1hueTjNH/4+ydfl0BFXZkKR63C2e3YmWtXvKjkoIj0GyPzsiBORLUw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-2.3.0.tgz", + "integrity": "sha512-+emaY4vff3ynAf5C3PfCOlleQIqiImbBpb6zkG5SVUa9Vn5x0SfYGT4Jumtbzq8XBknC1QIRKVlplC9BcnjcmQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0" + "@embedpdf/models": "2.3.0", + "@embedpdf/utils": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", - "@embedpdf/plugin-interaction-manager": "1.5.0", - "@embedpdf/plugin-viewport": "1.5.0", + "@embedpdf/core": "2.3.0", + "@embedpdf/plugin-interaction-manager": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -845,16 +898,15 @@ } }, "node_modules/@embedpdf/plugin-spread": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-1.5.0.tgz", - "integrity": "sha512-3EU5Cp+fPQSiMjvMR/P2kXxXry/RlnxHLs4JeskAaH95QcqWW3VD+DrHkWSiLFkdhI18rNNGNlMc5RvDGvbXGQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-2.3.0.tgz", + "integrity": "sha512-sFqYKwzKGPaCXn6hAyv6GHdVTlL2vg3poxRNd2W5kLQo07YtHlSjXr/XAhaGT/a4GtR9rtbSJ4hWNJjzIcwE0g==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0" + "@embedpdf/models": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", - "@embedpdf/plugin-loader": "1.5.0", + "@embedpdf/core": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -863,16 +915,16 @@ } }, "node_modules/@embedpdf/plugin-thumbnail": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-1.5.0.tgz", - "integrity": "sha512-Z2qpyyr5s2M6460KDGu1Vk6rdbQFIoCpnyFAT6e7UaTIKkqJSNpmjqMsBU5PosYCFu/cClpHPvS7tg9/IKAk6g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-2.3.0.tgz", + "integrity": "sha512-CAOnipeBtdKSHGBuIm5420GykUw7k2rB7Z9GwouTbbycS7Cw+kiaGpOfHfenoKPTlWMkHYAwFcZiWKV3XG/nRQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0" + "@embedpdf/models": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", - "@embedpdf/plugin-render": "1.5.0", + "@embedpdf/core": "2.3.0", + "@embedpdf/plugin-render": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -881,18 +933,18 @@ } }, "node_modules/@embedpdf/plugin-tiling": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.5.0.tgz", - "integrity": "sha512-0Vx9elHNpMM+zv8hEoZXBEm8Q0+4kU52LxOlTYRr1A5FskF836sUct6g1ngwK1bmfbAfpz+62PnYI2EeilDZig==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-2.3.0.tgz", + "integrity": "sha512-6VJ042WksIyZVWyvXq1nf0Ct+U4Pl6+QUDy1ThJefwk/HKDfWU2zEr/+1STJKVWgfUx5QRdipf6Jghd+HnOg3Q==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0" + "@embedpdf/models": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", - "@embedpdf/plugin-render": "1.5.0", - "@embedpdf/plugin-scroll": "1.5.0", - "@embedpdf/plugin-viewport": "1.5.0", + "@embedpdf/core": "2.3.0", + "@embedpdf/plugin-render": "2.3.0", + "@embedpdf/plugin-scroll": "2.3.0", + "@embedpdf/plugin-viewport": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -901,15 +953,15 @@ } }, "node_modules/@embedpdf/plugin-viewport": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.5.0.tgz", - "integrity": "sha512-G8GDyYRhfehw72+r4qKkydnA5+AU8qH67g01Y12b0DzI0VIzymh/05Z4dK8DsY3jyWPXJfw2hlg5+KDHaMBHgQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-2.3.0.tgz", + "integrity": "sha512-3NQp3hVfRF7DMUPNAVOfZsqQQrugEfY0voRUrQI90eyi16GFntN3CP9Mc5cOp2jnUICMYlirQ/om+KCseMHS2Q==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0" + "@embedpdf/models": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", + "@embedpdf/core": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -918,19 +970,17 @@ } }, "node_modules/@embedpdf/plugin-zoom": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-1.5.0.tgz", - "integrity": "sha512-LiDkCd5/IXg2CRORl1Yikan2op+AYXSxhHzCFatyBdwzVj+n4y9I74OwCI62Mar8WDAIMyXZDCQxGPToSm+zDw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-2.3.0.tgz", + "integrity": "sha512-wnBqK02ku0zCViqQfSD1Vohy+aBUogXrqUTwo1/1QFEphmgnCHnHbEUduh9M0ghcT4s26pBgbJqCrShpGYAdvQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.5.0", - "hammerjs": "^2.0.8" + "@embedpdf/models": "2.3.0" }, "peerDependencies": { - "@embedpdf/core": "1.5.0", - "@embedpdf/plugin-interaction-manager": "1.5.0", - "@embedpdf/plugin-scroll": "1.5.0", - "@embedpdf/plugin-viewport": "1.5.0", + "@embedpdf/core": "2.3.0", + "@embedpdf/plugin-scroll": "2.3.0", + "@embedpdf/plugin-viewport": "2.3.0", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -939,14 +989,15 @@ } }, "node_modules/@embedpdf/utils": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-1.5.0.tgz", - "integrity": "sha512-L6jsAPQPGM8ne+MMFAd5gqXb1RNEgNyh16VvVUVKcVnJlBhwil59nVeEQ0cwPhjF5qVeY6MQDIOjBzJqkgXOYg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-2.3.0.tgz", + "integrity": "sha512-9DV+tu+GsnijchNSG/NzslnxTGIUH6j2MxBR8QOoZLsWETEVaMLkHtbvzXPyMOx/5RlvBn8wR0jNKTNptOCnXQ==", "license": "MIT", "peerDependencies": { "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, @@ -8929,15 +8980,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/hammerjs": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", - "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index f0d9e05bf..d90c0cfd0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,27 +7,28 @@ "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.7.7", "@dnd-kit/core": "^6.3.1", - "@embedpdf/core": "^1.5.0", - "@embedpdf/engines": "^1.5.0", - "@embedpdf/plugin-annotation": "^1.5.0", - "@embedpdf/plugin-bookmark": "^1.5.0", - "@embedpdf/plugin-export": "^1.5.0", - "@embedpdf/plugin-history": "^1.5.0", - "@embedpdf/plugin-interaction-manager": "^1.5.0", - "@embedpdf/plugin-loader": "^1.5.0", - "@embedpdf/plugin-pan": "^1.5.0", - "@embedpdf/plugin-print": "^1.5.0", - "@embedpdf/plugin-redaction": "^1.5.0", - "@embedpdf/plugin-render": "^1.5.0", - "@embedpdf/plugin-rotate": "^1.5.0", - "@embedpdf/plugin-scroll": "^1.5.0", - "@embedpdf/plugin-search": "^1.5.0", - "@embedpdf/plugin-selection": "^1.5.0", - "@embedpdf/plugin-spread": "^1.5.0", - "@embedpdf/plugin-thumbnail": "^1.5.0", - "@embedpdf/plugin-tiling": "^1.5.0", - "@embedpdf/plugin-viewport": "^1.5.0", - "@embedpdf/plugin-zoom": "^1.5.0", + "@embedpdf/core": "^2.3.0", + "@embedpdf/engines": "^2.3.0", + "@embedpdf/models": "^2.3.0", + "@embedpdf/plugin-annotation": "^2.3.0", + "@embedpdf/plugin-bookmark": "^2.3.0", + "@embedpdf/plugin-export": "^2.3.0", + "@embedpdf/plugin-history": "^2.3.0", + "@embedpdf/plugin-document-manager": "^2.3.0", + "@embedpdf/plugin-interaction-manager": "^2.3.0", + "@embedpdf/plugin-pan": "^2.3.0", + "@embedpdf/plugin-print": "^2.3.0", + "@embedpdf/plugin-redaction": "^2.3.0", + "@embedpdf/plugin-render": "^2.3.0", + "@embedpdf/plugin-rotate": "^2.3.0", + "@embedpdf/plugin-scroll": "^2.3.0", + "@embedpdf/plugin-search": "^2.3.0", + "@embedpdf/plugin-selection": "^2.3.0", + "@embedpdf/plugin-spread": "^2.3.0", + "@embedpdf/plugin-thumbnail": "^2.3.0", + "@embedpdf/plugin-tiling": "^2.3.0", + "@embedpdf/plugin-viewport": "^2.3.0", + "@embedpdf/plugin-zoom": "^2.3.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@iconify/react": "^6.0.2", diff --git a/frontend/src/core/components/viewer/ActiveDocumentContext.tsx b/frontend/src/core/components/viewer/ActiveDocumentContext.tsx new file mode 100644 index 000000000..663f6e6d4 --- /dev/null +++ b/frontend/src/core/components/viewer/ActiveDocumentContext.tsx @@ -0,0 +1,64 @@ +import React, { createContext, useContext, useState, useEffect, useRef } from 'react'; +import { useDocumentManagerPlugin } from '@embedpdf/plugin-document-manager/react'; + +interface ActiveDocumentContextType { + documentId: string | null; +} + +const ActiveDocumentContext = createContext({ documentId: null }); + +export function ActiveDocumentProvider({ children }: { children: React.ReactNode }) { + const { plugin, isLoading } = useDocumentManagerPlugin(); + const [documentId, setDocumentId] = useState(null); + const unsubscribeRef = useRef<(() => void) | null>(null); + const documentIdRef = useRef(null); + + useEffect(() => { + if (isLoading || !plugin) return; + + const docManagerApi = plugin.provides?.(); + if (!docManagerApi) return; + + // Get initial active document (synchronously if available) + const activeDoc = docManagerApi.getActiveDocument?.(); + if (activeDoc?.id && activeDoc.id !== documentIdRef.current) { + documentIdRef.current = activeDoc.id; + setDocumentId(activeDoc.id); + } + + // Subscribe to document changes (only if not already subscribed) + if (!unsubscribeRef.current && docManagerApi.onDocumentOpened) { + unsubscribeRef.current = docManagerApi.onDocumentOpened((event: any) => { + const docId = event?.documentId || event?.id || event?.document?.id; + if (docId && docId !== documentIdRef.current) { + documentIdRef.current = docId; + setDocumentId(docId); + } + }); + } + + // Note: We don't unsubscribe on effect cleanup to avoid re-subscribing on every render + // Cleanup happens only on unmount + }, [plugin, isLoading]); + + // Cleanup on unmount only + useEffect(() => { + return () => { + if (unsubscribeRef.current) { + unsubscribeRef.current(); + unsubscribeRef.current = null; + } + }; + }, []); + + return ( + + {children} + + ); +} + +export function useActiveDocument(): string | null { + const context = useContext(ActiveDocumentContext); + return context.documentId; +} diff --git a/frontend/src/core/components/viewer/AnnotationAPIBridge.tsx b/frontend/src/core/components/viewer/AnnotationAPIBridge.tsx index 676a583a2..2bc779fde 100644 --- a/frontend/src/core/components/viewer/AnnotationAPIBridge.tsx +++ b/frontend/src/core/components/viewer/AnnotationAPIBridge.tsx @@ -10,6 +10,7 @@ import type { } from '@app/components/viewer/viewerTypes'; type NoteIcon = NonNullable; + type AnnotationDefaults = | { type: @@ -17,13 +18,15 @@ type AnnotationDefaults = | PdfAnnotationSubtype.UNDERLINE | PdfAnnotationSubtype.STRIKEOUT | PdfAnnotationSubtype.SQUIGGLY; - color: string; + strokeColor: string; + color?: string; opacity: number; customData?: Record; } | { type: PdfAnnotationSubtype.INK; - color: string; + strokeColor: string; + color?: string; opacity?: number; borderWidth?: number; strokeWidth?: number; @@ -37,7 +40,8 @@ type AnnotationDefaults = fontFamily?: string; textAlign?: number; opacity?: number; - backgroundColor?: string; + color?: string; + interiorColor?: string; borderWidth?: number; contents?: string; icon?: PdfAnnotationIcon; @@ -106,7 +110,7 @@ const DEFAULTS = { ink: '#1f2933', inkHighlighter: '#ffd54f', text: '#111111', - note: '#ffd54f', // match highlight color + note: '#ffd54f', shapeFill: '#0000ff', shapeStroke: '#cf5b5b', shapeOpacity: 0.5, @@ -124,42 +128,62 @@ const buildStampDefaults: ToolDefaultsBuilder = (options) => ({ ...withCustomData(options), }); -const buildInkDefaults = (options?: AnnotationToolOptions, opacityOverride?: number): AnnotationDefaults => ({ - type: PdfAnnotationSubtype.INK, - color: options?.color ?? (opacityOverride ? DEFAULTS.inkHighlighter : DEFAULTS.ink), - opacity: options?.opacity ?? opacityOverride ?? 1, - borderWidth: options?.thickness ?? (opacityOverride ? 6 : 2), - strokeWidth: options?.thickness ?? (opacityOverride ? 6 : 2), - lineWidth: options?.thickness ?? (opacityOverride ? 6 : 2), - ...withCustomData(options), -}); +const buildInkDefaults = (options?: AnnotationToolOptions, opacityOverride?: number): AnnotationDefaults => { + const colorValue = options?.color ?? (opacityOverride ? DEFAULTS.inkHighlighter : DEFAULTS.ink); + return { + type: PdfAnnotationSubtype.INK, + strokeColor: colorValue, + color: colorValue, + opacity: options?.opacity ?? opacityOverride ?? 1, + borderWidth: options?.thickness ?? (opacityOverride ? 6 : 2), + strokeWidth: options?.thickness ?? (opacityOverride ? 6 : 2), + lineWidth: options?.thickness ?? (opacityOverride ? 6 : 2), + ...withCustomData(options), + }; +}; const TOOL_DEFAULT_BUILDERS: Record = { select: () => null, - highlight: (options) => ({ - type: PdfAnnotationSubtype.HIGHLIGHT, - color: options?.color ?? DEFAULTS.highlight, - opacity: options?.opacity ?? 0.6, - ...withCustomData(options), - }), - underline: (options) => ({ - type: PdfAnnotationSubtype.UNDERLINE, - color: options?.color ?? DEFAULTS.underline, - opacity: options?.opacity ?? 1, - ...withCustomData(options), - }), - strikeout: (options) => ({ - type: PdfAnnotationSubtype.STRIKEOUT, - color: options?.color ?? DEFAULTS.strikeout, - opacity: options?.opacity ?? 1, - ...withCustomData(options), - }), - squiggly: (options) => ({ - type: PdfAnnotationSubtype.SQUIGGLY, - color: options?.color ?? DEFAULTS.squiggly, - opacity: options?.opacity ?? 1, - ...withCustomData(options), - }), + highlight: (options) => { + const colorValue = options?.color ?? DEFAULTS.highlight; + return { + type: PdfAnnotationSubtype.HIGHLIGHT, + strokeColor: colorValue, + color: colorValue, + opacity: options?.opacity ?? 0.6, + ...withCustomData(options), + }; + }, + underline: (options) => { + const colorValue = options?.color ?? DEFAULTS.underline; + return { + type: PdfAnnotationSubtype.UNDERLINE, + strokeColor: colorValue, + color: colorValue, + opacity: options?.opacity ?? 1, + ...withCustomData(options), + }; + }, + strikeout: (options) => { + const colorValue = options?.color ?? DEFAULTS.strikeout; + return { + type: PdfAnnotationSubtype.STRIKEOUT, + strokeColor: colorValue, + color: colorValue, + opacity: options?.opacity ?? 1, + ...withCustomData(options), + }; + }, + squiggly: (options) => { + const colorValue = options?.color ?? DEFAULTS.squiggly; + return { + type: PdfAnnotationSubtype.SQUIGGLY, + strokeColor: colorValue, + color: colorValue, + opacity: options?.opacity ?? 1, + ...withCustomData(options), + }; + }, ink: (options) => buildInkDefaults(options), inkHighlighter: (options) => buildInkDefaults(options, options?.opacity ?? 0.6), text: (options) => ({ @@ -170,21 +194,21 @@ const TOOL_DEFAULT_BUILDERS: Record = { textAlign: options?.textAlign ?? 0, opacity: options?.opacity ?? 1, borderWidth: options?.thickness ?? 1, - ...(options?.fillColor ? { backgroundColor: options.fillColor } : {}), + ...(options?.fillColor ? { color: options.fillColor, interiorColor: options.fillColor } : {}), ...withCustomData(options), }), note: (options) => { - const backgroundColor = options?.fillColor ?? DEFAULTS.note; + const bgColor = options?.fillColor ?? DEFAULTS.note; const fontColor = options?.color ?? DEFAULTS.text; return { type: PdfAnnotationSubtype.FREETEXT, fontColor, - color: fontColor, fontFamily: options?.fontFamily ?? 'Helvetica', textAlign: options?.textAlign ?? 0, fontSize: options?.fontSize ?? 12, opacity: options?.opacity ?? 1, - backgroundColor, + color: bgColor, + interiorColor: bgColor, borderWidth: options?.thickness ?? 0, contents: options?.contents ?? 'Note', icon: getIconEnum(options?.icon), @@ -317,7 +341,11 @@ export const AnnotationAPIBridge = forwardRef(function Annotation // `this`/state dependency (e.g. reading `selectedUid` from undefined). // If that happens, fail gracefully and treat it as "no selection" // instead of crashing the entire annotations tool. - console.error('[AnnotationAPIBridge] getSelectedAnnotation failed:', error); + // Only log unexpected errors - "No active document" is a common expected state during init + const errorMessage = error instanceof Error ? error.message : String(error); + if (!errorMessage.includes('No active document')) { + console.error('[AnnotationAPIBridge] getSelectedAnnotation failed:', error); + } return null; } }, diff --git a/frontend/src/core/components/viewer/BookmarkAPIBridge.tsx b/frontend/src/core/components/viewer/BookmarkAPIBridge.tsx index 019b80920..10a012d04 100644 --- a/frontend/src/core/components/viewer/BookmarkAPIBridge.tsx +++ b/frontend/src/core/components/viewer/BookmarkAPIBridge.tsx @@ -1,6 +1,5 @@ import { useEffect, useMemo, useState, useCallback } from 'react'; -import { useBookmarkCapability } from '@embedpdf/plugin-bookmark/react'; -import { BookmarkCapability } from '@embedpdf/plugin-bookmark'; +import { useBookmarkCapability, BookmarkCapability } from '@embedpdf/plugin-bookmark/react'; import { useViewer } from '@app/contexts/ViewerContext'; import { BookmarkState, BookmarkAPIWrapper } from '@app/contexts/viewer/viewerBridges'; diff --git a/frontend/src/core/components/viewer/CustomSearchLayer.tsx b/frontend/src/core/components/viewer/CustomSearchLayer.tsx index 29f3b528c..7320e1bdb 100644 --- a/frontend/src/core/components/viewer/CustomSearchLayer.tsx +++ b/frontend/src/core/components/viewer/CustomSearchLayer.tsx @@ -1,11 +1,12 @@ import { useState, useEffect, useMemo } from 'react'; +import { useDocumentState } from '@embedpdf/core/react'; import { useSearch } from '@embedpdf/plugin-search/react'; -import { useViewer } from '@app/contexts/ViewerContext'; import { SEARCH_CONSTANTS } from '@app/components/viewer/constants/search'; interface SearchLayerProps { + documentId?: string; pageIndex: number; - scale: number; + scale?: number; highlightColor?: string; activeHighlightColor?: string; opacity?: number; @@ -25,18 +26,22 @@ interface SearchResultState { } export function CustomSearchLayer({ + documentId = '', pageIndex, - scale, + scale: scaleProp, highlightColor = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.BACKGROUND, activeHighlightColor = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.ACTIVE_BACKGROUND, opacity = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.OPACITY, padding = SEARCH_CONSTANTS.UI.HIGHLIGHT_PADDING, borderRadius = 4 }: SearchLayerProps) { - const { provides: searchProvides } = useSearch(); - const { scrollActions } = useViewer(); + const { provides: searchProvides } = useSearch(documentId); + const documentState = useDocumentState(documentId); const [searchResultState, setSearchResultState] = useState(null); + // Use document scale from EmbedPDF state, fallback to prop, then to 1 + const scale = documentState?.scale ?? scaleProp ?? 1; + // Subscribe to search result state changes useEffect(() => { if (!searchProvides) { @@ -45,16 +50,8 @@ export function CustomSearchLayer({ const unsubscribe = searchProvides.onSearchResultStateChange?.((state: SearchResultState) => { if (!state) return; - - // Auto-scroll to active search result - if (state.results && state.activeResultIndex !== undefined && state.activeResultIndex >= 0) { - const activeResult = state.results[state.activeResultIndex]; - if (activeResult) { - const pageNumber = activeResult.pageIndex + 1; // Convert to 1-based page number - scrollActions.scrollToPage(pageNumber); - } - } - + // Only update state - do NOT auto-scroll here + // Scrolling should only happen when user explicitly navigates via next/previous setSearchResultState(state); }); diff --git a/frontend/src/core/components/viewer/DocumentPermissionsAPIBridge.tsx b/frontend/src/core/components/viewer/DocumentPermissionsAPIBridge.tsx new file mode 100644 index 000000000..b8684eb2e --- /dev/null +++ b/frontend/src/core/components/viewer/DocumentPermissionsAPIBridge.tsx @@ -0,0 +1,63 @@ +import { useEffect, useMemo } from 'react'; +import { useViewer } from '@app/contexts/ViewerContext'; +import { + PdfPermissionFlag, + DocumentPermissionsState, + DocumentPermissionsAPIWrapper, +} from '@app/contexts/viewer/viewerBridges'; + +function hasPermissionFlag(permissions: number, flag: PdfPermissionFlag): boolean { + if (permissions === 0 || permissions === PdfPermissionFlag.AllowAll) { + return true; + } + return (permissions & flag) !== 0; +} + +interface DocumentPermissionsAPIBridgeProps { + isEncrypted?: boolean; + isOwnerUnlocked?: boolean; + permissions?: number; +} + +export function DocumentPermissionsAPIBridge({ + isEncrypted = false, + isOwnerUnlocked = false, + permissions = PdfPermissionFlag.AllowAll, +}: DocumentPermissionsAPIBridgeProps) { + const { registerBridge } = useViewer(); + + const state = useMemo(() => ({ + isEncrypted, + isOwnerUnlocked, + permissions, + canPrint: hasPermissionFlag(permissions, PdfPermissionFlag.Print), + canModifyContents: hasPermissionFlag(permissions, PdfPermissionFlag.ModifyContents), + canCopyContents: hasPermissionFlag(permissions, PdfPermissionFlag.CopyContents), + canModifyAnnotations: hasPermissionFlag(permissions, PdfPermissionFlag.ModifyAnnotations), + canFillForms: hasPermissionFlag(permissions, PdfPermissionFlag.FillForms), + canExtractForAccessibility: hasPermissionFlag(permissions, PdfPermissionFlag.ExtractForAccessibility), + canAssembleDocument: hasPermissionFlag(permissions, PdfPermissionFlag.AssembleDocument), + canPrintHighQuality: hasPermissionFlag(permissions, PdfPermissionFlag.PrintHighQuality), + }), [isEncrypted, isOwnerUnlocked, permissions]); + + const api = useMemo(() => ({ + hasPermission: (flag: PdfPermissionFlag) => hasPermissionFlag(permissions, flag), + hasAllPermissions: (flags: PdfPermissionFlag[]) => + flags.every(flag => hasPermissionFlag(permissions, flag)), + getEffectivePermission: (flag: PdfPermissionFlag) => { + if (isOwnerUnlocked) return true; + return hasPermissionFlag(permissions, flag); + }, + }), [permissions, isOwnerUnlocked]); + + useEffect(() => { + registerBridge('permissions', { + state, + api, + }); + }, [registerBridge, state, api]); + + return null; +} + +export { PdfPermissionFlag }; diff --git a/frontend/src/core/components/viewer/DocumentReadyWrapper.tsx b/frontend/src/core/components/viewer/DocumentReadyWrapper.tsx new file mode 100644 index 000000000..3330f36ce --- /dev/null +++ b/frontend/src/core/components/viewer/DocumentReadyWrapper.tsx @@ -0,0 +1,56 @@ +import React, { useState, useEffect } from 'react'; +import { useDocumentManagerPlugin } from '@embedpdf/plugin-document-manager/react'; + +interface DocumentReadyWrapperProps { + children: (documentId: string) => React.ReactNode; + fallback?: React.ReactNode; +} + +export function DocumentReadyWrapper({ children, fallback = null }: DocumentReadyWrapperProps) { + const { plugin, isLoading, ready } = useDocumentManagerPlugin(); + const [activeDocumentId, setActiveDocumentId] = useState(null); + + useEffect(() => { + if (isLoading || !plugin) return; + + const checkActiveDocument = async () => { + await ready; + + // Try to get the active document from the plugin's provides() + const docManagerApi = plugin.provides?.(); + if (docManagerApi) { + // Try different methods to get the active document + const activeDoc = docManagerApi.getActiveDocument?.(); + if (activeDoc?.id) { + setActiveDocumentId(activeDoc.id); + return; + } + } + }; + + checkActiveDocument(); + + // Subscribe to document changes + const docManagerApi = plugin.provides?.(); + if (docManagerApi?.onDocumentOpened) { + const unsubscribe = docManagerApi.onDocumentOpened((event: any) => { + const docId = event?.documentId || event?.id || event?.document?.id; + if (docId) { + setActiveDocumentId(docId); + } + }); + + return () => { + if (typeof unsubscribe === 'function') { + unsubscribe(); + } + }; + } + }, [plugin, isLoading, ready]); + + if (!activeDocumentId) { + return <>{fallback}; + } + + return <>{children(activeDocumentId)}; +} diff --git a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx index e67c2bab8..1d9434768 100644 --- a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx +++ b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx @@ -345,21 +345,6 @@ const EmbedPdfViewerContent = ({ // Always consider applied redactions as unsaved until export const hasAppliedRedactions = redactionsApplied; - // When in redact mode, still include annotation changes (users may draw) - if (isManualRedactMode) { - console.log('[Viewer] Checking for unsaved changes (redact mode):', { - hasPendingRedactions, - hasAppliedRedactions, - hasAnnotationChanges, - }); - } else { - console.log('[Viewer] Checking for unsaved changes:', { - hasAnnotationChanges, - hasPendingRedactions, - hasAppliedRedactions, - }); - } - return hasAnnotationChanges || hasPendingRedactions || hasAppliedRedactions; }; diff --git a/frontend/src/core/components/viewer/LinkLayer.tsx b/frontend/src/core/components/viewer/LinkLayer.tsx index 4062d96d7..d9a582b85 100644 --- a/frontend/src/core/components/viewer/LinkLayer.tsx +++ b/frontend/src/core/components/viewer/LinkLayer.tsx @@ -1,13 +1,17 @@ import React, { useEffect, useState, useMemo, useCallback } from 'react'; +import { useDocumentState } from '@embedpdf/core/react'; import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react'; import { useScroll } from '@embedpdf/plugin-scroll/react'; +import { PdfAnnotationSubtype } from '@embedpdf/models'; enum PDFActionType { GoTo = 0, GoToR = 1, GoToE = 2, URI = 3, - // Add other types as needed + Launch = 4, + Named = 5, + JavaScript = 6, } interface PDFRect { @@ -46,24 +50,242 @@ function isURIAction(action: PDFAction): boolean { return action.type === 'URI' || action.type === PDFActionType.URI; } -function isInternalLink(link: LinkAnnotation): boolean { +// Utility functions for link type detection - prefixed to indicate future use +function _isInternalLink(link: LinkAnnotation): boolean { return Boolean(link.target?.type === 'destination' || (link.target?.type === 'action' && link.target.action && isGoToAction(link.target.action))); } -function isExternalLink(link: LinkAnnotation): boolean { +function _isExternalLink(link: LinkAnnotation): boolean { return Boolean(link.target?.type === 'uri' || (link.target?.type === 'action' && link.target.action && isURIAction(link.target.action))); } interface LinkLayerProps { + documentId: string; pageIndex: number; - scale: number; - document?: any; - pdfFile?: File | Blob; - onLinkClick?: (target: any) => void; + pageWidth: number; + pageHeight: number; } +export const LinkLayer: React.FC = ({ + documentId, + pageIndex, + pageWidth, + pageHeight, +}) => { + const { provides: annotation } = useAnnotationCapability(); + const { provides: scroll } = useScroll(documentId); + const documentState = useDocumentState(documentId); + const [links, setLinks] = useState([]); + const [isNavigating, setIsNavigating] = useState(false); + + // Get original PDF page dimensions from document state + const pdfPage = documentState?.document?.pages?.[pageIndex]; + const pdfPageWidth = pdfPage?.size?.width ?? 0; + const pdfPageHeight = pdfPage?.size?.height ?? 0; + + // Process links with proper coordinate transformation + const processedLinks = useMemo(() => { + if (!pageWidth || !pageHeight || !pdfPageWidth || !pdfPageHeight) return []; + + // Use the document scale like EmbedPDF's AnnotationLayer does + const scale = documentState?.scale ?? 1; + + + + return links.map(link => { + const { origin, size } = link.rect; + + // Use document scale like EmbedPDF does + const scaledLeft = origin.x * scale; + const scaledTop = origin.y * scale; + const scaledWidth = size.width * scale; + const scaledHeight = size.height * scale; + + return { + ...link, + scaledRect: { + left: scaledLeft, + top: scaledTop, + width: scaledWidth, + height: scaledHeight, + }, + }; + }); + }, [links, pageWidth, pageHeight, pdfPageWidth, pdfPageHeight, documentState?.scale]); + + useEffect(() => { + const fetchLinks = async () => { + if (!annotation) return; + + try { + const pageAnnotationsResult = annotation.getPageAnnotations({ + pageIndex, + }); + + const resolveAnnotations = async (result: unknown): Promise => { + if (result && typeof (result as any).toPromise === 'function') { + return (result as any).toPromise(); + } + if (result && typeof (result as any).then === 'function') { + return result as Promise; + } + if (Array.isArray(result)) { + return result; + } + return []; + }; + + const pageAnnotations = await resolveAnnotations(pageAnnotationsResult); + + const linkAnnotations = pageAnnotations.filter( + (ann: any) => ann.type === 2 || ann.type === PdfAnnotationSubtype.LINK + ) as LinkAnnotation[]; + + if (linkAnnotations.length > 0) { + setLinks(linkAnnotations); + } + } catch (error) { + console.error('[LinkLayer] Failed to fetch links from annotation API:', { + error, + pageIndex, + }); + } + }; + + fetchLinks(); + }, [annotation, pageIndex]); + + const handleLinkClick = useCallback(async (link: LinkAnnotation) => { + if (isNavigating) return; + + try { + setIsNavigating(true); + + // Try to extract destination from various possible locations in the link object + const linkData = link as any; + + // Check for destination in various locations + let targetPage: number | undefined; + let uri: string | undefined; + + // Try target.destination first + if (link.target?.destination?.pageIndex !== undefined) { + targetPage = link.target.destination.pageIndex; + } + // Try target.action.destination + else if (link.target?.action?.destination?.pageIndex !== undefined) { + targetPage = link.target.action.destination.pageIndex; + } + // Try direct dest property (PDF.js style) + else if (linkData.dest?.pageIndex !== undefined) { + targetPage = linkData.dest.pageIndex; + } + // Try destination at root level + else if (linkData.destination?.pageIndex !== undefined) { + targetPage = linkData.destination.pageIndex; + } + // Try action at root level + else if (linkData.action?.destination?.pageIndex !== undefined) { + targetPage = linkData.action.destination.pageIndex; + } + + // Check for URI in various locations + if (link.target?.uri) { + uri = link.target.uri; + } else if (link.target?.action?.uri) { + uri = link.target.action.uri; + } else if (linkData.uri) { + uri = linkData.uri; + } else if (linkData.url) { + uri = linkData.url; + } else if (linkData.action?.uri) { + uri = linkData.action.uri; + } + + if (targetPage !== undefined && scroll) { + scroll.scrollToPage({ + pageNumber: targetPage + 1, + behavior: 'smooth', + }); + } else if (uri) { + try { + const url = new URL(uri, window.location.href); + if (['http:', 'https:', 'mailto:'].includes(url.protocol)) { + window.open(uri, '_blank', 'noopener,noreferrer'); + } else { + console.warn('[LinkLayer] Blocked potentially unsafe URL protocol:', url.protocol); + } + } catch { + window.open(uri, '_blank', 'noopener,noreferrer'); + } + } else { + console.warn('[LinkLayer] Could not extract destination or URI from link:', link); + } + } catch (error) { + console.error('[LinkLayer] Navigation failed:', error); + } finally { + setIsNavigating(false); + } + }, [isNavigating, scroll]); + + return ( +
+ {processedLinks.map((link) => { + const { id } = link; + const { left, top, width, height } = link.scaledRect; + + return ( + { + e.preventDefault(); + e.stopPropagation(); + handleLinkClick(link); + }} + onMouseDown={(e) => e.stopPropagation()} + className={`absolute block cursor-pointer ${isNavigating ? 'cursor-not-allowed' : ''}`} + style={{ + left: `${left}px`, + top: `${top}px`, + width: `${width}px`, + height: `${height}px`, + minWidth: '8px', + minHeight: '8px', + pointerEvents: 'auto', + backgroundColor: 'transparent', + zIndex: 11, + border: '2px solid transparent', + borderRadius: '2px', + transition: 'background-color 0.15s ease-in-out, border-color 0.15s ease-in-out', + }} + onMouseEnter={(e) => { + e.currentTarget.style.backgroundColor = 'rgba(59, 130, 246, 0.15)'; + e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.backgroundColor = 'transparent'; + e.currentTarget.style.borderColor = 'transparent'; + }} + title={getLinkTitle(link)} + aria-label={getLinkAriaLabel(link)} + > + {/* Invisible clickable area */} + + ); + })} +
+ ); +}; + const getLinkTitle = (link: LinkAnnotation): string => { if (link.target?.type === 'destination') { return `Go to page ${(link.target.destination?.pageIndex ?? 0) + 1}`; @@ -92,153 +314,3 @@ const getLinkAriaLabel = (link: LinkAnnotation): string => { } return 'Open external link'; }; - -export const LinkLayer: React.FC = ({ - pageIndex, - scale, - document: pdfDocument, - onLinkClick -}) => { - const { provides: annotation } = useAnnotationCapability(); - const { provides: scroll } = useScroll(); - const [links, setLinks] = useState([]); - const [isNavigating, setIsNavigating] = useState(false); - - const processedLinks = useMemo(() => { - return links.map(link => ({ - ...link, - scaledRect: { - left: link.rect.origin.x * scale, - top: link.rect.origin.y * scale, - width: link.rect.size.width * scale, - height: link.rect.size.height * scale, - } - })); - }, [links, scale]); - - useEffect(() => { - const fetchLinks = async () => { - if (!annotation) return; - - try { - // Use the annotation API's built-in filtering if available - const pageAnnotations = await annotation - .getPageAnnotations({ - pageIndex, - // Try to filter for link annotations (type 2) if the API supports it - ...(annotation.getPageAnnotations.length > 1 ? { types: [2] } : {}) - }) - .toPromise(); - - // Filter for link annotations (type 2 is LINK in PDF spec) as fallback - const linkAnnotations = pageAnnotations.filter( - (ann: any) => ann.type === 2 - ) as LinkAnnotation[]; - - if (linkAnnotations.length > 0) { - setLinks(linkAnnotations); - return; - } - } catch (error) { - console.error('[LinkLayer] Failed to fetch links from annotation API:', error); - } - - if (pdfDocument) { - try { - // Try different methods to get link annotations - let pdfLinks: any[] = []; - - if (pdfDocument.getPageAnnotations && typeof pdfDocument.getPageAnnotations === 'function') { - pdfLinks = await pdfDocument.getPageAnnotations(pageIndex); - } else if (pdfDocument.getAnnotations && typeof pdfDocument.getAnnotations === 'function') { - const allAnnotations = await pdfDocument.getAnnotations(); - pdfLinks = allAnnotations.filter((ann: any) => ann.pageIndex === pageIndex && ann.type === 2); - } else if (pdfDocument.pages && pdfDocument.pages[pageIndex]) { - const page = pdfDocument.pages[pageIndex]; - if (page.getAnnotations && typeof page.getAnnotations === 'function') { - pdfLinks = await page.getAnnotations(); - } - } - - const convertedLinks = pdfLinks.map((ann: any) => ({ - id: ann.id || `pdf-link-${pageIndex}-${Math.random()}`, - type: ann.type || 2, - rect: ann.rect || ann, - target: ann.target || ann.action - })) as LinkAnnotation[]; - - setLinks(convertedLinks); - } catch (error) { - console.warn('[LinkLayer] Failed to get annotations from PDF document:', error); - } - } else { - console.warn('[LinkLayer] No annotation API or PDF document available'); - } - }; - - fetchLinks(); - }, [annotation, pageIndex, pdfDocument]); - - const handleLinkClick = useCallback(async (link: LinkAnnotation) => { - if (isNavigating) return; // Prevent multiple simultaneous navigations - - try { - setIsNavigating(true); - - if (onLinkClick) { - onLinkClick(link.target); - return; - } - - if (isInternalLink(link)) { - const targetPage = link.target?.destination?.pageIndex ?? - link.target?.action?.destination?.pageIndex; - if (targetPage !== undefined && scroll) { - await scroll.scrollToPage({ - pageNumber: targetPage + 1, // PDF pages are 1-indexed - behavior: 'smooth', - }); - } - } else if (isExternalLink(link)) { - const uri = link.target?.uri ?? link.target?.action?.uri; - if (uri) { - window.open(uri, '_blank', 'noopener,noreferrer'); - } - } else { - throw new Error(`Unsupported link type: ${link.target?.type}`); - } - } catch (error) { - console.error('[LinkLayer] Navigation failed:', error); - } finally { - setIsNavigating(false); - } - }, [isNavigating, onLinkClick, scroll]); - - return ( -
- {processedLinks.map((link) => { - const { id } = link; - const { left, top, width, height } = link.scaledRect; - - return ( -
- ); -}; diff --git a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx index d022340a0..1007aecaf 100644 --- a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx +++ b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx @@ -6,8 +6,8 @@ import { PrivateContent } from '@app/components/shared/PrivateContent'; // Import the essential plugins import { Viewport, ViewportPluginPackage } from '@embedpdf/plugin-viewport/react'; -import { Scroller, ScrollPluginPackage, ScrollStrategy } from '@embedpdf/plugin-scroll/react'; -import { LoaderPluginPackage } from '@embedpdf/plugin-loader/react'; +import { Scroller, ScrollPluginPackage } from '@embedpdf/plugin-scroll/react'; +import { DocumentManagerPluginPackage } from '@embedpdf/plugin-document-manager/react'; import { RenderPluginPackage } from '@embedpdf/plugin-render/react'; import { ZoomPluginPackage, ZoomMode } from '@embedpdf/plugin-zoom/react'; import { InteractionManagerPluginPackage, PagePointerProvider, GlobalPointerProvider } from '@embedpdf/plugin-interaction-manager/react'; @@ -19,7 +19,7 @@ import { SearchPluginPackage } from '@embedpdf/plugin-search/react'; import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/react'; import { RotatePluginPackage, Rotate } from '@embedpdf/plugin-rotate/react'; import { ExportPluginPackage } from '@embedpdf/plugin-export/react'; -import { BookmarkPluginPackage } from '@embedpdf/plugin-bookmark'; +import { BookmarkPluginPackage } from '@embedpdf/plugin-bookmark/react'; import { PrintPluginPackage } from '@embedpdf/plugin-print/react'; import { HistoryPluginPackage } from '@embedpdf/plugin-history/react'; import { AnnotationLayer, AnnotationPluginPackage } from '@embedpdf/plugin-annotation/react'; @@ -49,8 +49,13 @@ import { LinkLayer } from '@app/components/viewer/LinkLayer'; import { RedactionSelectionMenu } from '@app/components/viewer/RedactionSelectionMenu'; import { RedactionPendingTracker, RedactionPendingTrackerAPI } from '@app/components/viewer/RedactionPendingTracker'; import { RedactionAPIBridge } from '@app/components/viewer/RedactionAPIBridge'; +import { DocumentPermissionsAPIBridge } from '@app/components/viewer/DocumentPermissionsAPIBridge'; +import { DocumentReadyWrapper } from '@app/components/viewer/DocumentReadyWrapper'; +import { ActiveDocumentProvider } from '@app/components/viewer/ActiveDocumentContext'; import { absoluteWithBasePath } from '@app/constants/app'; +const DOCUMENT_NAME = 'stirling-pdf-viewer'; + interface LocalEmbedPDFProps { file?: File | Blob; url?: string | null; @@ -90,22 +95,16 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda const viewportGap = rootFontSize * 3.5; return [ - createPluginRegistration(LoaderPluginPackage, { - loadingOptions: { - type: 'url', - pdfFile: { - id: 'stirling-pdf-viewer', - url: pdfUrl, - }, - }, + createPluginRegistration(DocumentManagerPluginPackage, { + initialDocuments: [{ + url: pdfUrl, + name: DOCUMENT_NAME, + }], }), createPluginRegistration(ViewportPluginPackage, { viewportGap, }), - createPluginRegistration(ScrollPluginPackage, { - strategy: ScrollStrategy.Vertical, - initialPage: 0, - }), + createPluginRegistration(ScrollPluginPackage), createPluginRegistration(RenderPluginPackage, { withForms: true, withAnnotations: showBakedAnnotations && !enableAnnotations, // Show baked annotations only when: visibility is ON and annotation layer is OFF @@ -257,7 +256,8 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda { + onInitialized={async (registry: any) => { + // v2.0: Use registry.getPlugin() to access plugin APIs const annotationPlugin = registry.getPlugin('annotation'); if (!annotationPlugin || !annotationPlugin.provides) return; @@ -279,6 +279,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.HIGHLIGHT ? 10 : 0), defaults: { type: PdfAnnotationSubtype.HIGHLIGHT, + strokeColor: '#ffd54f', color: '#ffd54f', opacity: 0.6, }, @@ -295,6 +296,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.UNDERLINE ? 10 : 0), defaults: { type: PdfAnnotationSubtype.UNDERLINE, + strokeColor: '#ffb300', color: '#ffb300', opacity: 1, }, @@ -311,6 +313,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.STRIKEOUT ? 10 : 0), defaults: { type: PdfAnnotationSubtype.STRIKEOUT, + strokeColor: '#e53935', color: '#e53935', opacity: 1, }, @@ -327,6 +330,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.SQUIGGLY ? 10 : 0), defaults: { type: PdfAnnotationSubtype.SQUIGGLY, + strokeColor: '#00acc1', color: '#00acc1', opacity: 1, }, @@ -343,6 +347,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.INK ? 10 : 0), defaults: { type: PdfAnnotationSubtype.INK, + strokeColor: '#1f2933', color: '#1f2933', opacity: 1, borderWidth: 2, @@ -359,9 +364,10 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda id: 'inkHighlighter', name: 'Ink Highlighter', interaction: { exclusive: true, cursor: 'crosshair' }, - matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.INK && annotation.color === '#ffd54f' ? 8 : 0), + matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.INK && (annotation.strokeColor === '#ffd54f' || annotation.color === '#ffd54f') ? 8 : 0), defaults: { type: PdfAnnotationSubtype.INK, + strokeColor: '#ffd54f', color: '#ffd54f', opacity: 0.5, borderWidth: 6, @@ -590,6 +596,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda matchScore: () => 0, defaults: { type: PdfAnnotationSubtype.INK, + strokeColor: '#000000', color: '#000000', opacity: 1.0, borderWidth: 2, @@ -622,6 +629,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda } }} > + @@ -641,87 +649,98 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda - - - { - return ( - - -
e.preventDefault()} - onDrop={(e) => e.preventDefault()} - onDragOver={(e) => e.preventDefault()} - > - {/* High-resolution tile layer */} - + + + + + } + > + {(documentId) => ( + + + { + return ( + + +
e.preventDefault()} + onDrop={(e) => e.preventDefault()} + onDragOver={(e) => e.preventDefault()} + > + - {/* Search highlight layer */} - + - {/* Selection layer for text interaction */} - + - {/* Link layer for clickable PDF links */} - + {/* AnnotationLayer for annotation editing (only when enabled) */} + {enableAnnotations && ( + + )} - {/* Annotation layer for signatures (only when enabled) */} - {enableAnnotations && ( - - )} + {enableRedaction && ( + } + /> + )} - {/* Redaction layer for marking areas to redact (only when enabled) */} - {enableRedaction && ( - } - /> - )} -
-
-
- ); - }} - /> -
-
+ {/* LinkLayer on top to handle link navigation - must be last for click priority */} + +
+
+
+ ); + }} + /> +
+
+ )} + +
diff --git a/frontend/src/core/components/viewer/LocalEmbedPDFWithAnnotations.tsx b/frontend/src/core/components/viewer/LocalEmbedPDFWithAnnotations.tsx index cfd48185d..ac7b58ac7 100644 --- a/frontend/src/core/components/viewer/LocalEmbedPDFWithAnnotations.tsx +++ b/frontend/src/core/components/viewer/LocalEmbedPDFWithAnnotations.tsx @@ -5,8 +5,8 @@ import { usePdfiumEngine } from '@embedpdf/engines/react'; // Import the essential plugins import { Viewport, ViewportPluginPackage } from '@embedpdf/plugin-viewport/react'; -import { Scroller, ScrollPluginPackage, ScrollStrategy } from '@embedpdf/plugin-scroll/react'; -import { LoaderPluginPackage } from '@embedpdf/plugin-loader/react'; +import { Scroller, ScrollPluginPackage } from '@embedpdf/plugin-scroll/react'; +import { DocumentManagerPluginPackage } from '@embedpdf/plugin-document-manager/react'; import { RenderPluginPackage } from '@embedpdf/plugin-render/react'; import { ZoomPluginPackage } from '@embedpdf/plugin-zoom/react'; import { InteractionManagerPluginPackage, PagePointerProvider, GlobalPointerProvider } from '@embedpdf/plugin-interaction-manager/react'; @@ -17,12 +17,11 @@ import { SpreadPluginPackage, SpreadMode } from '@embedpdf/plugin-spread/react'; import { SearchPluginPackage } from '@embedpdf/plugin-search/react'; import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/react'; import { RotatePluginPackage, Rotate } from '@embedpdf/plugin-rotate/react'; -import { Rotation } from '@embedpdf/models'; +import { Rotation, PdfAnnotationSubtype } from '@embedpdf/models'; // Import annotation plugins import { HistoryPluginPackage } from '@embedpdf/plugin-history/react'; import { AnnotationLayer, AnnotationPluginPackage } from '@embedpdf/plugin-annotation/react'; -import { PdfAnnotationSubtype } from '@embedpdf/models'; import { CustomSearchLayer } from '@app/components/viewer/CustomSearchLayer'; import { ZoomAPIBridge } from '@app/components/viewer/ZoomAPIBridge'; @@ -35,6 +34,9 @@ import { SpreadAPIBridge } from '@app/components/viewer/SpreadAPIBridge'; import { SearchAPIBridge } from '@app/components/viewer/SearchAPIBridge'; import { ThumbnailAPIBridge } from '@app/components/viewer/ThumbnailAPIBridge'; import { RotateAPIBridge } from '@app/components/viewer/RotateAPIBridge'; +import { DocumentReadyWrapper } from '@app/components/viewer/DocumentReadyWrapper'; + +const DOCUMENT_NAME = 'stirling-pdf-signing-viewer'; interface LocalEmbedPDFWithAnnotationsProps { file?: File | Blob; @@ -69,22 +71,16 @@ export function LocalEmbedPDFWithAnnotations({ const viewportGap = rootFontSize * 3.5; return [ - createPluginRegistration(LoaderPluginPackage, { - loadingOptions: { - type: 'url', - pdfFile: { - id: 'stirling-pdf-signing-viewer', - url: pdfUrl, - }, - }, + createPluginRegistration(DocumentManagerPluginPackage, { + initialDocuments: [{ + url: pdfUrl, + name: DOCUMENT_NAME, + }], }), createPluginRegistration(ViewportPluginPackage, { viewportGap, }), - createPluginRegistration(ScrollPluginPackage, { - strategy: ScrollStrategy.Vertical, - initialPage: 0, - }), + createPluginRegistration(ScrollPluginPackage), createPluginRegistration(RenderPluginPackage, { withForms: true, withAnnotations: true, @@ -191,7 +187,8 @@ export function LocalEmbedPDFWithAnnotations({ { + onInitialized={async (registry: any) => { + // v2.0: Use registry.getPlugin() to access plugin APIs const annotationPlugin = registry.getPlugin('annotation'); if (!annotationPlugin || !annotationPlugin.provides) return; @@ -244,78 +241,71 @@ export function LocalEmbedPDFWithAnnotations({ - - - ( - - -
e.preventDefault()} - onDrop={(e) => e.preventDefault()} - onDragOver={(e) => e.preventDefault()} - > - {/* High-resolution tile layer */} - + + + + } + > + {(documentId) => ( + + + ( + + +
e.preventDefault()} + onDrop={(e) => e.preventDefault()} + onDragOver={(e) => e.preventDefault()} + > + - {/* Search highlight layer */} - + - {/* Selection layer for text interaction */} - + - {/* Annotation layer for signatures */} - -
-
-
- )} - /> -
-
+ +
+
+
+ )} + /> +
+
+ )} +
); diff --git a/frontend/src/core/components/viewer/PanAPIBridge.tsx b/frontend/src/core/components/viewer/PanAPIBridge.tsx index 6d6936215..a09b01394 100644 --- a/frontend/src/core/components/viewer/PanAPIBridge.tsx +++ b/frontend/src/core/components/viewer/PanAPIBridge.tsx @@ -1,44 +1,60 @@ -import { useEffect, useState, useRef } from 'react'; +import { useEffect, useRef } from 'react'; import { usePan } from '@embedpdf/plugin-pan/react'; import { useViewer } from '@app/contexts/ViewerContext'; +import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId'; -/** - * Component that runs inside EmbedPDF context and updates pan state in ViewerContext - */ export function PanAPIBridge() { - const { provides: pan, isPanning } = usePan(); + const activeDocumentId = useActiveDocumentId(); + + // Don't render the inner component until we have a valid document ID + if (!activeDocumentId) { + return null; + } + + return ; +} + +function PanAPIBridgeInner({ documentId }: { documentId: string }) { + const { provides: pan, isPanning } = usePan(documentId); const { registerBridge, triggerImmediatePanUpdate } = useViewer(); - // Store state locally - const [_localState, setLocalState] = useState({ - isPanning: false - }); + // Keep pan ref updated to avoid re-running effect when object reference changes + const panRef = useRef(pan); + useEffect(() => { + panRef.current = pan; + }, [pan]); // Track previous isPanning value to detect changes const prevIsPanningRef = useRef(isPanning); useEffect(() => { - if (pan) { - // Update local state + const currentPan = panRef.current; + if (currentPan) { const newState = { isPanning }; - setLocalState(newState); // Register this bridge with ViewerContext registerBridge('pan', { state: newState, api: { enable: () => { - pan.enablePan(); + currentPan.enablePan(); }, disable: () => { - pan.disablePan(); + currentPan.disablePan(); }, toggle: () => { - pan.togglePan(); + currentPan.togglePan(); + }, + makePanDefault: () => { + // v2.3.0: makePanDefault may not exist, enable pan as fallback + if ('makePanDefault' in currentPan && typeof (currentPan as any).makePanDefault === 'function') { + (currentPan as any).makePanDefault(); + } else { + currentPan.enablePan(); + } }, - makePanDefault: () => pan.makePanDefault(), } }); @@ -48,7 +64,7 @@ export function PanAPIBridge() { triggerImmediatePanUpdate(isPanning); } } - }, [pan, isPanning, triggerImmediatePanUpdate]); + }, [isPanning, registerBridge, triggerImmediatePanUpdate]); return null; } diff --git a/frontend/src/core/components/viewer/RedactionAPIBridge.tsx b/frontend/src/core/components/viewer/RedactionAPIBridge.tsx index a5b9ce4c3..4220da84e 100644 --- a/frontend/src/core/components/viewer/RedactionAPIBridge.tsx +++ b/frontend/src/core/components/viewer/RedactionAPIBridge.tsx @@ -1,17 +1,21 @@ import { useEffect, useImperativeHandle } from 'react'; import { useRedaction as useEmbedPdfRedaction } from '@embedpdf/plugin-redaction/react'; import { useRedaction } from '@app/contexts/RedactionContext'; +import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId'; -/** - * RedactionAPIBridge connects the EmbedPDF redaction plugin to our RedactionContext. - * It must be rendered inside the EmbedPDF context to access the plugin API. - * - * It does two things: - * 1. Syncs EmbedPDF state (pendingCount, activeType, isRedacting) to our context - * 2. Exposes the EmbedPDF API through our context's ref so outside components can call it - */ export function RedactionAPIBridge() { - const { state, provides } = useEmbedPdfRedaction(); + const activeDocumentId = useActiveDocumentId(); + + // Don't render the inner component until we have a valid document ID + if (!activeDocumentId) { + return null; + } + + return ; +} + +function RedactionAPIBridgeInner({ documentId }: { documentId: string }) { + const { state, provides } = useEmbedPdfRedaction(documentId); const { redactionApiRef, setPendingCount, diff --git a/frontend/src/core/components/viewer/RedactionPendingTracker.tsx b/frontend/src/core/components/viewer/RedactionPendingTracker.tsx index 2865b49a6..f5cf3b4f7 100644 --- a/frontend/src/core/components/viewer/RedactionPendingTracker.tsx +++ b/frontend/src/core/components/viewer/RedactionPendingTracker.tsx @@ -1,22 +1,28 @@ import { useEffect, useRef, useImperativeHandle, forwardRef } from 'react'; import { useRedaction as useEmbedPdfRedaction } from '@embedpdf/plugin-redaction/react'; +import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId'; export interface RedactionPendingTrackerAPI { commitAllPending: () => void; getPendingCount: () => number; } -/** - * RedactionPendingTracker monitors pending redactions and exposes an API - * for committing and checking pending redactions. - * Must be rendered inside the EmbedPDF context. - * - * Note: The unsaved changes checker is registered by EmbedPdfViewer, not here, - * to avoid conflicts and allow the viewer to check both annotations and redactions. - */ export const RedactionPendingTracker = forwardRef( function RedactionPendingTracker(_, ref) { - const { state, provides } = useEmbedPdfRedaction(); + const activeDocumentId = useActiveDocumentId(); + + // Don't render the inner component until we have a valid document ID + if (!activeDocumentId) { + return null; + } + + return ; + } +); + +const RedactionPendingTrackerInner = forwardRef( + function RedactionPendingTrackerInner({ documentId }, ref) { + const { state, provides } = useEmbedPdfRedaction(documentId); const pendingCountRef = useRef(0); diff --git a/frontend/src/core/components/viewer/RedactionSelectionMenu.tsx b/frontend/src/core/components/viewer/RedactionSelectionMenu.tsx index d1e1c69fe..aa2043c04 100644 --- a/frontend/src/core/components/viewer/RedactionSelectionMenu.tsx +++ b/frontend/src/core/components/viewer/RedactionSelectionMenu.tsx @@ -1,4 +1,4 @@ -import { useRedaction as useEmbedPdfRedaction, SelectionMenuProps } from '@embedpdf/plugin-redaction/react'; +import { useRedaction as useEmbedPdfRedaction, RedactionSelectionMenuProps } from '@embedpdf/plugin-redaction/react'; import { ActionIcon, Tooltip, Button, Group } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { createPortal } from 'react-dom'; @@ -6,46 +6,63 @@ import { useEffect, useState, useRef, useCallback } from 'react'; import DeleteIcon from '@mui/icons-material/Delete'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import { useRedaction } from '@app/contexts/RedactionContext'; +import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId'; -/** - * Custom menu component that appears when a pending redaction mark is selected. - * Allows users to remove or apply individual pending marks. - * Uses a portal to ensure it appears above all content, including next pages. - */ -export function RedactionSelectionMenu({ item, selected, menuWrapperProps }: SelectionMenuProps) { - const { t } = useTranslation(); - const { provides } = useEmbedPdfRedaction(); - const { setRedactionsApplied } = useRedaction(); - const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null); - const wrapperRef = useRef(null); +// Use the official EmbedPDF v2.3.0 types +export type { RedactionSelectionMenuProps }; + +export function RedactionSelectionMenu(props: RedactionSelectionMenuProps) { + const activeDocumentId = useActiveDocumentId(); - // Merge refs if menuWrapperProps has a ref + // Don't render until we have a valid document ID + if (!activeDocumentId) { + return null; + } + + return ( + + ); +} + +function RedactionSelectionMenuInner({ + documentId, + context, + selected, + menuWrapperProps, +}: RedactionSelectionMenuProps & { documentId: string }) { + // Extract item and pageIndex from context (EmbedPDF v2.3.0 API) + const item = context?.item; + const pageIndex = context?.pageIndex; + const { t } = useTranslation(); + const { provides } = useEmbedPdfRedaction(documentId); + const { setRedactionsApplied } = useRedaction(); + const wrapperRef = useRef(null); + const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null); + + // Merge refs - menuWrapperProps.ref is a callback ref const setRef = useCallback((node: HTMLDivElement | null) => { wrapperRef.current = node; - if (menuWrapperProps?.ref) { - const ref = menuWrapperProps.ref; - if (typeof ref === 'function') { - ref(node); - } else if (ref && 'current' in ref) { - (ref as React.MutableRefObject).current = node; - } - } + // Call the EmbedPDF ref callback + menuWrapperProps?.ref?.(node); }, [menuWrapperProps]); const handleRemove = useCallback(() => { - if (provides?.removePending && item) { - provides.removePending(item.page, item.id); + if (provides?.removePending && item && pageIndex !== undefined) { + provides.removePending(pageIndex, item.id); } - }, [provides, item]); + }, [provides, item, pageIndex]); const handleApply = useCallback(() => { - if (provides?.commitPending && item) { - provides.commitPending(item.page, item.id); + if (provides?.commitPending && item && pageIndex !== undefined) { + provides.commitPending(pageIndex, item.id); // Mark redactions as applied (but not yet saved) so the Save Changes button stays enabled // This ensures the button doesn't become disabled when pendingCount decreases setRedactionsApplied(true); } - }, [provides, item, setRedactionsApplied]); + }, [provides, item, pageIndex, setRedactionsApplied]); // Calculate position for portal based on wrapper element useEffect(() => { @@ -61,13 +78,13 @@ export function RedactionSelectionMenu({ item, selected, menuWrapperProps }: Sel return; } - const rect = wrapper.getBoundingClientRect(); + const wrapperRect = wrapper.getBoundingClientRect(); // Position menu below the wrapper, centered // Use getBoundingClientRect which gives viewport-relative coordinates // Since we're using fixed positioning in the portal, we don't need to add scroll offsets setMenuPosition({ - top: rect.bottom + 8, - left: rect.left + rect.width / 2, + top: wrapperRect.bottom + 8, + left: wrapperRect.left + wrapperRect.width / 2, }); }; @@ -152,19 +169,16 @@ export function RedactionSelectionMenu({ item, selected, menuWrapperProps }: Sel ) : null; - // Extract ref from menuWrapperProps to avoid conflicts - const { ref: _, ...wrapperPropsWithoutRef } = menuWrapperProps || {}; - return ( <> + {/* Invisible wrapper that provides positioning - uses EmbedPDF's menuWrapperProps */}
@@ -174,4 +188,3 @@ export function RedactionSelectionMenu({ item, selected, menuWrapperProps }: Sel ); } - diff --git a/frontend/src/core/components/viewer/RotateAPIBridge.tsx b/frontend/src/core/components/viewer/RotateAPIBridge.tsx index 5b12590d7..b746d5c59 100644 --- a/frontend/src/core/components/viewer/RotateAPIBridge.tsx +++ b/frontend/src/core/components/viewer/RotateAPIBridge.tsx @@ -1,39 +1,48 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef } from 'react'; import { useRotate } from '@embedpdf/plugin-rotate/react'; import { useViewer } from '@app/contexts/ViewerContext'; +import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId'; -/** - * Component that runs inside EmbedPDF context and updates rotation state in ViewerContext - */ export function RotateAPIBridge() { - const { provides: rotate, rotation } = useRotate(); - const { registerBridge } = useViewer(); + const activeDocumentId = useActiveDocumentId(); - // Store state locally - const [_localState, setLocalState] = useState({ - rotation: 0 - }); + // Don't render the inner component until we have a valid document ID + if (!activeDocumentId) { + return null; + } + + return ; +} + +function RotateAPIBridgeInner({ documentId }: { documentId: string }) { + const { provides: rotate, rotation } = useRotate(documentId); + const { registerBridge } = useViewer(); + + // Keep rotate ref updated to avoid re-running effect when object reference changes + const rotateRef = useRef(rotate); + useEffect(() => { + rotateRef.current = rotate; + }, [rotate]); useEffect(() => { - if (rotate) { - // Update local state + const currentRotate = rotateRef.current; + if (currentRotate) { const newState = { rotation }; - setLocalState(newState); // Register this bridge with ViewerContext registerBridge('rotation', { state: newState, api: { - rotateForward: () => rotate.rotateForward(), - rotateBackward: () => rotate.rotateBackward(), - setRotation: (rotationValue: number) => rotate.setRotation(rotationValue), + rotateForward: () => currentRotate.rotateForward(), + rotateBackward: () => currentRotate.rotateBackward(), + setRotation: (rotationValue: number) => currentRotate.setRotation(rotationValue), getRotation: () => rotation, } }); } - }, [rotate, rotation]); + }, [rotation, registerBridge]); return null; } \ No newline at end of file diff --git a/frontend/src/core/components/viewer/ScrollAPIBridge.tsx b/frontend/src/core/components/viewer/ScrollAPIBridge.tsx index f918b3bea..41feb2b3e 100644 --- a/frontend/src/core/components/viewer/ScrollAPIBridge.tsx +++ b/frontend/src/core/components/viewer/ScrollAPIBridge.tsx @@ -1,20 +1,39 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useScroll } from '@embedpdf/plugin-scroll/react'; import { useViewer } from '@app/contexts/ViewerContext'; +import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId'; -/** - * ScrollAPIBridge manages scroll state and exposes scroll actions. - * Registers with ViewerContext to provide scroll functionality to UI components. - */ export function ScrollAPIBridge() { - const { provides: scroll, state: scrollState } = useScroll(); + const activeDocumentId = useActiveDocumentId(); + + // Don't render the inner component until we have a valid document ID + if (!activeDocumentId) { + return null; + } + + return ; +} + +function ScrollAPIBridgeInner({ documentId }: { documentId: string }) { + const { provides: scroll, state: scrollState } = useScroll(documentId); const { registerBridge, triggerImmediateScrollUpdate } = useViewer(); + // Keep scroll ref updated to avoid re-running effect when object reference changes + const scrollRef = useRef(scroll); useEffect(() => { - if (scroll && scrollState) { + scrollRef.current = scroll; + }, [scroll]); + + // Extract primitive values to avoid dependency on object references + const currentPage = scrollState?.currentPage; + const totalPages = scrollState?.totalPages; + + useEffect(() => { + const currentScroll = scrollRef.current; + if (currentScroll && currentPage !== undefined && totalPages !== undefined) { const newState = { - currentPage: scrollState.currentPage, - totalPages: scrollState.totalPages, + currentPage, + totalPages, }; // Trigger immediate update for responsive UI @@ -22,10 +41,10 @@ export function ScrollAPIBridge() { registerBridge('scroll', { state: newState, - api: scroll + api: currentScroll }); } - }, [scroll, scrollState]); + }, [currentPage, totalPages, registerBridge, triggerImmediateScrollUpdate]); return null; } diff --git a/frontend/src/core/components/viewer/SearchAPIBridge.tsx b/frontend/src/core/components/viewer/SearchAPIBridge.tsx index 4003e1d0b..fd198ac89 100644 --- a/frontend/src/core/components/viewer/SearchAPIBridge.tsx +++ b/frontend/src/core/components/viewer/SearchAPIBridge.tsx @@ -1,6 +1,7 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; import { useSearch } from '@embedpdf/plugin-search/react'; import { useViewer } from '@app/contexts/ViewerContext'; +import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId'; interface SearchResult { pageIndex: number; @@ -10,13 +11,29 @@ interface SearchResult { }>; } -/** - * SearchAPIBridge manages search state and provides search functionality. - * Listens for search result changes from EmbedPDF and maintains local state. - */ export function SearchAPIBridge() { - const { provides: search } = useSearch(); - const { registerBridge } = useViewer(); + const activeDocumentId = useActiveDocumentId(); + + // Don't render the inner component until we have a valid document ID + if (!activeDocumentId) { + return null; + } + + return ; +} + +function SearchAPIBridgeInner({ documentId }: { documentId: string }) { + const { provides: search } = useSearch(documentId); + const { registerBridge, scrollActions } = useViewer(); + + // Keep search ref updated to avoid re-running effects when object reference changes + const searchRef = useRef(search); + const isSearchingRef = useRef(false); + const lastScrolledIndexRef = useRef(null); + + useEffect(() => { + searchRef.current = search; + }, [search]); const [localState, setLocalState] = useState({ results: null as SearchResult[] | null, @@ -24,10 +41,18 @@ export function SearchAPIBridge() { }); // Subscribe to search result changes from EmbedPDF + const subscriptionRef = useRef<(() => void) | null>(null); + useEffect(() => { + // Cleanup previous subscription + if (subscriptionRef.current) { + subscriptionRef.current(); + subscriptionRef.current = null; + } + if (!search) return; - const unsubscribe = search.onSearchResultStateChange?.((state: any) => { + subscriptionRef.current = search.onSearchResultStateChange?.((state: any) => { if (!state) return; const newState = { @@ -42,27 +67,80 @@ export function SearchAPIBridge() { } return prevState; }); - }); + }) ?? null; - return unsubscribe; + return () => { + if (subscriptionRef.current) { + subscriptionRef.current(); + subscriptionRef.current = null; + } + }; }, [search]); - // Register bridge whenever search API or state changes + // Extract primitive values from localState to avoid object reference dependencies + const localResults = localState.results; + const localActiveIndex = localState.activeIndex; + + // Scroll to active result when it changes (for next/previous navigation) useEffect(() => { - if (search) { + if (!localResults || localResults.length === 0) { + lastScrolledIndexRef.current = null; + return; + } + + // Only scroll if the active index actually changed + const activeResultIndex = localActiveIndex - 1; // Convert back to 0-based + if (activeResultIndex >= 0 && + activeResultIndex < localResults.length && + lastScrolledIndexRef.current !== activeResultIndex) { + const activeResult = localResults[activeResultIndex]; + if (activeResult) { + const pageNumber = activeResult.pageIndex + 1; // Convert to 1-based page number + scrollActions.scrollToPage(pageNumber); + lastScrolledIndexRef.current = activeResultIndex; + } + } + }, [localResults, localActiveIndex, scrollActions]); + + // Register bridge whenever state changes + useEffect(() => { + const currentSearch = searchRef.current; + if (currentSearch) { registerBridge('search', { - state: localState, + state: { results: localResults, activeIndex: localActiveIndex }, api: { search: async (query: string) => { - if (search?.startSearch && search?.searchAllPages) { - search.startSearch(); - return search.searchAllPages(query); + // Prevent overlapping searches + if (isSearchingRef.current) { + return null; + } + + if (!currentSearch?.startSearch || !currentSearch?.searchAllPages) { + return null; + } + + isSearchingRef.current = true; + + try { + currentSearch.startSearch(); + const results = await currentSearch.searchAllPages(query); + return results; + } catch (error: any) { + // Handle abort errors gracefully - these occur when searches overlap + if (error?.type === 'abort' || error?.message?.includes('abort')) { + // Silently handle abort - this is expected when user types quickly + return null; + } + console.error('Search failed:', error); + return null; + } finally { + isSearchingRef.current = false; } }, clear: () => { try { - if (search?.stopSearch) { - search.stopSearch(); + if (currentSearch?.stopSearch) { + currentSearch.stopSearch(); } } catch (error) { console.warn('Error stopping search:', error); @@ -71,21 +149,21 @@ export function SearchAPIBridge() { }, next: () => { try { - search?.nextResult?.(); + currentSearch?.nextResult?.(); } catch (error) { console.warn('Error navigating to next result:', error); } }, previous: () => { try { - search?.previousResult?.(); + currentSearch?.previousResult?.(); } catch (error) { console.warn('Error navigating to previous result:', error); } }, goToResult: (index: number) => { try { - search?.goToResult?.(index); + currentSearch?.goToResult?.(index); } catch (error) { console.warn('Error going to result:', error); } @@ -93,7 +171,7 @@ export function SearchAPIBridge() { } }); } - }, [search, localState]); + }, [localResults, localActiveIndex, registerBridge]); return null; } diff --git a/frontend/src/core/components/viewer/SelectionAPIBridge.tsx b/frontend/src/core/components/viewer/SelectionAPIBridge.tsx index 3492c8166..4aaf361bf 100644 --- a/frontend/src/core/components/viewer/SelectionAPIBridge.tsx +++ b/frontend/src/core/components/viewer/SelectionAPIBridge.tsx @@ -1,10 +1,7 @@ import { useEffect, useState } from 'react'; -import { useSelectionCapability, SelectionRangeX } from '@embedpdf/plugin-selection/react'; +import { useSelectionCapability } from '@embedpdf/plugin-selection/react'; import { useViewer } from '@app/contexts/ViewerContext'; -/** - * Component that runs inside EmbedPDF context and updates selection state in ViewerContext - */ export function SelectionAPIBridge() { const { provides: selection } = useSelectionCapability(); const { registerBridge } = useViewer(); @@ -16,7 +13,6 @@ export function SelectionAPIBridge() { hasSelection }; - // Register this bridge with ViewerContext registerBridge('selection', { state: newState, api: { @@ -26,12 +22,10 @@ export function SelectionAPIBridge() { } }); - // Listen for selection changes to track when text is selected - const unsubscribe = selection.onSelectionChange((sel: SelectionRangeX | null) => { - const hasText = !!sel; + const unsubscribe = selection.onSelectionChange((event: any) => { + const hasText = !!(event?.selection || event); setHasSelection(hasText); const updatedState = { hasSelection: hasText }; - // Re-register with updated state registerBridge('selection', { state: updatedState, api: { diff --git a/frontend/src/core/components/viewer/SpreadAPIBridge.tsx b/frontend/src/core/components/viewer/SpreadAPIBridge.tsx index 1163e7c7c..fa3660526 100644 --- a/frontend/src/core/components/viewer/SpreadAPIBridge.tsx +++ b/frontend/src/core/components/viewer/SpreadAPIBridge.tsx @@ -1,16 +1,32 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useSpread, SpreadMode } from '@embedpdf/plugin-spread/react'; import { useViewer } from '@app/contexts/ViewerContext'; +import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId'; -/** - * Component that runs inside EmbedPDF context and updates spread state in ViewerContext - */ export function SpreadAPIBridge() { - const { provides: spread, spreadMode } = useSpread(); + const activeDocumentId = useActiveDocumentId(); + + // Don't render the inner component until we have a valid document ID + if (!activeDocumentId) { + return null; + } + + return ; +} + +function SpreadAPIBridgeInner({ documentId }: { documentId: string }) { + const { provides: spread, spreadMode } = useSpread(documentId); const { registerBridge, triggerImmediateSpreadUpdate } = useViewer(); + // Keep spread ref updated to avoid re-running effect when object reference changes + const spreadRef = useRef(spread); useEffect(() => { - if (!spread) { + spreadRef.current = spread; + }, [spread]); + + useEffect(() => { + const currentSpread = spreadRef.current; + if (!currentSpread || spreadMode === undefined) { return; } @@ -23,20 +39,20 @@ export function SpreadAPIBridge() { state: newState, api: { setSpreadMode: (mode: SpreadMode) => { - spread.setSpreadMode(mode); + currentSpread.setSpreadMode(mode); }, - getSpreadMode: () => spread.getSpreadMode(), + getSpreadMode: () => currentSpread.getSpreadMode(), toggleSpreadMode: () => { - const current = spread.getSpreadMode(); + const current = currentSpread.getSpreadMode(); const nextMode = current === SpreadMode.None ? SpreadMode.Odd : SpreadMode.None; - spread.setSpreadMode(nextMode); + currentSpread.setSpreadMode(nextMode); }, SpreadMode, }, }); triggerImmediateSpreadUpdate(spreadMode); - }, [spread, spreadMode, registerBridge, triggerImmediateSpreadUpdate]); + }, [spreadMode, registerBridge, triggerImmediateSpreadUpdate]); return null; } diff --git a/frontend/src/core/components/viewer/ZoomAPIBridge.tsx b/frontend/src/core/components/viewer/ZoomAPIBridge.tsx index 80f587870..a4cd74e18 100644 --- a/frontend/src/core/components/viewer/ZoomAPIBridge.tsx +++ b/frontend/src/core/components/viewer/ZoomAPIBridge.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useZoom, ZoomMode } from '@embedpdf/plugin-zoom/react'; import { useSpread, SpreadMode } from '@embedpdf/plugin-spread/react'; import { useViewer } from '@app/contexts/ViewerContext'; +import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId'; import { useFileState } from '@app/contexts/FileContext'; import { determineAutoZoom, @@ -12,8 +13,19 @@ import { import { getFirstPageAspectRatioFromStub } from '@app/utils/pageMetadata'; export function ZoomAPIBridge() { - const { provides: zoom, state: zoomState } = useZoom(); - const { spreadMode } = useSpread(); + const activeDocumentId = useActiveDocumentId(); + + // Don't render the inner component until we have a valid document ID + if (!activeDocumentId) { + return null; + } + + return ; +} + +function ZoomAPIBridgeInner({ documentId }: { documentId: string }) { + const { provides: zoom, state: zoomState } = useZoom(documentId); + const { spreadMode } = useSpread(documentId); const { registerBridge, triggerImmediateZoomUpdate } = useViewer(); const { selectors } = useFileState(); @@ -21,8 +33,14 @@ export function ZoomAPIBridge() { const lastSpreadMode = useRef(spreadMode ?? SpreadMode.None); const lastFileId = useRef(undefined); const lastAppliedZoom = useRef(null); + const zoomRef = useRef(zoom); const [autoZoomTick, setAutoZoomTick] = useState(0); + // Keep zoom ref updated + useEffect(() => { + zoomRef.current = zoom; + }, [zoom]); + const scheduleAutoZoom = useCallback(() => { hasSetInitialZoom.current = false; lastAppliedZoom.current = null; @@ -30,15 +48,22 @@ export function ZoomAPIBridge() { }, []); const requestFitWidth = useCallback(() => { - if (zoom) { - zoom.requestZoom(ZoomMode.FitWidth, { vx: 0.5, vy: 0 }); + if (zoomRef.current) { + zoomRef.current.requestZoom(ZoomMode.FitWidth, { vx: 0.5, vy: 0 }); } - }, [zoom]); + }, []); const stubs = selectors.getStirlingFileStubs(); const firstFileStub = stubs[0]; const firstFileId = firstFileStub?.id; + // Extract primitive values from zoomState for dependency arrays + const zoomLevel = zoomState?.zoomLevel; + const currentZoomLevel = zoomState?.currentZoomLevel; + + // Extract metadata aspect ratio as a primitive to avoid object reference issues + const metadataAspectRatio = getFirstPageAspectRatioFromStub(firstFileStub); + useEffect(() => { if (!firstFileId) { hasSetInitialZoom.current = false; @@ -59,7 +84,6 @@ export function ZoomAPIBridge() { lastSpreadMode.current = currentSpreadMode; const hadTrackedAutoZoom = lastAppliedZoom.current !== null; - const zoomLevel = zoomState?.zoomLevel; if ( zoomLevel === ZoomMode.FitWidth || zoomLevel === ZoomMode.Automatic || @@ -69,13 +93,13 @@ export function ZoomAPIBridge() { scheduleAutoZoom(); } } - }, [spreadMode, zoomState?.zoomLevel, scheduleAutoZoom, requestFitWidth]); + }, [spreadMode, zoomLevel, scheduleAutoZoom, requestFitWidth]); const isManagedZoom = !!zoom && - (zoomState?.zoomLevel === ZoomMode.FitWidth || - zoomState?.zoomLevel === ZoomMode.Automatic || + (zoomLevel === ZoomMode.FitWidth || + zoomLevel === ZoomMode.Automatic || lastAppliedZoom.current !== null); useFitWidthResize({ @@ -85,7 +109,7 @@ export function ZoomAPIBridge() { }); useEffect(() => { - if (!zoom || !zoomState) { + if (!zoom || zoomLevel === undefined || currentZoomLevel === undefined) { return; } @@ -97,14 +121,14 @@ export function ZoomAPIBridge() { return; } - if (zoomState.zoomLevel !== ZoomMode.FitWidth) { - if (zoomState.zoomLevel === ZoomMode.Automatic) { + if (zoomLevel !== ZoomMode.FitWidth) { + if (zoomLevel === ZoomMode.Automatic) { requestFitWidth(); } return; } - const fitWidthZoom = zoomState.currentZoomLevel; + const fitWidthZoom = currentZoomLevel; if (!fitWidthZoom || fitWidthZoom <= 0 || fitWidthZoom === 1) { return; } @@ -121,7 +145,6 @@ export function ZoomAPIBridge() { const applyAutoZoom = async () => { const currentSpreadMode = spreadMode ?? SpreadMode.None; const pagesPerSpread = currentSpreadMode !== SpreadMode.None ? 2 : 1; - const metadataAspectRatio = getFirstPageAspectRatioFromStub(firstFileStub); if (cancelled) { return; @@ -165,21 +188,31 @@ export function ZoomAPIBridge() { }; }, [ zoom, - zoomState, + zoomLevel, + currentZoomLevel, firstFileId, - firstFileStub, + metadataAspectRatio, requestFitWidth, autoZoomTick, spreadMode, triggerImmediateZoomUpdate, ]); + // Subscribe to zoom changes - use ref to avoid re-subscribing when zoom reference changes + const zoomSubscriptionRef = useRef<(() => void) | null>(null); + useEffect(() => { + // Cleanup previous subscription if any + if (zoomSubscriptionRef.current) { + zoomSubscriptionRef.current(); + zoomSubscriptionRef.current = null; + } + if (!zoom) { return; } - const unsubscribe = zoom.onZoomChange((event: { newZoom?: number }) => { + zoomSubscriptionRef.current = zoom.onZoomChange((event: { newZoom?: number }) => { if (typeof event?.newZoom !== 'number') { return; } @@ -188,17 +221,25 @@ export function ZoomAPIBridge() { }); return () => { - unsubscribe(); + if (zoomSubscriptionRef.current) { + zoomSubscriptionRef.current(); + zoomSubscriptionRef.current = null; + } }; }, [zoom, triggerImmediateZoomUpdate]); + // Extract primitive values to avoid dependency on object reference + const zoomStateCurrentZoomLevel = zoomState?.currentZoomLevel; + + // Register bridge - only re-run when actual values change useEffect(() => { - if (!zoom || !zoomState) { + const currentZoom = zoomRef.current; + if (!currentZoom || zoomStateCurrentZoomLevel === undefined) { return; } const currentZoomLevel = - lastAppliedZoom.current ?? zoomState.currentZoomLevel ?? 1; + lastAppliedZoom.current ?? zoomStateCurrentZoomLevel ?? 1; const newState = { currentZoom: currentZoomLevel, @@ -209,9 +250,9 @@ export function ZoomAPIBridge() { registerBridge('zoom', { state: newState, - api: zoom, + api: currentZoom, }); - }, [zoom, zoomState, registerBridge, triggerImmediateZoomUpdate]); + }, [zoomStateCurrentZoomLevel, registerBridge, triggerImmediateZoomUpdate]); return null; } \ No newline at end of file diff --git a/frontend/src/core/components/viewer/useActiveDocumentId.ts b/frontend/src/core/components/viewer/useActiveDocumentId.ts new file mode 100644 index 000000000..aff97eeda --- /dev/null +++ b/frontend/src/core/components/viewer/useActiveDocumentId.ts @@ -0,0 +1,9 @@ +import { useActiveDocument } from '@app/components/viewer/ActiveDocumentContext'; + +/** + * Hook to get the currently active document ID. + * Uses a shared context to avoid multiple subscriptions. + */ +export function useActiveDocumentId(): string | null { + return useActiveDocument(); +} diff --git a/frontend/src/core/contexts/ViewerContext.tsx b/frontend/src/core/contexts/ViewerContext.tsx index 3d1bd8dd1..c2429a323 100644 --- a/frontend/src/core/contexts/ViewerContext.tsx +++ b/frontend/src/core/contexts/ViewerContext.tsx @@ -38,6 +38,8 @@ import { ExportState, ThumbnailAPIWrapper, BookmarkState, + DocumentPermissionsState, + PdfPermissionFlag, } from '@app/contexts/viewer/viewerBridges'; import { SpreadMode } from '@embedpdf/plugin-spread/react'; @@ -112,6 +114,8 @@ interface ViewerContextType { getExportState: () => ExportState; getBookmarkState: () => BookmarkState; hasBookmarkSupport: () => boolean; + getDocumentPermissions: () => DocumentPermissionsState; + hasPermission: (flag: PdfPermissionFlag) => boolean; // Immediate update callbacks registerImmediateZoomUpdate: (callback: (percent: number) => void) => () => void; @@ -308,6 +312,34 @@ export const ViewerProvider: React.FC = ({ children }) => { const hasBookmarkSupport = () => Boolean(bridgeRefs.current.bookmark); + const getDocumentPermissions = (): DocumentPermissionsState => { + return bridgeRefs.current.permissions?.state || { + isEncrypted: false, + isOwnerUnlocked: false, + permissions: PdfPermissionFlag.AllowAll, + canPrint: true, + canModifyContents: true, + canCopyContents: true, + canModifyAnnotations: true, + canFillForms: true, + canExtractForAccessibility: true, + canAssembleDocument: true, + canPrintHighQuality: true, + }; + }; + + const hasPermission = (flag: PdfPermissionFlag): boolean => { + const api = bridgeRefs.current.permissions?.api; + if (api?.hasPermission) { + return api.hasPermission(flag); + } + // Default: allow all permissions - warn in development + if (process.env.NODE_ENV === 'development') { + console.warn('[ViewerContext] Permissions API not available, defaulting to allow'); + } + return true; + }; + // Action handlers - call APIs directly const { scrollActions, @@ -360,6 +392,8 @@ export const ViewerProvider: React.FC = ({ children }) => { getExportState, getBookmarkState, hasBookmarkSupport, + getDocumentPermissions, + hasPermission, // Immediate updates registerImmediateZoomUpdate, diff --git a/frontend/src/core/contexts/viewer/viewerActions.ts b/frontend/src/core/contexts/viewer/viewerActions.ts index 1af7b5f01..85d6a1fc9 100644 --- a/frontend/src/core/contexts/viewer/viewerActions.ts +++ b/frontend/src/core/contexts/viewer/viewerActions.ts @@ -100,32 +100,63 @@ export function createViewerActions({ scrollToPage: (page: number, behavior?: 'smooth' | 'instant') => { const api = registry.current.scroll?.api; if (api?.scrollToPage) { - api.scrollToPage({ pageNumber: page, behavior: behavior || 'smooth' }); + try { + api.scrollToPage({ pageNumber: page, behavior: behavior || 'smooth' }); + } catch (error) { + // Silently handle "Strategy not found" errors that occur during document transitions + if (process.env.NODE_ENV === 'development') { + console.warn('[ScrollActions] scrollToPage failed (document may be transitioning):', error); + } + } } }, scrollToFirstPage: () => { const api = registry.current.scroll?.api; if (api?.scrollToPage) { - api.scrollToPage({ pageNumber: 1 }); + try { + api.scrollToPage({ pageNumber: 1 }); + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.warn('[ScrollActions] scrollToFirstPage failed:', error); + } + } } }, scrollToPreviousPage: () => { const api = registry.current.scroll?.api; if (api?.scrollToPreviousPage) { - api.scrollToPreviousPage(); + try { + api.scrollToPreviousPage(); + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.warn('[ScrollActions] scrollToPreviousPage failed:', error); + } + } } }, scrollToNextPage: () => { const api = registry.current.scroll?.api; if (api?.scrollToNextPage) { - api.scrollToNextPage(); + try { + api.scrollToNextPage(); + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.warn('[ScrollActions] scrollToNextPage failed:', error); + } + } } }, scrollToLastPage: () => { const api = registry.current.scroll?.api; const state = getScrollState(); if (api?.scrollToPage && state.totalPages > 0) { - api.scrollToPage({ pageNumber: state.totalPages }); + try { + api.scrollToPage({ pageNumber: state.totalPages }); + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.warn('[ScrollActions] scrollToLastPage failed:', error); + } + } } }, }; diff --git a/frontend/src/core/contexts/viewer/viewerBridges.ts b/frontend/src/core/contexts/viewer/viewerBridges.ts index f7172e54d..ce1a61e21 100644 --- a/frontend/src/core/contexts/viewer/viewerBridges.ts +++ b/frontend/src/core/contexts/viewer/viewerBridges.ts @@ -1,6 +1,38 @@ import { SpreadMode } from '@embedpdf/plugin-spread/react'; import { PdfBookmarkObject } from '@embedpdf/models'; +export enum PdfPermissionFlag { + Print = 0x0004, + ModifyContents = 0x0008, + CopyContents = 0x0010, + ModifyAnnotations = 0x0020, + FillForms = 0x0100, + ExtractForAccessibility = 0x0200, + AssembleDocument = 0x0400, + PrintHighQuality = 0x0800, + AllowAll = 0x0f3c, +} + +export interface DocumentPermissionsState { + isEncrypted: boolean; + isOwnerUnlocked: boolean; + permissions: number; + canPrint: boolean; + canModifyContents: boolean; + canCopyContents: boolean; + canModifyAnnotations: boolean; + canFillForms: boolean; + canExtractForAccessibility: boolean; + canAssembleDocument: boolean; + canPrintHighQuality: boolean; +} + +export interface DocumentPermissionsAPIWrapper { + hasPermission: (flag: PdfPermissionFlag) => boolean; + hasAllPermissions: (flags: PdfPermissionFlag[]) => boolean; + getEffectivePermission: (flag: PdfPermissionFlag) => boolean; +} + export interface ScrollAPIWrapper { scrollToPage: (params: { pageNumber: number; behavior?: ScrollBehavior }) => void; scrollToPreviousPage: () => void; @@ -137,6 +169,7 @@ export interface BridgeStateMap { export: ExportState; bookmark: BookmarkState; print: unknown; + permissions: DocumentPermissionsState; } export interface BridgeApiMap { @@ -151,6 +184,7 @@ export interface BridgeApiMap { export: ExportAPIWrapper; bookmark: BookmarkAPIWrapper; print: PrintAPIWrapper; + permissions: DocumentPermissionsAPIWrapper; } export type BridgeKey = keyof BridgeStateMap; @@ -171,6 +205,7 @@ export const createBridgeRegistry = (): ViewerBridgeRegistry => ({ export: null, bookmark: null, print: null, + permissions: null, }); export function registerBridge( diff --git a/frontend/src/core/tools/annotate/useAnnotationSelection.ts b/frontend/src/core/tools/annotate/useAnnotationSelection.ts index fd4027daf..020408b59 100644 --- a/frontend/src/core/tools/annotate/useAnnotationSelection.ts +++ b/frontend/src/core/tools/annotate/useAnnotationSelection.ts @@ -270,7 +270,11 @@ export function useAnnotationSelection({ // internal selection state (e.g., accessing `selectedUid` on // an undefined object). Treat this as "no current selection" // instead of crashing the annotations tool. - console.error('[useAnnotationSelection] getSelectedAnnotation failed:', error); + // Only log unexpected errors - "No active document" is a common expected state during init + const errorMessage = error instanceof Error ? error.message : String(error); + if (!errorMessage.includes('No active document')) { + console.error('[useAnnotationSelection] getSelectedAnnotation failed:', error); + } ann = null; } }