From 1bc8e7613fbbee77610b5003f9aa1a6263dc0c59 Mon Sep 17 00:00:00 2001 From: Reece Date: Fri, 31 Oct 2025 18:34:33 +0000 Subject: [PATCH] Viewer update and autozoom --- frontend/package-lock.json | 385 +++++++++++------- frontend/package.json | 36 +- .../core/components/viewer/EmbedPdfViewer.tsx | 13 +- .../core/components/viewer/LocalEmbedPDF.tsx | 11 +- .../components/viewer/PdfViewerToolbar.tsx | 47 ++- .../components/viewer/SpreadAPIBridge.tsx | 51 +-- .../core/components/viewer/ZoomAPIBridge.tsx | 290 ++++++++++--- frontend/src/core/contexts/ViewerContext.tsx | 101 +++-- .../src/core/contexts/file/fileActions.ts | 14 +- frontend/src/core/types/fileContext.ts | 2 + frontend/src/core/utils/pageMetadata.ts | 53 +++ frontend/src/core/utils/thumbnailUtils.ts | 16 +- frontend/src/core/utils/viewerZoom.ts | 188 +++++++++ 13 files changed, 906 insertions(+), 301 deletions(-) create mode 100644 frontend/src/core/utils/pageMetadata.ts create mode 100644 frontend/src/core/utils/viewerZoom.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c884272c4..c4f4caded 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,24 +10,24 @@ "license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.7.7", - "@embedpdf/core": "^1.3.14", - "@embedpdf/engines": "^1.3.14", - "@embedpdf/plugin-annotation": "^1.3.14", - "@embedpdf/plugin-export": "^1.3.14", - "@embedpdf/plugin-history": "^1.3.14", - "@embedpdf/plugin-interaction-manager": "^1.3.14", - "@embedpdf/plugin-loader": "^1.3.14", - "@embedpdf/plugin-pan": "^1.3.14", - "@embedpdf/plugin-render": "^1.3.14", - "@embedpdf/plugin-rotate": "^1.3.14", - "@embedpdf/plugin-scroll": "^1.3.14", - "@embedpdf/plugin-search": "^1.3.14", - "@embedpdf/plugin-selection": "^1.3.14", - "@embedpdf/plugin-spread": "^1.3.14", - "@embedpdf/plugin-thumbnail": "^1.3.14", - "@embedpdf/plugin-tiling": "^1.3.14", - "@embedpdf/plugin-viewport": "^1.3.14", - "@embedpdf/plugin-zoom": "^1.3.14", + "@embedpdf/core": "^1.4.1", + "@embedpdf/engines": "^1.4.1", + "@embedpdf/plugin-annotation": "^1.4.1", + "@embedpdf/plugin-export": "^1.4.1", + "@embedpdf/plugin-history": "^1.4.1", + "@embedpdf/plugin-interaction-manager": "^1.4.1", + "@embedpdf/plugin-loader": "^1.4.1", + "@embedpdf/plugin-pan": "^1.4.1", + "@embedpdf/plugin-render": "^1.4.1", + "@embedpdf/plugin-rotate": "^1.4.1", + "@embedpdf/plugin-scroll": "^1.4.1", + "@embedpdf/plugin-search": "^1.4.1", + "@embedpdf/plugin-selection": "^1.4.1", + "@embedpdf/plugin-spread": "^1.4.1", + "@embedpdf/plugin-thumbnail": "^1.4.1", + "@embedpdf/plugin-tiling": "^1.4.1", + "@embedpdf/plugin-viewport": "^1.4.1", + "@embedpdf/plugin-zoom": "^1.4.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@iconify/react": "^6.0.2", @@ -502,63 +502,65 @@ } }, "node_modules/@embedpdf/core": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.14.tgz", - "integrity": "sha512-lE/vfhA53CxamaCfGWEibrEPr+JeZT42QCF+cOELUwv4+Zt6b+IE6+4wsznx/8wjjJYwllXJ3GJ/un1UzTqARw==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.4.1.tgz", + "integrity": "sha512-TGpxn2CvAKRnOJWJ3bsK+dKBiCp75ehxftRUmv7wAmPomhnG5XrDfoWJungvO+zbbqAwso6PocdeXINVt3hlAw==", "license": "MIT", "dependencies": { - "@embedpdf/engines": "1.3.14", - "@embedpdf/models": "1.3.14" + "@embedpdf/engines": "1.4.1", + "@embedpdf/models": "1.4.1" }, "peerDependencies": { "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/engines": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-1.3.14.tgz", - "integrity": "sha512-+/FPW2gAzj2lQYvsMH/Oj9+MEXgkyEuyYDC+HFkltTuXvmiP2S/3BD0YslZDX9K4BzcmMxnWB+BiQpNJokbDVg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-1.4.1.tgz", + "integrity": "sha512-yugIb5OwTI/1VnAaEvSYxAd2DvYBPkV/D7wytagyaOq98o3sqzcY2Q9zHt+LhnawA5KKG1e/FDPjCd4qm8gsvg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14", - "@embedpdf/pdfium": "1.3.14" + "@embedpdf/models": "1.4.1", + "@embedpdf/pdfium": "1.4.1" }, "peerDependencies": { "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/models": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-1.3.14.tgz", - "integrity": "sha512-BujY4bmr8b2DQdoZkOge03SzoRVoWxzfIQATLSPPtp4WiFh1U4BPp6cADlGuCwGkp6zBcH/aM4h8PwwA75d/eg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-1.4.1.tgz", + "integrity": "sha512-2nTg8Q1qpplBvspZJXMCZOA+/OILpfdNRPddlplxZXY/Upx0rzKXx/e6pXWW7AuOgtfGneT4h9tMs3A595/PdQ==", "license": "MIT" }, "node_modules/@embedpdf/pdfium": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-1.3.14.tgz", - "integrity": "sha512-TQMZabXzHmzvvfPwopubFcYgQuYV7POvMgjICYu3Pgfn3sgr+UdIUh3aNXR/COcl3q8sXPMFQ2GDuyOHR9QQnA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-1.4.1.tgz", + "integrity": "sha512-BekKEK4UNCwzj7xOffKn6WpL0FQHxq+mTj2iGI3N7OwAX2J/BO2G+rDOB+lvojQG+Dkpg8uqm427ZKJDRyLgVQ==", "license": "MIT" }, "node_modules/@embedpdf/plugin-annotation": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-1.3.14.tgz", - "integrity": "sha512-JJYqEWwUKCdBZsXCDq/CW96p3pVLn8N+XZ4W3OyL7djI2fvYC9x6ys9m82vwlSathAVOxk1D7xXiY8AzJQVF0Q==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-1.4.1.tgz", + "integrity": "sha512-d4HibNy6ecyDqx2Y2R8VjaqppSdjNofAJmU6VenOd88wn080sAUqvnkeVJ6ehJH5BoND4ymQrcAkcbVeYK0myA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14", - "@embedpdf/utils": "1.3.14" + "@embedpdf/models": "1.4.1", + "@embedpdf/utils": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-history": "1.3.14", - "@embedpdf/plugin-interaction-manager": "1.3.14", - "@embedpdf/plugin-selection": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-history": "1.4.1", + "@embedpdf/plugin-interaction-manager": "1.4.1", + "@embedpdf/plugin-selection": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -566,31 +568,32 @@ } }, "node_modules/@embedpdf/plugin-export": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-1.3.14.tgz", - "integrity": "sha512-fMGp2YxvI4uTRIViUKxfnJts2Jw/vktEM45XUNGNSjT/kAW6znVNgdceYjpK++xU8CGs2grAQ1i5UvMd3aRNDA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-1.4.1.tgz", + "integrity": "sha512-g89fREFM/zkt2Ai2Q5dWwDkhXgC/JmVyUniaMgm1fTG/MZ0Z05E7f34DUzX/CKcJyVjxEgl6tojBTMeUbm15bA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", + "@embedpdf/core": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-history": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.3.14.tgz", - "integrity": "sha512-77hnNLp0W0FHw8lT7SeqzCgp8bOClfeOAPZdcInu/jPDhVASUGYbtE/0fkLhiaqPH7kyMirNCLif4sF6n4b5vg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.4.1.tgz", + "integrity": "sha512-5WLDiNMH6tACkLGGv/lJtNsDeozOhSbrh0mjD1btHun8u7Yscu/Vf8tdJRUOsd+nULivo2nQ2NFNKu0OTbVo8w==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", + "@embedpdf/core": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", @@ -598,232 +601,245 @@ } }, "node_modules/@embedpdf/plugin-interaction-manager": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.3.14.tgz", - "integrity": "sha512-nR0ZxNoTQtGqOHhweFh6QJ+nUJ4S4Ag1wWur6vAUAi8U95HUOfZhOEa0polZo0zR9WmmblGqRWjFM+mVSOoi1w==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.4.1.tgz", + "integrity": "sha512-Ng02S9SFIAi9JZS5rI+NXSnZZ1Yk9YYRw4MlN2pig49qOyivZdz0oScZaYxQPewo8ccJkLeghjdeWswOBW/6cA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", + "@embedpdf/core": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-loader": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.3.14.tgz", - "integrity": "sha512-KoJX1MacEWE2DrO1OeZeG/Ehz76//u+ida/xb4r9BfwqAp5TfYlksq09cOvcF8LMW5FY4pbAL+AHKI1Hjz+HNA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.4.1.tgz", + "integrity": "sha512-m3ZOk8JygsLxoa4cZ+0BVB5pfRWuBCg2/gPqjhoFZNKTqAFw4J6HGUrhYKg94GRYe+w1cTJl/NbTBYuU5DOrsA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", + "@embedpdf/core": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-pan": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-1.3.14.tgz", - "integrity": "sha512-7EG+I5nn8yDCV8pT4x/g5mv7zJli2t3wPrh6Kt8uIpUorPHNb6J0Z67gl0uc/8rEasNzuKOuT0er46Y6/UYLzQ==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-1.4.1.tgz", + "integrity": "sha512-zmOZJ9dUqXiaV0F5GPf/5WTWf3jAEkiv153Tl3x8HT9Rfff+WQhV48NruCIBAy/T4jVt4aH7D1zt/B/ftvcdkA==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-interaction-manager": "1.3.14", - "@embedpdf/plugin-viewport": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-interaction-manager": "1.4.1", + "@embedpdf/plugin-viewport": "1.4.1", "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.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.3.14.tgz", - "integrity": "sha512-IPj7GCQXJBsY++JaU+z7y+FwX5NaDBj4YYV6hsHNtSGf42Y1AdlwJzDYetivG2bA84xmk7KgD1X2Y3eIFBhjwA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.4.1.tgz", + "integrity": "sha512-gKCdNKw6WBHBEpTc2DLBWIWOxzsNnaNbpfeY6C4f2Bum0EO+XW3Hl2oIx1uaRHjIhhnXso1J3QweqelsPwDGwg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", + "@embedpdf/core": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-rotate": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-1.3.14.tgz", - "integrity": "sha512-OroEm11x/fPPXI9C0X+nm9LOjwaI0MvsToZRH+HpV60/FbQeOJvt6D8wThCDVLK95Na6A+JeYIMEu+Hiix7H+A==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-1.4.1.tgz", + "integrity": "sha512-hVzHkKwMNH3tUhxqJGsj5qTLpYZXbj6E74AEcG0w/fz5FrK7EnofPqt0gRfYmIzxnQGIh+39BRtcp8gmx8UNnw==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", + "@embedpdf/core": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-scroll": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.3.14.tgz", - "integrity": "sha512-fQbt7OlRMLQJMuZj/Bzh0qpRxMw1ld5Qe/OTw8N54b/plljnFA52joE7cITl3H03huWWyHS3NKOScbw7f34dog==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.4.1.tgz", + "integrity": "sha512-Y9O+matB4j4fLim5s/jn7qIi+lMC9vmDJRpJhiWe8bvD9oYLP2xfD/DdhFgAjRKcNhPoxC+j8q8QN5BMeGAv2Q==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-viewport": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-viewport": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-search": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-1.3.14.tgz", - "integrity": "sha512-tlZEgR2tG+GSNnh2u1SjCxhUHfTDgcr38sE/xRK1bRLDGPZWlr6Ln7qP7JSWqeYBGni75sGrj0iZqcZbPWyJag==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-1.4.1.tgz", + "integrity": "sha512-8JG4CbOcUsLuT0vHJJ4cECmu+Yn53EokWFUVXi2Mo/XvHjhrQuWmD7+y6s/qQPEpctFYWmUCXTDAX9ynPud+2Q==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-loader": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-loader": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-selection": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.3.14.tgz", - "integrity": "sha512-EXENuaAsse3rT6cjA1nYzyrNvoy62ojJl28wblCng6zcs3HSlGPemIQZAvaYKPUxoY608M+6nKlcMQ5neRnk/A==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.4.1.tgz", + "integrity": "sha512-lo5Ytk1PH0PrRKv6zKVupm4t02VGsqIrnSIeP6NO8Ujx0wfqEhj//sqIuO/EwfFVJD8lcQIP9UUo9y8baCrEog==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-interaction-manager": "1.3.14", - "@embedpdf/plugin-viewport": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-interaction-manager": "1.4.1", + "@embedpdf/plugin-viewport": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-spread": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-1.3.14.tgz", - "integrity": "sha512-DVlk6tDgUoDRkp2S4Jc3LrRTuf4DPMlph9vywJw5z6Qpbh0vgcMnObg896/S0Eu5FgACNAj0WGcXpLrcrn5b9Q==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-1.4.1.tgz", + "integrity": "sha512-l+SrDVGTiiItkt2cEtzv7V/X5HhmLbYHcQ8CFobGeIKdJtzKS1Nu/JSKqg7Ki7eCNgyPL1yMNfNE92bNKYVN4w==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-loader": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-loader": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-thumbnail": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-1.3.14.tgz", - "integrity": "sha512-cnwb5dG8Jph8XSArys1WFCQ6kK2R5FKoO0B5mDrHFv9Fcm2pKszlmZC/NDoskX4pgNUgSnwhI1X3cP37ebF9Ng==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-1.4.1.tgz", + "integrity": "sha512-bN3msjI0PovazgbPK3LyugYVTwIDo0RyBUhBaG42FgJxeY3hmFOWTPgfUH1QF7twHlySnksIvHRFYR3nViryVw==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-render": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-render": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-tiling": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.3.14.tgz", - "integrity": "sha512-SaCTo2LdZwGeE6jCqkwJxvwt8YKbsI3QGxa9S7Ez+5OcBchlhHeTfLQswcErDQ3WH2p8WHtGuucAcOLrVVOm0A==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.4.1.tgz", + "integrity": "sha512-wgTfj5T8HV6KP61iiR63DVNrbVp8sPxTqa1Sm+2/D0jY+EPSSCmpt1/qYWiAXd1X+t78foOjCnfbo7fEMn5/pg==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-render": "1.3.14", - "@embedpdf/plugin-scroll": "1.3.14", - "@embedpdf/plugin-viewport": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-render": "1.4.1", + "@embedpdf/plugin-scroll": "1.4.1", + "@embedpdf/plugin-viewport": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-viewport": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.3.14.tgz", - "integrity": "sha512-mfJ7EbbU68eKk6oFvQ4ozGJNpxUxWbjQ5Gm3uuB+Gj5/tWgBocBOX36k/9LgivEEeX7g2S0tOgyErljApmH8Vg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.4.1.tgz", + "integrity": "sha512-+TgFHKPCLTBiDYe2DdsmTS37hwQgcZ3dYIc7bE0l5cp+GVwouu1h0MTmjL+90loizeWwCiu10E/zXR6hz+CUaQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14" + "@embedpdf/models": "1.4.1" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", + "@embedpdf/core": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/plugin-zoom": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-1.3.14.tgz", - "integrity": "sha512-/N5tyMk+8OzhObrS3O9yPkcmX8EPiuTo+WaT2QCVSmIUqKnOO4AnKpHJ6Vl0uVhcuXHCMwLucZKyhJ7tRqavwg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-1.4.1.tgz", + "integrity": "sha512-9HocmXnPZxqN06q7kyNAmLjgDHOEW8/8QfgNE3nMpRyNHIgnAjxvsWc9lApgp5ErDPG0cSDt0Cduil6nB3wSBQ==", "license": "MIT", "dependencies": { - "@embedpdf/models": "1.3.14", + "@embedpdf/models": "1.4.1", "hammerjs": "^2.0.8" }, "peerDependencies": { - "@embedpdf/core": "1.3.14", - "@embedpdf/plugin-interaction-manager": "1.3.14", - "@embedpdf/plugin-scroll": "1.3.14", - "@embedpdf/plugin-viewport": "1.3.14", + "@embedpdf/core": "1.4.1", + "@embedpdf/plugin-interaction-manager": "1.4.1", + "@embedpdf/plugin-scroll": "1.4.1", + "@embedpdf/plugin-viewport": "1.4.1", "preact": "^10.26.4", "react": ">=16.8.0", "react-dom": ">=16.8.0", + "svelte": ">=5 <6", "vue": ">=3.2.0" } }, "node_modules/@embedpdf/utils": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-1.3.14.tgz", - "integrity": "sha512-gxEJD12nageCMqAjdbicNfDQolXU3nvnV0EX96OdZITRNj0Q1tisutVYoaxcCiJu3vvIEOzipjsAnQOubbFCEA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-1.4.1.tgz", + "integrity": "sha512-vvJ51Qsz3PyJWR2YvDMMpJXg4+YqdV7Vn2cusmW9sx+4EnAiBiw0HevEE+FepgFV8k+A0WbwXzmsujDIQJ7R4A==", "license": "MIT", "peerDependencies": { "preact": "^10.26.4", @@ -3045,6 +3061,16 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz", + "integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "acorn": "^8.9.0" + } + }, "node_modules/@swc/core": { "version": "1.13.5", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", @@ -3802,7 +3828,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/gapi": { @@ -4743,7 +4768,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -5147,6 +5171,16 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/b4a": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", @@ -7176,6 +7210,13 @@ "node": "*" } }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT", + "peer": true + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -7234,6 +7275,16 @@ "node": ">=0.10" } }, + "node_modules/esrap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.1.tgz", + "integrity": "sha512-ebTT9B6lOtZGMgJ3o5r12wBacHctG7oEWazIda8UlPfA3HD/Wrv8FdXoVo73vzdpwCxNyXjPauyN2bbJzMkB9A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -8747,6 +8798,16 @@ "dev": true, "license": "MIT" }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.6" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -9623,6 +9684,13 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT", + "peer": true + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -12959,6 +13027,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svelte": { + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.0.tgz", + "integrity": "sha512-1sRxVbgJAB+UGzwkc3GUoiBSzEOf0jqzccMaVoI2+pI+kASUe9qubslxace8+Mzhqw19k4syTA5niCIJwfXpOA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^2.1.0", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -14447,6 +14551,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT", + "peer": true + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/frontend/package.json b/frontend/package.json index 892e48569..bebf9a3e6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,24 +6,24 @@ "proxy": "http://localhost:8080", "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.7.7", - "@embedpdf/core": "^1.3.14", - "@embedpdf/engines": "^1.3.14", - "@embedpdf/plugin-annotation": "^1.3.14", - "@embedpdf/plugin-export": "^1.3.14", - "@embedpdf/plugin-history": "^1.3.14", - "@embedpdf/plugin-interaction-manager": "^1.3.14", - "@embedpdf/plugin-loader": "^1.3.14", - "@embedpdf/plugin-pan": "^1.3.14", - "@embedpdf/plugin-render": "^1.3.14", - "@embedpdf/plugin-rotate": "^1.3.14", - "@embedpdf/plugin-scroll": "^1.3.14", - "@embedpdf/plugin-search": "^1.3.14", - "@embedpdf/plugin-selection": "^1.3.14", - "@embedpdf/plugin-spread": "^1.3.14", - "@embedpdf/plugin-thumbnail": "^1.3.14", - "@embedpdf/plugin-tiling": "^1.3.14", - "@embedpdf/plugin-viewport": "^1.3.14", - "@embedpdf/plugin-zoom": "^1.3.14", + "@embedpdf/core": "^1.4.1", + "@embedpdf/engines": "^1.4.1", + "@embedpdf/plugin-annotation": "^1.4.1", + "@embedpdf/plugin-export": "^1.4.1", + "@embedpdf/plugin-history": "^1.4.1", + "@embedpdf/plugin-interaction-manager": "^1.4.1", + "@embedpdf/plugin-loader": "^1.4.1", + "@embedpdf/plugin-pan": "^1.4.1", + "@embedpdf/plugin-render": "^1.4.1", + "@embedpdf/plugin-rotate": "^1.4.1", + "@embedpdf/plugin-scroll": "^1.4.1", + "@embedpdf/plugin-search": "^1.4.1", + "@embedpdf/plugin-selection": "^1.4.1", + "@embedpdf/plugin-spread": "^1.4.1", + "@embedpdf/plugin-thumbnail": "^1.4.1", + "@embedpdf/plugin-tiling": "^1.4.1", + "@embedpdf/plugin-viewport": "^1.4.1", + "@embedpdf/plugin-zoom": "^1.4.1", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@iconify/react": "^6.0.2", diff --git a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx index f85058a73..5ec3e6eb7 100644 --- a/frontend/src/core/components/viewer/EmbedPdfViewer.tsx +++ b/frontend/src/core/components/viewer/EmbedPdfViewer.tsx @@ -35,14 +35,12 @@ const EmbedPdfViewerContent = ({ const viewerRef = React.useRef(null); const [isViewerHovered, setIsViewerHovered] = React.useState(false); - const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getZoomState, getSpreadState, getRotationState, isAnnotationMode, isAnnotationsVisible, exportActions } = useViewer(); + const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getRotationState, isAnnotationMode, isAnnotationsVisible, exportActions } = useViewer(); // Register viewer right-rail buttons useViewerRightRailButtons(); const scrollState = getScrollState(); - const zoomState = getZoomState(); - const spreadState = getSpreadState(); const rotationState = getRotationState(); // Track initial rotation to detect changes @@ -320,15 +318,6 @@ const EmbedPdfViewerContent = ({ { - // Page navigation handled by scrollActions - console.log('Navigate to page:', page); - }} - dualPage={spreadState.isDualPage} - onDualPageToggle={() => { - spreadActions.toggleSpreadMode(); - }} - currentZoom={zoomState.zoomPercent} /> diff --git a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx index bfe75df2f..1fe44b3d4 100644 --- a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx +++ b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx @@ -8,7 +8,7 @@ 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 { RenderPluginPackage } from '@embedpdf/plugin-render/react'; -import { ZoomPluginPackage } from '@embedpdf/plugin-zoom/react'; +import { ZoomPluginPackage, ZoomMode } from '@embedpdf/plugin-zoom/react'; import { InteractionManagerPluginPackage, PagePointerProvider, GlobalPointerProvider } from '@embedpdf/plugin-interaction-manager/react'; import { SelectionLayer, SelectionPluginPackage } from '@embedpdf/plugin-selection/react'; import { TilingLayer, TilingPluginPackage } from '@embedpdf/plugin-tiling/react'; @@ -114,9 +114,9 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur // Register zoom plugin with configuration createPluginRegistration(ZoomPluginPackage, { - defaultZoomLevel: 1.4, // Start at 140% zoom for better readability + defaultZoomLevel: ZoomMode.FitWidth, // Start with FitWidth, will be adjusted in ZoomAPIBridge minZoom: 0.2, - maxZoom: 3.0, + maxZoom: 5.0, }), // Register tiling plugin (depends on Render, Scroll, Viewport) @@ -287,6 +287,8 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur minHeight: 0, minWidth: 0, contain: 'strict', + display: 'flex', + justifyContent: 'center', }} >
void; - - // Dual page toggle (placeholder for now) - dualPage?: boolean; - onDualPageToggle?: () => void; - - // Zoom controls (connected via ViewerContext) - currentZoom?: number; } export function PdfViewerToolbar({ currentPage = 1, totalPages: _totalPages = 1, onPageChange, - dualPage = false, - onDualPageToggle, - currentZoom: _currentZoom = 100, }: PdfViewerToolbarProps) { const { t } = useTranslation(); - const { getScrollState, getZoomState, scrollActions, zoomActions, registerImmediateZoomUpdate, registerImmediateScrollUpdate } = useViewer(); + const { + getScrollState, + getZoomState, + getSpreadState, + scrollActions, + zoomActions, + spreadActions, + registerImmediateZoomUpdate, + registerImmediateScrollUpdate, + registerImmediateSpreadUpdate, + } = useViewer(); const scrollState = getScrollState(); const zoomState = getZoomState(); + const spreadState = getSpreadState(); const [pageInput, setPageInput] = useState(scrollState.currentPage || currentPage); const [displayZoomPercent, setDisplayZoomPercent] = useState(zoomState.zoomPercent || 140); + const [isDualPageActive, setIsDualPageActive] = useState(spreadState.isDualPage); // Register for immediate scroll updates and sync with actual scroll state useEffect(() => { @@ -53,6 +55,13 @@ export function PdfViewerToolbar({ setDisplayZoomPercent(zoomState.zoomPercent || 140); }, [zoomState.zoomPercent, registerImmediateZoomUpdate]); + useEffect(() => { + registerImmediateSpreadUpdate((_mode, isDual) => { + setIsDualPageActive(isDual); + }); + setIsDualPageActive(spreadState.isDualPage); + }, [registerImmediateSpreadUpdate, spreadState.isDualPage]); + const handleZoomOut = () => { zoomActions.zoomOut(); }; @@ -69,6 +78,10 @@ export function PdfViewerToolbar({ setPageInput(page); }; + const handleDualPageToggle = () => { + spreadActions.toggleSpreadMode(); + }; + const handleFirstPage = () => { scrollActions.scrollToFirstPage(); }; @@ -188,15 +201,19 @@ export function PdfViewerToolbar({ {/* Dual Page Toggle */} {/* Zoom Controls */} diff --git a/frontend/src/core/components/viewer/SpreadAPIBridge.tsx b/frontend/src/core/components/viewer/SpreadAPIBridge.tsx index e256ecc8d..1163e7c7c 100644 --- a/frontend/src/core/components/viewer/SpreadAPIBridge.tsx +++ b/frontend/src/core/components/viewer/SpreadAPIBridge.tsx @@ -7,33 +7,36 @@ import { useViewer } from '@app/contexts/ViewerContext'; */ export function SpreadAPIBridge() { const { provides: spread, spreadMode } = useSpread(); - const { registerBridge } = useViewer(); + const { registerBridge, triggerImmediateSpreadUpdate } = useViewer(); useEffect(() => { - if (spread) { - const newState = { - spreadMode, - isDualPage: spreadMode !== SpreadMode.None - }; - - // Register this bridge with ViewerContext - registerBridge('spread', { - state: newState, - api: { - setSpreadMode: (mode: SpreadMode) => { - spread.setSpreadMode(mode); - }, - getSpreadMode: () => spread.getSpreadMode(), - toggleSpreadMode: () => { - // Toggle between None and Odd (most common dual-page mode) - const newMode = spreadMode === SpreadMode.None ? SpreadMode.Odd : SpreadMode.None; - spread.setSpreadMode(newMode); - }, - SpreadMode: SpreadMode, // Export enum for reference - } - }); + if (!spread) { + return; } - }, [spread, spreadMode]); + + const newState = { + spreadMode, + isDualPage: spreadMode !== SpreadMode.None, + }; + + registerBridge('spread', { + state: newState, + api: { + setSpreadMode: (mode: SpreadMode) => { + spread.setSpreadMode(mode); + }, + getSpreadMode: () => spread.getSpreadMode(), + toggleSpreadMode: () => { + const current = spread.getSpreadMode(); + const nextMode = current === SpreadMode.None ? SpreadMode.Odd : SpreadMode.None; + spread.setSpreadMode(nextMode); + }, + SpreadMode, + }, + }); + + triggerImmediateSpreadUpdate(spreadMode); + }, [spread, spreadMode, registerBridge, triggerImmediateSpreadUpdate]); return null; } diff --git a/frontend/src/core/components/viewer/ZoomAPIBridge.tsx b/frontend/src/core/components/viewer/ZoomAPIBridge.tsx index 000bf47d9..9cec9e9da 100644 --- a/frontend/src/core/components/viewer/ZoomAPIBridge.tsx +++ b/frontend/src/core/components/viewer/ZoomAPIBridge.tsx @@ -1,68 +1,256 @@ -import { useEffect, useRef } from 'react'; -import { useZoom } from '@embedpdf/plugin-zoom/react'; +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 { useFileState } from '@app/contexts/FileContext'; +import { + determineAutoZoom, + DEFAULT_FALLBACK_ZOOM, + DEFAULT_VISIBILITY_THRESHOLD, + measureRenderedPageRect, + useFitWidthResize, + ZoomViewport, +} from '@core/utils/viewerZoom'; +import { getFirstPageAspectRatioFromStub } from '@core/utils/pageMetadata'; -/** - * Component that runs inside EmbedPDF context and manages zoom state locally - */ export function ZoomAPIBridge() { const { provides: zoom, state: zoomState } = useZoom(); + const { state: spreadState } = useSpread(); const { registerBridge, triggerImmediateZoomUpdate } = useViewer(); - const hasSetInitialZoom = useRef(false); + const { selectors } = useFileState(); + + const hasSetInitialZoom = useRef(false); + const lastSpreadMode = useRef(spreadState?.spreadMode); + const lastFileId = useRef(); + const lastAppliedZoom = useRef(null); + const [autoZoomTick, setAutoZoomTick] = useState(0); + + const scheduleAutoZoom = useCallback(() => { + hasSetInitialZoom.current = false; + lastAppliedZoom.current = null; + setAutoZoomTick((tick) => tick + 1); + }, []); + + const requestFitWidth = useCallback(() => { + if (zoom) { + zoom.requestZoom(ZoomMode.FitWidth, { vx: 0.5, vy: 0 }); + } + }, [zoom]); + + const stubs = selectors.getStirlingFileStubs(); + const firstFileStub = stubs[0]; + const firstFileId = firstFileStub?.id; - // Set initial zoom once when plugin is ready useEffect(() => { - if (!zoom || hasSetInitialZoom.current) { + if (!firstFileId) { + hasSetInitialZoom.current = false; + lastFileId.current = undefined; + lastAppliedZoom.current = null; return; } - let retryTimer: ReturnType | undefined; - const attemptInitialZoom = () => { - try { - zoom.requestZoom(1.4); - hasSetInitialZoom.current = true; - } catch (error) { - console.log('Zoom initialization delayed, viewport not ready:', error); - retryTimer = setTimeout(() => { - try { - zoom.requestZoom(1.4); - hasSetInitialZoom.current = true; - } catch (retryError) { - console.log('Zoom initialization failed:', retryError); - } - }, 200); - } - }; - - const timer = setTimeout(attemptInitialZoom, 50); - - return () => { - clearTimeout(timer); - if (retryTimer) { - clearTimeout(retryTimer); - } - }; - }, [zoom, zoomState]); + if (firstFileId !== lastFileId.current) { + lastFileId.current = firstFileId; + scheduleAutoZoom(); + } + }, [firstFileId, scheduleAutoZoom]); useEffect(() => { - if (zoom && zoomState) { - // Update local state - const currentZoomLevel = zoomState.currentZoomLevel ?? 1.4; - const newState = { - currentZoom: currentZoomLevel, - zoomPercent: Math.round(currentZoomLevel * 100), - }; + const currentSpreadMode = spreadState?.spreadMode ?? SpreadMode.None; + if (currentSpreadMode !== lastSpreadMode.current) { + lastSpreadMode.current = currentSpreadMode; - // Trigger immediate update for responsive UI - triggerImmediateZoomUpdate(newState.zoomPercent); - - // Register this bridge with ViewerContext - registerBridge('zoom', { - state: newState, - api: zoom - }); + const hadTrackedAutoZoom = lastAppliedZoom.current !== null; + const zoomLevel = zoomState?.zoomLevel; + if ( + zoomLevel === ZoomMode.FitWidth || + zoomLevel === ZoomMode.Automatic || + hadTrackedAutoZoom + ) { + requestFitWidth(); + scheduleAutoZoom(); + } } - }, [zoom, zoomState]); + }, [ + spreadState?.spreadMode, + zoomState?.zoomLevel, + scheduleAutoZoom, + requestFitWidth, + ]); + + const getViewportSnapshot = useCallback((): ZoomViewport | null => { + if (!zoomState || typeof zoomState !== 'object') { + return null; + } + + if ('viewport' in zoomState) { + const candidate = (zoomState as { viewport?: ZoomViewport | null }).viewport; + return candidate ?? null; + } + + return null; + }, [zoomState]); + + const isManagedZoom = + !!zoom && + (zoomState?.zoomLevel === ZoomMode.FitWidth || + zoomState?.zoomLevel === ZoomMode.Automatic || + lastAppliedZoom.current !== null); + + useFitWidthResize({ + isManaged: isManagedZoom, + requestFitWidth, + onDebouncedResize: scheduleAutoZoom, + }); + + useEffect(() => { + if (!zoom || !zoomState) { + return; + } + + if (!firstFileId) { + return; + } + + if (hasSetInitialZoom.current) { + return; + } + + if (zoomState.zoomLevel !== ZoomMode.FitWidth) { + if (zoomState.zoomLevel === ZoomMode.Automatic) { + requestFitWidth(); + } + return; + } + + const fitWidthZoom = zoomState.currentZoomLevel; + if (!fitWidthZoom || fitWidthZoom <= 0) { + return; + } + + const applyTrackedZoom = (level: number | ZoomMode, effectiveZoom: number) => { + zoom.requestZoom(level, { vx: 0.5, vy: 0 }); + lastAppliedZoom.current = effectiveZoom; + triggerImmediateZoomUpdate(Math.round(effectiveZoom * 100)); + hasSetInitialZoom.current = true; + }; + + let cancelled = false; + + const applyAutoZoom = async () => { + const spreadMode = spreadState?.spreadMode ?? SpreadMode.None; + const pagesPerSpread = spreadMode !== SpreadMode.None ? 2 : 1; + const metadataAspectRatio = getFirstPageAspectRatioFromStub(firstFileStub); + + const viewport = getViewportSnapshot(); + + if (cancelled) { + return; + } + + const metrics = viewport ?? {}; + const viewportWidth = + metrics.clientWidth ?? metrics.width ?? window.innerWidth ?? 0; + const viewportHeight = + metrics.clientHeight ?? metrics.height ?? window.innerHeight ?? 0; + + if (viewportWidth <= 0 || viewportHeight <= 0) { + return; + } + + const pageRect = await measureRenderedPageRect({ + shouldCancel: () => cancelled, + }); + if (cancelled) { + return; + } + + const decision = determineAutoZoom({ + viewportWidth, + viewportHeight, + fitWidthZoom, + pagesPerSpread, + pageRect: pageRect + ? { width: pageRect.width, height: pageRect.height } + : undefined, + metadataAspectRatio: metadataAspectRatio ?? null, + visibilityThreshold: DEFAULT_VISIBILITY_THRESHOLD, + fallbackZoom: DEFAULT_FALLBACK_ZOOM, + }); + + if (decision.type === 'fallback') { + applyTrackedZoom(decision.zoom, decision.zoom); + return; + } + + if (decision.type === 'fitWidth') { + applyTrackedZoom(ZoomMode.FitWidth, fitWidthZoom); + return; + } + + applyTrackedZoom(decision.zoom, decision.zoom); + }; + + applyAutoZoom(); + + return () => { + cancelled = true; + }; + }, [ + zoom, + zoomState, + firstFileId, + firstFileStub, + requestFitWidth, + getViewportSnapshot, + autoZoomTick, + spreadState?.spreadMode, + triggerImmediateZoomUpdate, + ]); + + useEffect(() => { + if (!zoom || typeof zoom.onZoomChange !== 'function') { + return; + } + + const unsubscribe = zoom.onZoomChange((event: { newZoom?: number }) => { + if (typeof event?.newZoom !== 'number') { + return; + } + lastAppliedZoom.current = event.newZoom; + triggerImmediateZoomUpdate(Math.round(event.newZoom * 100)); + }); + + return () => { + if (typeof unsubscribe === 'function') { + unsubscribe(); + } + }; + }, [zoom, triggerImmediateZoomUpdate]); + + useEffect(() => { + if (!zoom || !zoomState) { + return; + } + + const currentZoomLevel = + lastAppliedZoom.current ?? zoomState.currentZoomLevel ?? 1; + + const newState = { + currentZoom: currentZoomLevel, + zoomPercent: Math.round(currentZoomLevel * 100), + }; + + triggerImmediateZoomUpdate(newState.zoomPercent); + + registerBridge('zoom', { + state: newState, + api: zoom, + }); + }, [zoom, zoomState, registerBridge, triggerImmediateZoomUpdate]); return null; } + + + diff --git a/frontend/src/core/contexts/ViewerContext.tsx b/frontend/src/core/contexts/ViewerContext.tsx index 8e0bea44a..076cf86f7 100644 --- a/frontend/src/core/contexts/ViewerContext.tsx +++ b/frontend/src/core/contexts/ViewerContext.tsx @@ -1,4 +1,11 @@ -import React, { createContext, useContext, useState, ReactNode, useRef } from 'react'; +import React, { + createContext, + useContext, + useState, + ReactNode, + useRef, + useCallback, +} from 'react'; import { SpreadMode } from '@embedpdf/plugin-spread/react'; import { useNavigation } from '@app/contexts/NavigationContext'; @@ -109,6 +116,20 @@ interface BridgeRef { api: TApi; } +function useImmediateNotifier() { + const callbackRef = useRef<((...args: Args) => void) | null>(null); + + const register = useCallback((callback: (...args: Args) => void) => { + callbackRef.current = callback; + }, []); + + const trigger = useCallback((...args: Args) => { + callbackRef.current?.(...args); + }, []); + + return { register, trigger }; +} + /** * ViewerContext provides a unified interface to EmbedPDF functionality. * @@ -150,10 +171,12 @@ interface ViewerContextType { // Immediate update callbacks registerImmediateZoomUpdate: (callback: (percent: number) => void) => void; registerImmediateScrollUpdate: (callback: (currentPage: number, totalPages: number) => void) => void; + registerImmediateSpreadUpdate: (callback: (mode: SpreadMode, isDualPage: boolean) => void) => void; // Internal - for bridges to trigger immediate updates triggerImmediateScrollUpdate: (currentPage: number, totalPages: number) => void; triggerImmediateZoomUpdate: (zoomPercent: number) => void; + triggerImmediateSpreadUpdate: (mode: SpreadMode, isDualPage?: boolean) => void; // Action handlers - call EmbedPDF APIs directly scrollActions: { @@ -241,11 +264,39 @@ export const ViewerProvider: React.FC = ({ children }) => { export: null as BridgeRef | null, }); - // Immediate zoom callback for responsive display updates - const immediateZoomUpdateCallback = useRef<((percent: number) => void) | null>(null); + const { + register: registerImmediateZoomUpdate, + trigger: triggerImmediateZoomInternal, + } = useImmediateNotifier<[number]>(); + const { + register: registerImmediateScrollUpdate, + trigger: triggerImmediateScrollInternal, + } = useImmediateNotifier<[number, number]>(); + const { + register: registerImmediateSpreadUpdate, + trigger: triggerImmediateSpreadInternal, + } = useImmediateNotifier<[SpreadMode, boolean]>(); - // Immediate scroll callback for responsive display updates - const immediateScrollUpdateCallback = useRef<((currentPage: number, totalPages: number) => void) | null>(null); + const triggerImmediateZoomUpdate = useCallback( + (percent: number) => { + triggerImmediateZoomInternal(percent); + }, + [triggerImmediateZoomInternal] + ); + + const triggerImmediateScrollUpdate = useCallback( + (currentPage: number, totalPages: number) => { + triggerImmediateScrollInternal(currentPage, totalPages); + }, + [triggerImmediateScrollInternal] + ); + + const triggerImmediateSpreadUpdate = useCallback( + (mode: SpreadMode, isDualPage: boolean = mode !== SpreadMode.None) => { + triggerImmediateSpreadInternal(mode, isDualPage); + }, + [triggerImmediateSpreadInternal] + ); const registerBridge = (type: string, ref: BridgeRef) => { // Type-safe assignment - we know the bridges will provide correct types @@ -372,24 +423,18 @@ export const ViewerProvider: React.FC = ({ children }) => { zoomIn: () => { const api = bridgeRefs.current.zoom?.api; if (api?.zoomIn) { - // Update display immediately if callback is registered - if (immediateZoomUpdateCallback.current) { - const currentState = getZoomState(); - const newPercent = Math.min(Math.round(currentState.zoomPercent * 1.2), 300); - immediateZoomUpdateCallback.current(newPercent); - } + const currentState = getZoomState(); + const newPercent = Math.min(Math.round(currentState.zoomPercent * 1.2), 300); + triggerImmediateZoomUpdate(newPercent); api.zoomIn(); } }, zoomOut: () => { const api = bridgeRefs.current.zoom?.api; if (api?.zoomOut) { - // Update display immediately if callback is registered - if (immediateZoomUpdateCallback.current) { - const currentState = getZoomState(); - const newPercent = Math.max(Math.round(currentState.zoomPercent / 1.2), 20); - immediateZoomUpdateCallback.current(newPercent); - } + const currentState = getZoomState(); + const newPercent = Math.max(Math.round(currentState.zoomPercent / 1.2), 20); + triggerImmediateZoomUpdate(newPercent); api.zoomOut(); } }, @@ -550,26 +595,6 @@ export const ViewerProvider: React.FC = ({ children }) => { } }; - const registerImmediateZoomUpdate = (callback: (percent: number) => void) => { - immediateZoomUpdateCallback.current = callback; - }; - - const registerImmediateScrollUpdate = (callback: (currentPage: number, totalPages: number) => void) => { - immediateScrollUpdateCallback.current = callback; - }; - - const triggerImmediateScrollUpdate = (currentPage: number, totalPages: number) => { - if (immediateScrollUpdateCallback.current) { - immediateScrollUpdateCallback.current(currentPage, totalPages); - } - }; - - const triggerImmediateZoomUpdate = (zoomPercent: number) => { - if (immediateZoomUpdateCallback.current) { - immediateZoomUpdateCallback.current(zoomPercent); - } - }; - const value: ViewerContextType = { // UI state isThumbnailSidebarVisible, @@ -600,8 +625,10 @@ export const ViewerProvider: React.FC = ({ children }) => { // Immediate updates registerImmediateZoomUpdate, registerImmediateScrollUpdate, + registerImmediateSpreadUpdate, triggerImmediateScrollUpdate, triggerImmediateZoomUpdate, + triggerImmediateSpreadUpdate, // Actions scrollActions, diff --git a/frontend/src/core/contexts/file/fileActions.ts b/frontend/src/core/contexts/file/fileActions.ts index 5b4d1d2d9..0a37134f1 100644 --- a/frontend/src/core/contexts/file/fileActions.ts +++ b/frontend/src/core/contexts/file/fileActions.ts @@ -58,14 +58,21 @@ const addFilesMutex = new SimpleMutex(); /** * Helper to create ProcessedFile metadata structure */ -export function createProcessedFile(pageCount: number, thumbnail?: string, pageRotations?: number[]) { +export function createProcessedFile( + pageCount: number, + thumbnail?: string, + pageRotations?: number[], + pageDimensions?: Array<{ width: number; height: number }> +) { return { totalPages: pageCount, pages: Array.from({ length: pageCount }, (_, index) => ({ pageNumber: index + 1, thumbnail: index === 0 ? thumbnail : undefined, // Only first page gets thumbnail initially rotation: pageRotations?.[index] ?? 0, - splitBefore: false + splitBefore: false, + width: pageDimensions?.[index]?.width, + height: pageDimensions?.[index]?.height })), thumbnailUrl: thumbnail, lastProcessed: Date.now() @@ -92,7 +99,8 @@ export async function generateProcessedFileMetadata(file: File): Promise 0 ? page.width : null; + const height = + typeof page?.height === 'number' && page.height > 0 ? page.height : null; + + return { width, height }; +} + +export function getFirstPageDimensionsFromMetadata( + metadata?: ProcessedFileMetadata | null +): PageDimensions { + if (!metadata?.pages?.length) { + return { width: null, height: null }; + } + + return getPageDimensions(metadata.pages[0]); +} + +export function getFirstPageDimensionsFromStub( + file?: StirlingFileStub +): PageDimensions { + return getFirstPageDimensionsFromMetadata(file?.processedFile); +} + +export function getFirstPageAspectRatioFromMetadata( + metadata?: ProcessedFileMetadata | null +): number | null { + const { width, height } = getFirstPageDimensionsFromMetadata(metadata); + if (width && height) { + return height / width; + } + return null; +} + +export function getFirstPageAspectRatioFromStub( + file?: StirlingFileStub +): number | null { + return getFirstPageAspectRatioFromMetadata(file?.processedFile); +} diff --git a/frontend/src/core/utils/thumbnailUtils.ts b/frontend/src/core/utils/thumbnailUtils.ts index 8faec2644..88c4aeaef 100644 --- a/frontend/src/core/utils/thumbnailUtils.ts +++ b/frontend/src/core/utils/thumbnailUtils.ts @@ -4,6 +4,7 @@ export interface ThumbnailWithMetadata { thumbnail: string; // Always returns a thumbnail (placeholder if needed) pageCount: number; pageRotations?: number[]; // Rotation for each page (0, 90, 180, 270) + pageDimensions?: Array<{ width: number; height: number }>; } interface ColorScheme { @@ -402,12 +403,18 @@ export async function generateThumbnailWithMetadata(file: File, applyRotation: b const pageCount = pdf.numPages; const page = await pdf.getPage(1); + const pageDimensions: Array<{ width: number; height: number }> = []; // If applyRotation is false, render without rotation (for CSS-based rotation) // If applyRotation is true, let PDF.js apply rotation (for static display) const viewport = applyRotation ? page.getViewport({ scale }) : page.getViewport({ scale, rotation: 0 }); + const baseViewport = page.getViewport({ scale: 1, rotation: 0 }); + pageDimensions[0] = { + width: baseViewport.width, + height: baseViewport.height + }; const canvas = document.createElement("canvas"); canvas.width = viewport.width; @@ -428,10 +435,17 @@ export async function generateThumbnailWithMetadata(file: File, applyRotation: b const p = await pdf.getPage(i); const rotation = p.rotate || 0; pageRotations.push(rotation); + if (!pageDimensions[i - 1]) { + const pageViewport = p.getViewport({ scale: 1, rotation: 0 }); + pageDimensions[i - 1] = { + width: pageViewport.width, + height: pageViewport.height + }; + } } pdfWorkerManager.destroyDocument(pdf); - return { thumbnail, pageCount, pageRotations }; + return { thumbnail, pageCount, pageRotations, pageDimensions }; } catch (error) { if (error instanceof Error && error.name === "PasswordException") { diff --git a/frontend/src/core/utils/viewerZoom.ts b/frontend/src/core/utils/viewerZoom.ts new file mode 100644 index 000000000..d185398c0 --- /dev/null +++ b/frontend/src/core/utils/viewerZoom.ts @@ -0,0 +1,188 @@ +import { useEffect, useRef } from 'react'; + +export const DEFAULT_VISIBILITY_THRESHOLD = 80; // Require at least 80% of the page height to be visible +export const DEFAULT_FALLBACK_ZOOM = 1.44; // 144% fallback when no reliable metadata is present + +export interface ZoomViewport { + clientWidth?: number; + clientHeight?: number; + width?: number; + height?: number; +} + +export type AutoZoomDecision = + | { type: 'fallback'; zoom: number } + | { type: 'fitWidth' } + | { type: 'adjust'; zoom: number }; + +export interface AutoZoomParams { + viewportWidth: number; + viewportHeight: number; + fitWidthZoom: number; + pagesPerSpread: number; + pageRect?: { width: number; height: number } | null; + metadataAspectRatio?: number | null; + visibilityThreshold?: number; + fallbackZoom?: number; +} + +export function determineAutoZoom({ + viewportWidth, + viewportHeight, + fitWidthZoom, + pagesPerSpread, + pageRect, + metadataAspectRatio, + visibilityThreshold = DEFAULT_VISIBILITY_THRESHOLD, + fallbackZoom = DEFAULT_FALLBACK_ZOOM, +}: AutoZoomParams): AutoZoomDecision { + const rectWidth = pageRect?.width ?? 0; + const rectHeight = pageRect?.height ?? 0; + + const aspectRatio: number | null = + rectWidth > 0 ? rectHeight / rectWidth : metadataAspectRatio ?? null; + + let renderedHeight: number | null = rectHeight > 0 ? rectHeight : null; + + if (!renderedHeight || renderedHeight <= 0) { + if (aspectRatio == null || aspectRatio <= 0) { + return { type: 'fallback', zoom: Math.min(fitWidthZoom, fallbackZoom) }; + } + + const pageWidth = viewportWidth / (fitWidthZoom * pagesPerSpread); + const pageHeight = pageWidth * aspectRatio; + renderedHeight = pageHeight * fitWidthZoom; + } + + if (!renderedHeight || renderedHeight <= 0) { + return { type: 'fitWidth' }; + } + + const isLandscape = aspectRatio !== null && aspectRatio < 1; + const targetVisibility = isLandscape ? 100 : visibilityThreshold; + + const visiblePercent = (viewportHeight / renderedHeight) * 100; + + if (visiblePercent >= targetVisibility) { + return { type: 'fitWidth' }; + } + + const allowableHeightRatio = targetVisibility / 100; + const zoomScale = + viewportHeight / (allowableHeightRatio * renderedHeight); + const targetZoom = Math.min(fitWidthZoom, fitWidthZoom * zoomScale); + + if (Math.abs(targetZoom - fitWidthZoom) < 0.001) { + return { type: 'fitWidth' }; + } + + return { type: 'adjust', zoom: targetZoom }; +} + +export interface MeasurePageRectOptions { + selector?: string; + maxAttempts?: number; + shouldCancel?: () => boolean; +} + +export async function measureRenderedPageRect({ + selector = '[data-page-index="0"]', + maxAttempts = 12, + shouldCancel, +}: MeasurePageRectOptions = {}): Promise { + if (typeof window === 'undefined' || typeof document === 'undefined') { + return null; + } + + let rafId: number | null = null; + + const waitForNextFrame = () => + new Promise((resolve) => { + rafId = window.requestAnimationFrame(() => { + rafId = null; + resolve(); + }); + }); + + try { + for (let attempt = 0; attempt < maxAttempts; attempt++) { + if (shouldCancel?.()) { + return null; + } + + const element = document.querySelector(selector) as HTMLElement | null; + + if (element) { + const rect = element.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) { + return rect; + } + } + + await waitForNextFrame(); + } + } finally { + if (rafId !== null) { + window.cancelAnimationFrame(rafId); + } + } + + return null; +} + +export interface FitWidthResizeOptions { + isManaged: boolean; + requestFitWidth: () => void; + onDebouncedResize: () => void; + debounceMs?: number; +} + +export function useFitWidthResize({ + isManaged, + requestFitWidth, + onDebouncedResize, + debounceMs = 150, +}: FitWidthResizeOptions): void { + const managedRef = useRef(isManaged); + const requestFitWidthRef = useRef(requestFitWidth); + const onDebouncedResizeRef = useRef(onDebouncedResize); + + useEffect(() => { + managedRef.current = isManaged; + }, [isManaged]); + + useEffect(() => { + requestFitWidthRef.current = requestFitWidth; + }, [requestFitWidth]); + + useEffect(() => { + onDebouncedResizeRef.current = onDebouncedResize; + }, [onDebouncedResize]); + + useEffect(() => { + let timeoutId: number | undefined; + + const handleResize = () => { + if (!managedRef.current) { + return; + } + + if (typeof timeoutId === 'number') { + window.clearTimeout(timeoutId); + } + + timeoutId = window.setTimeout(() => { + requestFitWidthRef.current?.(); + onDebouncedResizeRef.current?.(); + }, debounceMs); + }; + + window.addEventListener('resize', handleResize); + return () => { + if (typeof timeoutId === 'number') { + window.clearTimeout(timeoutId); + } + window.removeEventListener('resize', handleResize); + }; + }, [debounceMs]); +}