mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Viewer update and autozoom (#4800)
Updated embed PDF Added Autozoom Added file page size to metadata for use in calculations for autozoom, will come in handy elsewhere. --------- Co-authored-by: James Brunton <jbrunton96@gmail.com>
This commit is contained in:
parent
3cf89b6ede
commit
ce6b2460d8
84
frontend/package-lock.json
generated
84
frontend/package-lock.json
generated
@ -10,24 +10,24 @@
|
|||||||
"license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE",
|
"license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||||
"@embedpdf/core": "^1.3.14",
|
"@embedpdf/core": "^1.4.1",
|
||||||
"@embedpdf/engines": "^1.3.14",
|
"@embedpdf/engines": "^1.4.1",
|
||||||
"@embedpdf/plugin-annotation": "^1.3.14",
|
"@embedpdf/plugin-annotation": "^1.4.1",
|
||||||
"@embedpdf/plugin-export": "^1.3.14",
|
"@embedpdf/plugin-export": "^1.4.1",
|
||||||
"@embedpdf/plugin-history": "^1.3.14",
|
"@embedpdf/plugin-history": "^1.4.1",
|
||||||
"@embedpdf/plugin-interaction-manager": "^1.3.14",
|
"@embedpdf/plugin-interaction-manager": "^1.4.1",
|
||||||
"@embedpdf/plugin-loader": "^1.3.14",
|
"@embedpdf/plugin-loader": "^1.4.1",
|
||||||
"@embedpdf/plugin-pan": "^1.3.14",
|
"@embedpdf/plugin-pan": "^1.4.1",
|
||||||
"@embedpdf/plugin-render": "^1.3.14",
|
"@embedpdf/plugin-render": "^1.4.1",
|
||||||
"@embedpdf/plugin-rotate": "^1.3.14",
|
"@embedpdf/plugin-rotate": "^1.4.1",
|
||||||
"@embedpdf/plugin-scroll": "^1.3.14",
|
"@embedpdf/plugin-scroll": "^1.4.1",
|
||||||
"@embedpdf/plugin-search": "^1.3.14",
|
"@embedpdf/plugin-search": "^1.4.1",
|
||||||
"@embedpdf/plugin-selection": "^1.3.14",
|
"@embedpdf/plugin-selection": "^1.4.1",
|
||||||
"@embedpdf/plugin-spread": "^1.3.14",
|
"@embedpdf/plugin-spread": "^1.4.1",
|
||||||
"@embedpdf/plugin-thumbnail": "^1.3.14",
|
"@embedpdf/plugin-thumbnail": "^1.4.1",
|
||||||
"@embedpdf/plugin-tiling": "^1.3.14",
|
"@embedpdf/plugin-tiling": "^1.4.1",
|
||||||
"@embedpdf/plugin-viewport": "^1.3.14",
|
"@embedpdf/plugin-viewport": "^1.4.1",
|
||||||
"@embedpdf/plugin-zoom": "^1.3.14",
|
"@embedpdf/plugin-zoom": "^1.4.1",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
@ -441,7 +441,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@ -488,7 +487,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@ -512,7 +510,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.4.1.tgz",
|
||||||
"integrity": "sha512-TGpxn2CvAKRnOJWJ3bsK+dKBiCp75ehxftRUmv7wAmPomhnG5XrDfoWJungvO+zbbqAwso6PocdeXINVt3hlAw==",
|
"integrity": "sha512-TGpxn2CvAKRnOJWJ3bsK+dKBiCp75ehxftRUmv7wAmPomhnG5XrDfoWJungvO+zbbqAwso6PocdeXINVt3hlAw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/engines": "1.4.1",
|
"@embedpdf/engines": "1.4.1",
|
||||||
"@embedpdf/models": "1.4.1"
|
"@embedpdf/models": "1.4.1"
|
||||||
@ -596,7 +593,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.4.1.tgz",
|
||||||
"integrity": "sha512-5WLDiNMH6tACkLGGv/lJtNsDeozOhSbrh0mjD1btHun8u7Yscu/Vf8tdJRUOsd+nULivo2nQ2NFNKu0OTbVo8w==",
|
"integrity": "sha512-5WLDiNMH6tACkLGGv/lJtNsDeozOhSbrh0mjD1btHun8u7Yscu/Vf8tdJRUOsd+nULivo2nQ2NFNKu0OTbVo8w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "1.4.1"
|
"@embedpdf/models": "1.4.1"
|
||||||
},
|
},
|
||||||
@ -613,7 +609,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.4.1.tgz",
|
||||||
"integrity": "sha512-Ng02S9SFIAi9JZS5rI+NXSnZZ1Yk9YYRw4MlN2pig49qOyivZdz0oScZaYxQPewo8ccJkLeghjdeWswOBW/6cA==",
|
"integrity": "sha512-Ng02S9SFIAi9JZS5rI+NXSnZZ1Yk9YYRw4MlN2pig49qOyivZdz0oScZaYxQPewo8ccJkLeghjdeWswOBW/6cA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "1.4.1"
|
"@embedpdf/models": "1.4.1"
|
||||||
},
|
},
|
||||||
@ -631,7 +626,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.4.1.tgz",
|
||||||
"integrity": "sha512-m3ZOk8JygsLxoa4cZ+0BVB5pfRWuBCg2/gPqjhoFZNKTqAFw4J6HGUrhYKg94GRYe+w1cTJl/NbTBYuU5DOrsA==",
|
"integrity": "sha512-m3ZOk8JygsLxoa4cZ+0BVB5pfRWuBCg2/gPqjhoFZNKTqAFw4J6HGUrhYKg94GRYe+w1cTJl/NbTBYuU5DOrsA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "1.4.1"
|
"@embedpdf/models": "1.4.1"
|
||||||
},
|
},
|
||||||
@ -668,7 +662,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.4.1.tgz",
|
||||||
"integrity": "sha512-gKCdNKw6WBHBEpTc2DLBWIWOxzsNnaNbpfeY6C4f2Bum0EO+XW3Hl2oIx1uaRHjIhhnXso1J3QweqelsPwDGwg==",
|
"integrity": "sha512-gKCdNKw6WBHBEpTc2DLBWIWOxzsNnaNbpfeY6C4f2Bum0EO+XW3Hl2oIx1uaRHjIhhnXso1J3QweqelsPwDGwg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "1.4.1"
|
"@embedpdf/models": "1.4.1"
|
||||||
},
|
},
|
||||||
@ -703,7 +696,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.4.1.tgz",
|
||||||
"integrity": "sha512-Y9O+matB4j4fLim5s/jn7qIi+lMC9vmDJRpJhiWe8bvD9oYLP2xfD/DdhFgAjRKcNhPoxC+j8q8QN5BMeGAv2Q==",
|
"integrity": "sha512-Y9O+matB4j4fLim5s/jn7qIi+lMC9vmDJRpJhiWe8bvD9oYLP2xfD/DdhFgAjRKcNhPoxC+j8q8QN5BMeGAv2Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "1.4.1"
|
"@embedpdf/models": "1.4.1"
|
||||||
},
|
},
|
||||||
@ -740,7 +732,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.4.1.tgz",
|
||||||
"integrity": "sha512-lo5Ytk1PH0PrRKv6zKVupm4t02VGsqIrnSIeP6NO8Ujx0wfqEhj//sqIuO/EwfFVJD8lcQIP9UUo9y8baCrEog==",
|
"integrity": "sha512-lo5Ytk1PH0PrRKv6zKVupm4t02VGsqIrnSIeP6NO8Ujx0wfqEhj//sqIuO/EwfFVJD8lcQIP9UUo9y8baCrEog==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "1.4.1"
|
"@embedpdf/models": "1.4.1"
|
||||||
},
|
},
|
||||||
@ -816,7 +807,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.4.1.tgz",
|
||||||
"integrity": "sha512-+TgFHKPCLTBiDYe2DdsmTS37hwQgcZ3dYIc7bE0l5cp+GVwouu1h0MTmjL+90loizeWwCiu10E/zXR6hz+CUaQ==",
|
"integrity": "sha512-+TgFHKPCLTBiDYe2DdsmTS37hwQgcZ3dYIc7bE0l5cp+GVwouu1h0MTmjL+90loizeWwCiu10E/zXR6hz+CUaQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@embedpdf/models": "1.4.1"
|
"@embedpdf/models": "1.4.1"
|
||||||
},
|
},
|
||||||
@ -972,7 +962,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
||||||
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.18.3",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
@ -1016,7 +1005,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
|
||||||
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
|
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.18.3",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
@ -2047,7 +2035,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.5.tgz",
|
||||||
"integrity": "sha512-PdVNLMgOS2vFhOujRi6/VC9ic8w3UDyKX7ftwDeJ7yQT8CiepUxfbWWYpVpnq23bdWh/7fIT2Pn1EY8r8GOk7g==",
|
"integrity": "sha512-PdVNLMgOS2vFhOujRi6/VC9ic8w3UDyKX7ftwDeJ7yQT8CiepUxfbWWYpVpnq23bdWh/7fIT2Pn1EY8r8GOk7g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/react": "^0.27.16",
|
"@floating-ui/react": "^0.27.16",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@ -2098,7 +2085,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.5.tgz",
|
||||||
"integrity": "sha512-0Wf08eWLKi3WkKlxnV1W5vfuN6wcvAV2VbhQlOy0R9nrWorGTtonQF6qqBE3PnJFYF1/ZE+HkYZQ/Dr7DmYSMQ==",
|
"integrity": "sha512-0Wf08eWLKi3WkKlxnV1W5vfuN6wcvAV2VbhQlOy0R9nrWorGTtonQF6qqBE3PnJFYF1/ZE+HkYZQ/Dr7DmYSMQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.x || ^19.x"
|
"react": "^18.x || ^19.x"
|
||||||
}
|
}
|
||||||
@ -2166,7 +2152,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz",
|
||||||
"integrity": "sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==",
|
"integrity": "sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.4",
|
"@babel/runtime": "^7.28.4",
|
||||||
"@mui/core-downloads-tracker": "^7.3.4",
|
"@mui/core-downloads-tracker": "^7.3.4",
|
||||||
@ -3850,7 +3835,6 @@
|
|||||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.10.4",
|
"@babel/code-frame": "^7.10.4",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
@ -4174,7 +4158,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@ -4185,7 +4168,6 @@
|
|||||||
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
@ -4246,7 +4228,6 @@
|
|||||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.46.2",
|
"@typescript-eslint/scope-manager": "8.46.2",
|
||||||
"@typescript-eslint/types": "8.46.2",
|
"@typescript-eslint/types": "8.46.2",
|
||||||
@ -4960,6 +4941,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz",
|
||||||
"integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
|
"integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/shared": "3.5.22"
|
"@vue/shared": "3.5.22"
|
||||||
}
|
}
|
||||||
@ -4969,6 +4951,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
|
||||||
"integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
|
"integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.22",
|
"@vue/reactivity": "3.5.22",
|
||||||
"@vue/shared": "3.5.22"
|
"@vue/shared": "3.5.22"
|
||||||
@ -4979,6 +4962,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
|
||||||
"integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
|
"integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.22",
|
"@vue/reactivity": "3.5.22",
|
||||||
"@vue/runtime-core": "3.5.22",
|
"@vue/runtime-core": "3.5.22",
|
||||||
@ -4991,6 +4975,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
|
||||||
"integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
|
"integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-ssr": "3.5.22",
|
"@vue/compiler-ssr": "3.5.22",
|
||||||
"@vue/shared": "3.5.22"
|
"@vue/shared": "3.5.22"
|
||||||
@ -5017,7 +5002,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@ -5702,7 +5686,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.19",
|
"baseline-browser-mapping": "^2.8.19",
|
||||||
"caniuse-lite": "^1.0.30001751",
|
"caniuse-lite": "^1.0.30001751",
|
||||||
@ -6748,8 +6731,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz",
|
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz",
|
||||||
"integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==",
|
"integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/dezalgo": {
|
"node_modules/dezalgo": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
@ -7144,7 +7126,6 @@
|
|||||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@ -7315,7 +7296,6 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@ -8638,7 +8618,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.6"
|
"@babel/runtime": "^7.27.6"
|
||||||
},
|
},
|
||||||
@ -9446,7 +9425,6 @@
|
|||||||
"integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==",
|
"integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asamuzakjp/dom-selector": "^6.7.2",
|
"@asamuzakjp/dom-selector": "^6.7.2",
|
||||||
"cssstyle": "^5.3.1",
|
"cssstyle": "^5.3.1",
|
||||||
@ -11223,7 +11201,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@ -11503,7 +11480,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
|
||||||
"integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
|
"integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/preact"
|
"url": "https://opencollective.com/preact"
|
||||||
@ -11876,7 +11852,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -11886,7 +11861,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@ -13321,9 +13295,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte": {
|
"node_modules/svelte": {
|
||||||
"version": "5.42.3",
|
"version": "5.43.0",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.42.3.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.0.tgz",
|
||||||
"integrity": "sha512-+8dUmdJGvKSWEfbAgIaUmpD97s1bBAGxEf6s7wQonk+HNdMmrBZtpStzRypRqrYBFUmmhaUgBHUjraE8gLqWAw==",
|
"integrity": "sha512-1sRxVbgJAB+UGzwkc3GUoiBSzEOf0jqzccMaVoI2+pI+kASUe9qubslxace8+Mzhqw19k4syTA5niCIJwfXpOA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -13557,7 +13531,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -13859,7 +13832,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@ -13942,7 +13914,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"napi-postinstall": "^0.3.0"
|
"napi-postinstall": "^0.3.0"
|
||||||
},
|
},
|
||||||
@ -14147,7 +14118,6 @@
|
|||||||
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@ -14299,7 +14269,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -14313,7 +14282,6 @@
|
|||||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/expect": "3.2.4",
|
"@vitest/expect": "3.2.4",
|
||||||
|
|||||||
@ -6,24 +6,24 @@
|
|||||||
"proxy": "http://localhost:8080",
|
"proxy": "http://localhost:8080",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||||
"@embedpdf/core": "^1.3.14",
|
"@embedpdf/core": "^1.4.1",
|
||||||
"@embedpdf/engines": "^1.3.14",
|
"@embedpdf/engines": "^1.4.1",
|
||||||
"@embedpdf/plugin-annotation": "^1.3.14",
|
"@embedpdf/plugin-annotation": "^1.4.1",
|
||||||
"@embedpdf/plugin-export": "^1.3.14",
|
"@embedpdf/plugin-export": "^1.4.1",
|
||||||
"@embedpdf/plugin-history": "^1.3.14",
|
"@embedpdf/plugin-history": "^1.4.1",
|
||||||
"@embedpdf/plugin-interaction-manager": "^1.3.14",
|
"@embedpdf/plugin-interaction-manager": "^1.4.1",
|
||||||
"@embedpdf/plugin-loader": "^1.3.14",
|
"@embedpdf/plugin-loader": "^1.4.1",
|
||||||
"@embedpdf/plugin-pan": "^1.3.14",
|
"@embedpdf/plugin-pan": "^1.4.1",
|
||||||
"@embedpdf/plugin-render": "^1.3.14",
|
"@embedpdf/plugin-render": "^1.4.1",
|
||||||
"@embedpdf/plugin-rotate": "^1.3.14",
|
"@embedpdf/plugin-rotate": "^1.4.1",
|
||||||
"@embedpdf/plugin-scroll": "^1.3.14",
|
"@embedpdf/plugin-scroll": "^1.4.1",
|
||||||
"@embedpdf/plugin-search": "^1.3.14",
|
"@embedpdf/plugin-search": "^1.4.1",
|
||||||
"@embedpdf/plugin-selection": "^1.3.14",
|
"@embedpdf/plugin-selection": "^1.4.1",
|
||||||
"@embedpdf/plugin-spread": "^1.3.14",
|
"@embedpdf/plugin-spread": "^1.4.1",
|
||||||
"@embedpdf/plugin-thumbnail": "^1.3.14",
|
"@embedpdf/plugin-thumbnail": "^1.4.1",
|
||||||
"@embedpdf/plugin-tiling": "^1.3.14",
|
"@embedpdf/plugin-tiling": "^1.4.1",
|
||||||
"@embedpdf/plugin-viewport": "^1.3.14",
|
"@embedpdf/plugin-viewport": "^1.4.1",
|
||||||
"@embedpdf/plugin-zoom": "^1.3.14",
|
"@embedpdf/plugin-zoom": "^1.4.1",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@tauri-apps/api": "^2.5.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
"@tauri-apps/plugin-fs": "^2.4.0",
|
"@tauri-apps/plugin-fs": "^2.4.0",
|
||||||
|
|||||||
@ -35,14 +35,12 @@ const EmbedPdfViewerContent = ({
|
|||||||
const viewerRef = React.useRef<HTMLDivElement>(null);
|
const viewerRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
|
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
|
// Register viewer right-rail buttons
|
||||||
useViewerRightRailButtons();
|
useViewerRightRailButtons();
|
||||||
|
|
||||||
const scrollState = getScrollState();
|
const scrollState = getScrollState();
|
||||||
const zoomState = getZoomState();
|
|
||||||
const spreadState = getSpreadState();
|
|
||||||
const rotationState = getRotationState();
|
const rotationState = getRotationState();
|
||||||
|
|
||||||
// Track initial rotation to detect changes
|
// Track initial rotation to detect changes
|
||||||
@ -320,15 +318,6 @@ const EmbedPdfViewerContent = ({
|
|||||||
<PdfViewerToolbar
|
<PdfViewerToolbar
|
||||||
currentPage={scrollState.currentPage}
|
currentPage={scrollState.currentPage}
|
||||||
totalPages={scrollState.totalPages}
|
totalPages={scrollState.totalPages}
|
||||||
onPageChange={(page) => {
|
|
||||||
// Page navigation handled by scrollActions
|
|
||||||
console.log('Navigate to page:', page);
|
|
||||||
}}
|
|
||||||
dualPage={spreadState.isDualPage}
|
|
||||||
onDualPageToggle={() => {
|
|
||||||
spreadActions.toggleSpreadMode();
|
|
||||||
}}
|
|
||||||
currentZoom={zoomState.zoomPercent}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { Viewport, ViewportPluginPackage } from '@embedpdf/plugin-viewport/react
|
|||||||
import { Scroller, ScrollPluginPackage, ScrollStrategy } from '@embedpdf/plugin-scroll/react';
|
import { Scroller, ScrollPluginPackage, ScrollStrategy } from '@embedpdf/plugin-scroll/react';
|
||||||
import { LoaderPluginPackage } from '@embedpdf/plugin-loader/react';
|
import { LoaderPluginPackage } from '@embedpdf/plugin-loader/react';
|
||||||
import { RenderPluginPackage } from '@embedpdf/plugin-render/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 { InteractionManagerPluginPackage, PagePointerProvider, GlobalPointerProvider } from '@embedpdf/plugin-interaction-manager/react';
|
||||||
import { SelectionLayer, SelectionPluginPackage } from '@embedpdf/plugin-selection/react';
|
import { SelectionLayer, SelectionPluginPackage } from '@embedpdf/plugin-selection/react';
|
||||||
import { TilingLayer, TilingPluginPackage } from '@embedpdf/plugin-tiling/react';
|
import { TilingLayer, TilingPluginPackage } from '@embedpdf/plugin-tiling/react';
|
||||||
@ -115,9 +115,9 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
|
|||||||
|
|
||||||
// Register zoom plugin with configuration
|
// Register zoom plugin with configuration
|
||||||
createPluginRegistration(ZoomPluginPackage, {
|
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,
|
minZoom: 0.2,
|
||||||
maxZoom: 3.0,
|
maxZoom: 5.0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Register tiling plugin (depends on Render, Scroll, Viewport)
|
// Register tiling plugin (depends on Render, Scroll, Viewport)
|
||||||
@ -287,6 +287,8 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
|
|||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
contain: 'strict',
|
contain: 'strict',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Scroller
|
<Scroller
|
||||||
@ -295,6 +297,9 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
|
|||||||
<Rotate key={document?.id} pageSize={{ width, height }}>
|
<Rotate key={document?.id} pageSize={{ width, height }}>
|
||||||
<PagePointerProvider pageIndex={pageIndex} pageWidth={width} pageHeight={height} scale={scale} rotation={rotation}>
|
<PagePointerProvider pageIndex={pageIndex} pageWidth={width} pageHeight={height} scale={scale} rotation={rotation}>
|
||||||
<div
|
<div
|
||||||
|
data-page-index={pageIndex}
|
||||||
|
data-page-width={width}
|
||||||
|
data-page-height={height}
|
||||||
style={{
|
style={{
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
|||||||
@ -14,30 +14,32 @@ interface PdfViewerToolbarProps {
|
|||||||
currentPage?: number;
|
currentPage?: number;
|
||||||
totalPages?: number;
|
totalPages?: number;
|
||||||
onPageChange?: (page: number) => void;
|
onPageChange?: (page: number) => void;
|
||||||
|
|
||||||
// Dual page toggle (placeholder for now)
|
|
||||||
dualPage?: boolean;
|
|
||||||
onDualPageToggle?: () => void;
|
|
||||||
|
|
||||||
// Zoom controls (connected via ViewerContext)
|
|
||||||
currentZoom?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PdfViewerToolbar({
|
export function PdfViewerToolbar({
|
||||||
currentPage = 1,
|
currentPage = 1,
|
||||||
totalPages: _totalPages = 1,
|
totalPages: _totalPages = 1,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
dualPage = false,
|
|
||||||
onDualPageToggle,
|
|
||||||
currentZoom: _currentZoom = 100,
|
|
||||||
}: PdfViewerToolbarProps) {
|
}: PdfViewerToolbarProps) {
|
||||||
const { t } = useTranslation();
|
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 scrollState = getScrollState();
|
||||||
const zoomState = getZoomState();
|
const zoomState = getZoomState();
|
||||||
|
const spreadState = getSpreadState();
|
||||||
const [pageInput, setPageInput] = useState(scrollState.currentPage || currentPage);
|
const [pageInput, setPageInput] = useState(scrollState.currentPage || currentPage);
|
||||||
const [displayZoomPercent, setDisplayZoomPercent] = useState(zoomState.zoomPercent || 140);
|
const [displayZoomPercent, setDisplayZoomPercent] = useState(zoomState.zoomPercent || 140);
|
||||||
|
const [isDualPageActive, setIsDualPageActive] = useState(spreadState.isDualPage);
|
||||||
|
|
||||||
// Register for immediate scroll updates and sync with actual scroll state
|
// Register for immediate scroll updates and sync with actual scroll state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -53,6 +55,13 @@ export function PdfViewerToolbar({
|
|||||||
setDisplayZoomPercent(zoomState.zoomPercent || 140);
|
setDisplayZoomPercent(zoomState.zoomPercent || 140);
|
||||||
}, [zoomState.zoomPercent, registerImmediateZoomUpdate]);
|
}, [zoomState.zoomPercent, registerImmediateZoomUpdate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
registerImmediateSpreadUpdate((_mode, isDual) => {
|
||||||
|
setIsDualPageActive(isDual);
|
||||||
|
});
|
||||||
|
setIsDualPageActive(spreadState.isDualPage);
|
||||||
|
}, [registerImmediateSpreadUpdate, spreadState.isDualPage]);
|
||||||
|
|
||||||
const handleZoomOut = () => {
|
const handleZoomOut = () => {
|
||||||
zoomActions.zoomOut();
|
zoomActions.zoomOut();
|
||||||
};
|
};
|
||||||
@ -69,6 +78,10 @@ export function PdfViewerToolbar({
|
|||||||
setPageInput(page);
|
setPageInput(page);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDualPageToggle = () => {
|
||||||
|
spreadActions.toggleSpreadMode();
|
||||||
|
};
|
||||||
|
|
||||||
const handleFirstPage = () => {
|
const handleFirstPage = () => {
|
||||||
scrollActions.scrollToFirstPage();
|
scrollActions.scrollToFirstPage();
|
||||||
};
|
};
|
||||||
@ -188,15 +201,19 @@ export function PdfViewerToolbar({
|
|||||||
|
|
||||||
{/* Dual Page Toggle */}
|
{/* Dual Page Toggle */}
|
||||||
<Button
|
<Button
|
||||||
variant={dualPage ? "filled" : "light"}
|
variant={isDualPageActive ? "filled" : "light"}
|
||||||
color="blue"
|
color="blue"
|
||||||
size="md"
|
size="md"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
onClick={onDualPageToggle}
|
onClick={handleDualPageToggle}
|
||||||
style={{ minWidth: '2.5rem' }}
|
style={{ minWidth: '2.5rem' }}
|
||||||
title={dualPage ? t("viewer.singlePageView", "Single Page View") : t("viewer.dualPageView", "Dual Page View")}
|
title={
|
||||||
|
isDualPageActive
|
||||||
|
? t("viewer.singlePageView", "Single Page View")
|
||||||
|
: t("viewer.dualPageView", "Dual Page View")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{dualPage ? <DescriptionIcon fontSize="small" /> : <ViewWeekIcon fontSize="small" />}
|
{isDualPageActive ? <DescriptionIcon fontSize="small" /> : <ViewWeekIcon fontSize="small" />}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Zoom Controls */}
|
{/* Zoom Controls */}
|
||||||
|
|||||||
@ -7,33 +7,36 @@ import { useViewer } from '@app/contexts/ViewerContext';
|
|||||||
*/
|
*/
|
||||||
export function SpreadAPIBridge() {
|
export function SpreadAPIBridge() {
|
||||||
const { provides: spread, spreadMode } = useSpread();
|
const { provides: spread, spreadMode } = useSpread();
|
||||||
const { registerBridge } = useViewer();
|
const { registerBridge, triggerImmediateSpreadUpdate } = useViewer();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (spread) {
|
if (!spread) {
|
||||||
const newState = {
|
return;
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,68 +1,246 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useZoom } from '@embedpdf/plugin-zoom/react';
|
import { useZoom, ZoomMode } from '@embedpdf/plugin-zoom/react';
|
||||||
|
import { useSpread, SpreadMode } from '@embedpdf/plugin-spread/react';
|
||||||
import { useViewer } from '@app/contexts/ViewerContext';
|
import { useViewer } from '@app/contexts/ViewerContext';
|
||||||
|
import { useFileState } from '@app/contexts/FileContext';
|
||||||
|
import {
|
||||||
|
determineAutoZoom,
|
||||||
|
DEFAULT_FALLBACK_ZOOM,
|
||||||
|
DEFAULT_VISIBILITY_THRESHOLD,
|
||||||
|
measureRenderedPageRect,
|
||||||
|
useFitWidthResize,
|
||||||
|
ZoomViewport,
|
||||||
|
} from '@app/utils/viewerZoom';
|
||||||
|
import { getFirstPageAspectRatioFromStub } from '@app/utils/pageMetadata';
|
||||||
|
|
||||||
/**
|
|
||||||
* Component that runs inside EmbedPDF context and manages zoom state locally
|
|
||||||
*/
|
|
||||||
export function ZoomAPIBridge() {
|
export function ZoomAPIBridge() {
|
||||||
const { provides: zoom, state: zoomState } = useZoom();
|
const { provides: zoom, state: zoomState } = useZoom();
|
||||||
|
const { spreadMode } = useSpread();
|
||||||
const { registerBridge, triggerImmediateZoomUpdate } = useViewer();
|
const { registerBridge, triggerImmediateZoomUpdate } = useViewer();
|
||||||
const hasSetInitialZoom = useRef(false);
|
const { selectors } = useFileState();
|
||||||
|
|
||||||
|
const hasSetInitialZoom = useRef(false);
|
||||||
|
const lastSpreadMode = useRef(spreadMode ?? SpreadMode.None);
|
||||||
|
const lastFileId = useRef<string | undefined>(undefined);
|
||||||
|
const lastAppliedZoom = useRef<number | null>(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(() => {
|
useEffect(() => {
|
||||||
if (!zoom || hasSetInitialZoom.current) {
|
if (!firstFileId) {
|
||||||
|
hasSetInitialZoom.current = false;
|
||||||
|
lastFileId.current = undefined;
|
||||||
|
lastAppliedZoom.current = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
if (firstFileId !== lastFileId.current) {
|
||||||
const attemptInitialZoom = () => {
|
lastFileId.current = firstFileId;
|
||||||
try {
|
scheduleAutoZoom();
|
||||||
zoom.requestZoom(1.4);
|
}
|
||||||
hasSetInitialZoom.current = true;
|
}, [firstFileId, scheduleAutoZoom]);
|
||||||
} 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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (zoom && zoomState) {
|
const currentSpreadMode = spreadMode ?? SpreadMode.None;
|
||||||
// Update local state
|
if (currentSpreadMode !== lastSpreadMode.current) {
|
||||||
const currentZoomLevel = zoomState.currentZoomLevel ?? 1.4;
|
lastSpreadMode.current = currentSpreadMode;
|
||||||
const newState = {
|
|
||||||
currentZoom: currentZoomLevel,
|
|
||||||
zoomPercent: Math.round(currentZoomLevel * 100),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Trigger immediate update for responsive UI
|
const hadTrackedAutoZoom = lastAppliedZoom.current !== null;
|
||||||
triggerImmediateZoomUpdate(newState.zoomPercent);
|
const zoomLevel = zoomState?.zoomLevel;
|
||||||
|
if (
|
||||||
// Register this bridge with ViewerContext
|
zoomLevel === ZoomMode.FitWidth ||
|
||||||
registerBridge('zoom', {
|
zoomLevel === ZoomMode.Automatic ||
|
||||||
state: newState,
|
hadTrackedAutoZoom
|
||||||
api: zoom
|
) {
|
||||||
});
|
requestFitWidth();
|
||||||
|
scheduleAutoZoom();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [zoom, zoomState]);
|
}, [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 currentSpreadMode = spreadMode ?? SpreadMode.None;
|
||||||
|
const pagesPerSpread = currentSpreadMode !== 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,
|
||||||
|
spreadMode,
|
||||||
|
triggerImmediateZoomUpdate,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!zoom) {
|
||||||
|
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 () => {
|
||||||
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,112 +1,55 @@
|
|||||||
import React, { createContext, useContext, useState, ReactNode, useRef } from 'react';
|
import React, {
|
||||||
import { SpreadMode } from '@embedpdf/plugin-spread/react';
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
ReactNode,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
} from 'react';
|
||||||
import { useNavigation } from '@app/contexts/NavigationContext';
|
import { useNavigation } from '@app/contexts/NavigationContext';
|
||||||
|
import {
|
||||||
|
createViewerActions,
|
||||||
|
ScrollActions,
|
||||||
|
ZoomActions,
|
||||||
|
PanActions,
|
||||||
|
SelectionActions,
|
||||||
|
SpreadActions,
|
||||||
|
RotationActions,
|
||||||
|
SearchActions,
|
||||||
|
ExportActions,
|
||||||
|
} from '@app/contexts/viewer/viewerActions';
|
||||||
|
import {
|
||||||
|
BridgeRef,
|
||||||
|
BridgeApiMap,
|
||||||
|
BridgeStateMap,
|
||||||
|
BridgeKey,
|
||||||
|
ViewerBridgeRegistry,
|
||||||
|
createBridgeRegistry,
|
||||||
|
registerBridge as setBridgeRef,
|
||||||
|
ScrollState,
|
||||||
|
ZoomState,
|
||||||
|
PanState,
|
||||||
|
SelectionState,
|
||||||
|
SpreadState,
|
||||||
|
RotationState,
|
||||||
|
SearchState,
|
||||||
|
ExportState,
|
||||||
|
ThumbnailAPIWrapper,
|
||||||
|
} from '@app/contexts/viewer/viewerBridges';
|
||||||
|
import { SpreadMode } from '@embedpdf/plugin-spread/react';
|
||||||
|
|
||||||
// Bridge API interfaces - these match what the bridges provide
|
function useImmediateNotifier<Args extends unknown[]>() {
|
||||||
interface ScrollAPIWrapper {
|
const callbackRef = useRef<((...args: Args) => void) | null>(null);
|
||||||
scrollToPage: (params: { pageNumber: number }) => void;
|
|
||||||
scrollToPreviousPage: () => void;
|
|
||||||
scrollToNextPage: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ZoomAPIWrapper {
|
const register = useCallback((callback: (...args: Args) => void) => {
|
||||||
zoomIn: () => void;
|
callbackRef.current = callback;
|
||||||
zoomOut: () => void;
|
}, []);
|
||||||
toggleMarqueeZoom: () => void;
|
|
||||||
requestZoom: (level: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PanAPIWrapper {
|
const trigger = useCallback((...args: Args) => {
|
||||||
enable: () => void;
|
callbackRef.current?.(...args);
|
||||||
disable: () => void;
|
}, []);
|
||||||
toggle: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SelectionAPIWrapper {
|
return { register, trigger };
|
||||||
copyToClipboard: () => void;
|
|
||||||
getSelectedText: () => string | any;
|
|
||||||
getFormattedSelection: () => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SpreadAPIWrapper {
|
|
||||||
setSpreadMode: (mode: SpreadMode) => void;
|
|
||||||
getSpreadMode: () => SpreadMode | null;
|
|
||||||
toggleSpreadMode: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RotationAPIWrapper {
|
|
||||||
rotateForward: () => void;
|
|
||||||
rotateBackward: () => void;
|
|
||||||
setRotation: (rotation: number) => void;
|
|
||||||
getRotation: () => number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchAPIWrapper {
|
|
||||||
search: (query: string) => Promise<any>;
|
|
||||||
clear: () => void;
|
|
||||||
next: () => void;
|
|
||||||
previous: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ThumbnailAPIWrapper {
|
|
||||||
renderThumb: (pageIndex: number, scale: number) => { toPromise: () => Promise<Blob> };
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExportAPIWrapper {
|
|
||||||
download: () => void;
|
|
||||||
saveAsCopy: () => { toPromise: () => Promise<ArrayBuffer> };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// State interfaces - represent the shape of data from each bridge
|
|
||||||
interface ScrollState {
|
|
||||||
currentPage: number;
|
|
||||||
totalPages: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ZoomState {
|
|
||||||
currentZoom: number;
|
|
||||||
zoomPercent: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PanState {
|
|
||||||
isPanning: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SelectionState {
|
|
||||||
hasSelection: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SpreadState {
|
|
||||||
spreadMode: SpreadMode;
|
|
||||||
isDualPage: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RotationState {
|
|
||||||
rotation: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchResult {
|
|
||||||
pageIndex: number;
|
|
||||||
rects: Array<{
|
|
||||||
origin: { x: number; y: number };
|
|
||||||
size: { width: number; height: number };
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchState {
|
|
||||||
results: SearchResult[] | null;
|
|
||||||
activeIndex: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExportState {
|
|
||||||
canExport: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bridge registration interface - bridges register with state and API
|
|
||||||
interface BridgeRef<TState = unknown, TApi = unknown> {
|
|
||||||
state: TState;
|
|
||||||
api: TApi;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -150,66 +93,28 @@ interface ViewerContextType {
|
|||||||
// Immediate update callbacks
|
// Immediate update callbacks
|
||||||
registerImmediateZoomUpdate: (callback: (percent: number) => void) => void;
|
registerImmediateZoomUpdate: (callback: (percent: number) => void) => void;
|
||||||
registerImmediateScrollUpdate: (callback: (currentPage: number, totalPages: 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
|
// Internal - for bridges to trigger immediate updates
|
||||||
triggerImmediateScrollUpdate: (currentPage: number, totalPages: number) => void;
|
triggerImmediateScrollUpdate: (currentPage: number, totalPages: number) => void;
|
||||||
triggerImmediateZoomUpdate: (zoomPercent: number) => void;
|
triggerImmediateZoomUpdate: (zoomPercent: number) => void;
|
||||||
|
triggerImmediateSpreadUpdate: (mode: SpreadMode, isDualPage?: boolean) => void;
|
||||||
|
|
||||||
// Action handlers - call EmbedPDF APIs directly
|
// Action handlers - call EmbedPDF APIs directly
|
||||||
scrollActions: {
|
scrollActions: ScrollActions;
|
||||||
scrollToPage: (page: number) => void;
|
zoomActions: ZoomActions;
|
||||||
scrollToFirstPage: () => void;
|
panActions: PanActions;
|
||||||
scrollToPreviousPage: () => void;
|
selectionActions: SelectionActions;
|
||||||
scrollToNextPage: () => void;
|
spreadActions: SpreadActions;
|
||||||
scrollToLastPage: () => void;
|
rotationActions: RotationActions;
|
||||||
};
|
searchActions: SearchActions;
|
||||||
|
exportActions: ExportActions;
|
||||||
zoomActions: {
|
|
||||||
zoomIn: () => void;
|
|
||||||
zoomOut: () => void;
|
|
||||||
toggleMarqueeZoom: () => void;
|
|
||||||
requestZoom: (level: number) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
panActions: {
|
|
||||||
enablePan: () => void;
|
|
||||||
disablePan: () => void;
|
|
||||||
togglePan: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
selectionActions: {
|
|
||||||
copyToClipboard: () => void;
|
|
||||||
getSelectedText: () => string;
|
|
||||||
getFormattedSelection: () => unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
spreadActions: {
|
|
||||||
setSpreadMode: (mode: SpreadMode) => void;
|
|
||||||
getSpreadMode: () => SpreadMode | null;
|
|
||||||
toggleSpreadMode: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
rotationActions: {
|
|
||||||
rotateForward: () => void;
|
|
||||||
rotateBackward: () => void;
|
|
||||||
setRotation: (rotation: number) => void;
|
|
||||||
getRotation: () => number;
|
|
||||||
};
|
|
||||||
|
|
||||||
searchActions: {
|
|
||||||
search: (query: string) => Promise<void>;
|
|
||||||
next: () => void;
|
|
||||||
previous: () => void;
|
|
||||||
clear: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
exportActions: {
|
|
||||||
download: () => void;
|
|
||||||
saveAsCopy: () => Promise<ArrayBuffer | null>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Bridge registration - internal use by bridges
|
// Bridge registration - internal use by bridges
|
||||||
registerBridge: (type: string, ref: BridgeRef) => void;
|
registerBridge: <K extends BridgeKey>(
|
||||||
|
type: K,
|
||||||
|
ref: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]>
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ViewerContext = createContext<ViewerContextType | null>(null);
|
export const ViewerContext = createContext<ViewerContextType | null>(null);
|
||||||
@ -229,56 +134,51 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
useNavigation();
|
useNavigation();
|
||||||
|
|
||||||
// Bridge registry - bridges register their state and APIs here
|
// Bridge registry - bridges register their state and APIs here
|
||||||
const bridgeRefs = useRef({
|
const bridgeRefs = useRef<ViewerBridgeRegistry>(createBridgeRegistry());
|
||||||
scroll: null as BridgeRef<ScrollState, ScrollAPIWrapper> | null,
|
|
||||||
zoom: null as BridgeRef<ZoomState, ZoomAPIWrapper> | null,
|
|
||||||
pan: null as BridgeRef<PanState, PanAPIWrapper> | null,
|
|
||||||
selection: null as BridgeRef<SelectionState, SelectionAPIWrapper> | null,
|
|
||||||
search: null as BridgeRef<SearchState, SearchAPIWrapper> | null,
|
|
||||||
spread: null as BridgeRef<SpreadState, SpreadAPIWrapper> | null,
|
|
||||||
rotation: null as BridgeRef<RotationState, RotationAPIWrapper> | null,
|
|
||||||
thumbnail: null as BridgeRef<unknown, ThumbnailAPIWrapper> | null,
|
|
||||||
export: null as BridgeRef<ExportState, ExportAPIWrapper> | null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Immediate zoom callback for responsive display updates
|
const {
|
||||||
const immediateZoomUpdateCallback = useRef<((percent: number) => void) | null>(null);
|
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 triggerImmediateZoomUpdate = useCallback(
|
||||||
const immediateScrollUpdateCallback = useRef<((currentPage: number, totalPages: number) => void) | null>(null);
|
(percent: number) => {
|
||||||
|
triggerImmediateZoomInternal(percent);
|
||||||
|
},
|
||||||
|
[triggerImmediateZoomInternal]
|
||||||
|
);
|
||||||
|
|
||||||
const registerBridge = (type: string, ref: BridgeRef) => {
|
const triggerImmediateScrollUpdate = useCallback(
|
||||||
// Type-safe assignment - we know the bridges will provide correct types
|
(currentPage: number, totalPages: number) => {
|
||||||
switch (type) {
|
triggerImmediateScrollInternal(currentPage, totalPages);
|
||||||
case 'scroll':
|
},
|
||||||
bridgeRefs.current.scroll = ref as BridgeRef<ScrollState, ScrollAPIWrapper>;
|
[triggerImmediateScrollInternal]
|
||||||
break;
|
);
|
||||||
case 'zoom':
|
|
||||||
bridgeRefs.current.zoom = ref as BridgeRef<ZoomState, ZoomAPIWrapper>;
|
const triggerImmediateSpreadUpdate = useCallback(
|
||||||
break;
|
(mode: SpreadMode, isDualPage: boolean = mode !== SpreadMode.None) => {
|
||||||
case 'pan':
|
triggerImmediateSpreadInternal(mode, isDualPage);
|
||||||
bridgeRefs.current.pan = ref as BridgeRef<PanState, PanAPIWrapper>;
|
},
|
||||||
break;
|
[triggerImmediateSpreadInternal]
|
||||||
case 'selection':
|
);
|
||||||
bridgeRefs.current.selection = ref as BridgeRef<SelectionState, SelectionAPIWrapper>;
|
|
||||||
break;
|
const registerBridge = useCallback(
|
||||||
case 'search':
|
<K extends BridgeKey>(
|
||||||
bridgeRefs.current.search = ref as BridgeRef<SearchState, SearchAPIWrapper>;
|
type: K,
|
||||||
break;
|
ref: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]>
|
||||||
case 'spread':
|
) => {
|
||||||
bridgeRefs.current.spread = ref as BridgeRef<SpreadState, SpreadAPIWrapper>;
|
setBridgeRef(bridgeRefs.current, type, ref);
|
||||||
break;
|
},
|
||||||
case 'rotation':
|
[]
|
||||||
bridgeRefs.current.rotation = ref as BridgeRef<RotationState, RotationAPIWrapper>;
|
);
|
||||||
break;
|
|
||||||
case 'thumbnail':
|
|
||||||
bridgeRefs.current.thumbnail = ref as BridgeRef<unknown, ThumbnailAPIWrapper>;
|
|
||||||
break;
|
|
||||||
case 'export':
|
|
||||||
bridgeRefs.current.export = ref as BridgeRef<ExportState, ExportAPIWrapper>;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleThumbnailSidebar = () => {
|
const toggleThumbnailSidebar = () => {
|
||||||
setIsThumbnailSidebarVisible(prev => !prev);
|
setIsThumbnailSidebarVisible(prev => !prev);
|
||||||
@ -334,241 +234,21 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Action handlers - call APIs directly
|
// Action handlers - call APIs directly
|
||||||
const scrollActions = {
|
const {
|
||||||
scrollToPage: (page: number) => {
|
scrollActions,
|
||||||
const api = bridgeRefs.current.scroll?.api;
|
zoomActions,
|
||||||
if (api?.scrollToPage) {
|
panActions,
|
||||||
api.scrollToPage({ pageNumber: page });
|
selectionActions,
|
||||||
}
|
spreadActions,
|
||||||
},
|
rotationActions,
|
||||||
scrollToFirstPage: () => {
|
searchActions,
|
||||||
const api = bridgeRefs.current.scroll?.api;
|
exportActions,
|
||||||
if (api?.scrollToPage) {
|
} = createViewerActions({
|
||||||
api.scrollToPage({ pageNumber: 1 });
|
registry: bridgeRefs,
|
||||||
}
|
getScrollState,
|
||||||
},
|
getZoomState,
|
||||||
scrollToPreviousPage: () => {
|
triggerImmediateZoomUpdate,
|
||||||
const api = bridgeRefs.current.scroll?.api;
|
});
|
||||||
if (api?.scrollToPreviousPage) {
|
|
||||||
api.scrollToPreviousPage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollToNextPage: () => {
|
|
||||||
const api = bridgeRefs.current.scroll?.api;
|
|
||||||
if (api?.scrollToNextPage) {
|
|
||||||
api.scrollToNextPage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollToLastPage: () => {
|
|
||||||
const scrollState = getScrollState();
|
|
||||||
const api = bridgeRefs.current.scroll?.api;
|
|
||||||
if (api?.scrollToPage && scrollState.totalPages > 0) {
|
|
||||||
api.scrollToPage({ pageNumber: scrollState.totalPages });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const zoomActions = {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
api.zoomOut();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleMarqueeZoom: () => {
|
|
||||||
const api = bridgeRefs.current.zoom?.api;
|
|
||||||
if (api?.toggleMarqueeZoom) {
|
|
||||||
api.toggleMarqueeZoom();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
requestZoom: (level: number) => {
|
|
||||||
const api = bridgeRefs.current.zoom?.api;
|
|
||||||
if (api?.requestZoom) {
|
|
||||||
api.requestZoom(level);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const panActions = {
|
|
||||||
enablePan: () => {
|
|
||||||
const api = bridgeRefs.current.pan?.api;
|
|
||||||
if (api?.enable) {
|
|
||||||
api.enable();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
disablePan: () => {
|
|
||||||
const api = bridgeRefs.current.pan?.api;
|
|
||||||
if (api?.disable) {
|
|
||||||
api.disable();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
togglePan: () => {
|
|
||||||
const api = bridgeRefs.current.pan?.api;
|
|
||||||
if (api?.toggle) {
|
|
||||||
api.toggle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectionActions = {
|
|
||||||
copyToClipboard: () => {
|
|
||||||
const api = bridgeRefs.current.selection?.api;
|
|
||||||
if (api?.copyToClipboard) {
|
|
||||||
api.copyToClipboard();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getSelectedText: () => {
|
|
||||||
const api = bridgeRefs.current.selection?.api;
|
|
||||||
if (api?.getSelectedText) {
|
|
||||||
return api.getSelectedText();
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
getFormattedSelection: () => {
|
|
||||||
const api = bridgeRefs.current.selection?.api;
|
|
||||||
if (api?.getFormattedSelection) {
|
|
||||||
return api.getFormattedSelection();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const spreadActions = {
|
|
||||||
setSpreadMode: (mode: SpreadMode) => {
|
|
||||||
const api = bridgeRefs.current.spread?.api;
|
|
||||||
if (api?.setSpreadMode) {
|
|
||||||
api.setSpreadMode(mode);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getSpreadMode: () => {
|
|
||||||
const api = bridgeRefs.current.spread?.api;
|
|
||||||
if (api?.getSpreadMode) {
|
|
||||||
return api.getSpreadMode();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
toggleSpreadMode: () => {
|
|
||||||
const api = bridgeRefs.current.spread?.api;
|
|
||||||
if (api?.toggleSpreadMode) {
|
|
||||||
api.toggleSpreadMode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rotationActions = {
|
|
||||||
rotateForward: () => {
|
|
||||||
const api = bridgeRefs.current.rotation?.api;
|
|
||||||
if (api?.rotateForward) {
|
|
||||||
api.rotateForward();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rotateBackward: () => {
|
|
||||||
const api = bridgeRefs.current.rotation?.api;
|
|
||||||
if (api?.rotateBackward) {
|
|
||||||
api.rotateBackward();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setRotation: (rotation: number) => {
|
|
||||||
const api = bridgeRefs.current.rotation?.api;
|
|
||||||
if (api?.setRotation) {
|
|
||||||
api.setRotation(rotation);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getRotation: () => {
|
|
||||||
const api = bridgeRefs.current.rotation?.api;
|
|
||||||
if (api?.getRotation) {
|
|
||||||
return api.getRotation();
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchActions = {
|
|
||||||
search: async (query: string) => {
|
|
||||||
const api = bridgeRefs.current.search?.api;
|
|
||||||
if (api?.search) {
|
|
||||||
return api.search(query);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
next: () => {
|
|
||||||
const api = bridgeRefs.current.search?.api;
|
|
||||||
if (api?.next) {
|
|
||||||
api.next();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
previous: () => {
|
|
||||||
const api = bridgeRefs.current.search?.api;
|
|
||||||
if (api?.previous) {
|
|
||||||
api.previous();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clear: () => {
|
|
||||||
const api = bridgeRefs.current.search?.api;
|
|
||||||
if (api?.clear) {
|
|
||||||
api.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportActions = {
|
|
||||||
download: () => {
|
|
||||||
const api = bridgeRefs.current.export?.api;
|
|
||||||
if (api?.download) {
|
|
||||||
api.download();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
saveAsCopy: async () => {
|
|
||||||
const api = bridgeRefs.current.export?.api;
|
|
||||||
if (api?.saveAsCopy) {
|
|
||||||
try {
|
|
||||||
const result = api.saveAsCopy();
|
|
||||||
return await result.toPromise();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save PDF copy:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 = {
|
const value: ViewerContextType = {
|
||||||
// UI state
|
// UI state
|
||||||
@ -600,8 +280,10 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
// Immediate updates
|
// Immediate updates
|
||||||
registerImmediateZoomUpdate,
|
registerImmediateZoomUpdate,
|
||||||
registerImmediateScrollUpdate,
|
registerImmediateScrollUpdate,
|
||||||
|
registerImmediateSpreadUpdate,
|
||||||
triggerImmediateScrollUpdate,
|
triggerImmediateScrollUpdate,
|
||||||
triggerImmediateZoomUpdate,
|
triggerImmediateZoomUpdate,
|
||||||
|
triggerImmediateSpreadUpdate,
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
scrollActions,
|
scrollActions,
|
||||||
|
|||||||
@ -58,14 +58,21 @@ const addFilesMutex = new SimpleMutex();
|
|||||||
/**
|
/**
|
||||||
* Helper to create ProcessedFile metadata structure
|
* 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 {
|
return {
|
||||||
totalPages: pageCount,
|
totalPages: pageCount,
|
||||||
pages: Array.from({ length: pageCount }, (_, index) => ({
|
pages: Array.from({ length: pageCount }, (_, index) => ({
|
||||||
pageNumber: index + 1,
|
pageNumber: index + 1,
|
||||||
thumbnail: index === 0 ? thumbnail : undefined, // Only first page gets thumbnail initially
|
thumbnail: index === 0 ? thumbnail : undefined, // Only first page gets thumbnail initially
|
||||||
rotation: pageRotations?.[index] ?? 0,
|
rotation: pageRotations?.[index] ?? 0,
|
||||||
splitBefore: false
|
splitBefore: false,
|
||||||
|
width: pageDimensions?.[index]?.width,
|
||||||
|
height: pageDimensions?.[index]?.height
|
||||||
})),
|
})),
|
||||||
thumbnailUrl: thumbnail,
|
thumbnailUrl: thumbnail,
|
||||||
lastProcessed: Date.now()
|
lastProcessed: Date.now()
|
||||||
@ -92,7 +99,8 @@ export async function generateProcessedFileMetadata(file: File): Promise<Process
|
|||||||
const processedFile = createProcessedFile(
|
const processedFile = createProcessedFile(
|
||||||
unrotatedResult.pageCount,
|
unrotatedResult.pageCount,
|
||||||
unrotatedResult.thumbnail, // Page thumbnails (unrotated)
|
unrotatedResult.thumbnail, // Page thumbnails (unrotated)
|
||||||
unrotatedResult.pageRotations
|
unrotatedResult.pageRotations,
|
||||||
|
unrotatedResult.pageDimensions
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use rotated thumbnail for file manager
|
// Use rotated thumbnail for file manager
|
||||||
|
|||||||
311
frontend/src/core/contexts/viewer/viewerActions.ts
Normal file
311
frontend/src/core/contexts/viewer/viewerActions.ts
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
import { MutableRefObject } from 'react';
|
||||||
|
import { SpreadMode } from '@embedpdf/plugin-spread/react';
|
||||||
|
import {
|
||||||
|
ViewerBridgeRegistry,
|
||||||
|
ScrollState,
|
||||||
|
ZoomState,
|
||||||
|
} from '@app/contexts/viewer/viewerBridges';
|
||||||
|
|
||||||
|
export interface ScrollActions {
|
||||||
|
scrollToPage: (page: number) => void;
|
||||||
|
scrollToFirstPage: () => void;
|
||||||
|
scrollToPreviousPage: () => void;
|
||||||
|
scrollToNextPage: () => void;
|
||||||
|
scrollToLastPage: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZoomActions {
|
||||||
|
zoomIn: () => void;
|
||||||
|
zoomOut: () => void;
|
||||||
|
toggleMarqueeZoom: () => void;
|
||||||
|
requestZoom: (level: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PanActions {
|
||||||
|
enablePan: () => void;
|
||||||
|
disablePan: () => void;
|
||||||
|
togglePan: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectionActions {
|
||||||
|
copyToClipboard: () => void;
|
||||||
|
getSelectedText: () => string;
|
||||||
|
getFormattedSelection: () => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpreadActions {
|
||||||
|
setSpreadMode: (mode: SpreadMode) => void;
|
||||||
|
getSpreadMode: () => SpreadMode | null;
|
||||||
|
toggleSpreadMode: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RotationActions {
|
||||||
|
rotateForward: () => void;
|
||||||
|
rotateBackward: () => void;
|
||||||
|
setRotation: (rotation: number) => void;
|
||||||
|
getRotation: () => number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchActions {
|
||||||
|
search: (query: string) => Promise<any> | undefined;
|
||||||
|
next: () => void;
|
||||||
|
previous: () => void;
|
||||||
|
clear: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExportActions {
|
||||||
|
download: () => void;
|
||||||
|
saveAsCopy: () => Promise<ArrayBuffer | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewerActionsBundle {
|
||||||
|
scrollActions: ScrollActions;
|
||||||
|
zoomActions: ZoomActions;
|
||||||
|
panActions: PanActions;
|
||||||
|
selectionActions: SelectionActions;
|
||||||
|
spreadActions: SpreadActions;
|
||||||
|
rotationActions: RotationActions;
|
||||||
|
searchActions: SearchActions;
|
||||||
|
exportActions: ExportActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ViewerActionDependencies {
|
||||||
|
registry: MutableRefObject<ViewerBridgeRegistry>;
|
||||||
|
getScrollState: () => ScrollState;
|
||||||
|
getZoomState: () => ZoomState;
|
||||||
|
triggerImmediateZoomUpdate: (percent: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createViewerActions({
|
||||||
|
registry,
|
||||||
|
getScrollState,
|
||||||
|
getZoomState,
|
||||||
|
triggerImmediateZoomUpdate,
|
||||||
|
}: ViewerActionDependencies): ViewerActionsBundle {
|
||||||
|
const scrollActions: ScrollActions = {
|
||||||
|
scrollToPage: (page: number) => {
|
||||||
|
const api = registry.current.scroll?.api;
|
||||||
|
if (api?.scrollToPage) {
|
||||||
|
api.scrollToPage({ pageNumber: page });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollToFirstPage: () => {
|
||||||
|
const api = registry.current.scroll?.api;
|
||||||
|
if (api?.scrollToPage) {
|
||||||
|
api.scrollToPage({ pageNumber: 1 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollToPreviousPage: () => {
|
||||||
|
const api = registry.current.scroll?.api;
|
||||||
|
if (api?.scrollToPreviousPage) {
|
||||||
|
api.scrollToPreviousPage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollToNextPage: () => {
|
||||||
|
const api = registry.current.scroll?.api;
|
||||||
|
if (api?.scrollToNextPage) {
|
||||||
|
api.scrollToNextPage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollToLastPage: () => {
|
||||||
|
const api = registry.current.scroll?.api;
|
||||||
|
const state = getScrollState();
|
||||||
|
if (api?.scrollToPage && state.totalPages > 0) {
|
||||||
|
api.scrollToPage({ pageNumber: state.totalPages });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const zoomActions: ZoomActions = {
|
||||||
|
zoomIn: () => {
|
||||||
|
const api = registry.current.zoom?.api;
|
||||||
|
if (api?.zoomIn) {
|
||||||
|
const currentState = getZoomState();
|
||||||
|
const newPercent = Math.min(
|
||||||
|
Math.round(currentState.zoomPercent * 1.2),
|
||||||
|
300
|
||||||
|
);
|
||||||
|
triggerImmediateZoomUpdate(newPercent);
|
||||||
|
api.zoomIn();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
zoomOut: () => {
|
||||||
|
const api = registry.current.zoom?.api;
|
||||||
|
if (api?.zoomOut) {
|
||||||
|
const currentState = getZoomState();
|
||||||
|
const newPercent = Math.max(
|
||||||
|
Math.round(currentState.zoomPercent / 1.2),
|
||||||
|
20
|
||||||
|
);
|
||||||
|
triggerImmediateZoomUpdate(newPercent);
|
||||||
|
api.zoomOut();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleMarqueeZoom: () => {
|
||||||
|
const api = registry.current.zoom?.api;
|
||||||
|
if (api?.toggleMarqueeZoom) {
|
||||||
|
api.toggleMarqueeZoom();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requestZoom: (level: number) => {
|
||||||
|
const api = registry.current.zoom?.api;
|
||||||
|
if (api?.requestZoom) {
|
||||||
|
api.requestZoom(level);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const panActions: PanActions = {
|
||||||
|
enablePan: () => {
|
||||||
|
const api = registry.current.pan?.api;
|
||||||
|
if (api?.enable) {
|
||||||
|
api.enable();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disablePan: () => {
|
||||||
|
const api = registry.current.pan?.api;
|
||||||
|
if (api?.disable) {
|
||||||
|
api.disable();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
togglePan: () => {
|
||||||
|
const api = registry.current.pan?.api;
|
||||||
|
if (api?.toggle) {
|
||||||
|
api.toggle();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectionActions: SelectionActions = {
|
||||||
|
copyToClipboard: () => {
|
||||||
|
const api = registry.current.selection?.api;
|
||||||
|
if (api?.copyToClipboard) {
|
||||||
|
api.copyToClipboard();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getSelectedText: () => {
|
||||||
|
const api = registry.current.selection?.api;
|
||||||
|
if (api?.getSelectedText) {
|
||||||
|
return api.getSelectedText() ?? '';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
getFormattedSelection: () => {
|
||||||
|
const api = registry.current.selection?.api;
|
||||||
|
if (api?.getFormattedSelection) {
|
||||||
|
return api.getFormattedSelection();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const spreadActions: SpreadActions = {
|
||||||
|
setSpreadMode: (mode: SpreadMode) => {
|
||||||
|
const api = registry.current.spread?.api;
|
||||||
|
if (api?.setSpreadMode) {
|
||||||
|
api.setSpreadMode(mode);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getSpreadMode: () => {
|
||||||
|
const api = registry.current.spread?.api;
|
||||||
|
if (api?.getSpreadMode) {
|
||||||
|
return api.getSpreadMode();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
toggleSpreadMode: () => {
|
||||||
|
const api = registry.current.spread?.api;
|
||||||
|
if (api?.toggleSpreadMode) {
|
||||||
|
api.toggleSpreadMode();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const rotationActions: RotationActions = {
|
||||||
|
rotateForward: () => {
|
||||||
|
const api = registry.current.rotation?.api;
|
||||||
|
if (api?.rotateForward) {
|
||||||
|
api.rotateForward();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rotateBackward: () => {
|
||||||
|
const api = registry.current.rotation?.api;
|
||||||
|
if (api?.rotateBackward) {
|
||||||
|
api.rotateBackward();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setRotation: (rotation: number) => {
|
||||||
|
const api = registry.current.rotation?.api;
|
||||||
|
if (api?.setRotation) {
|
||||||
|
api.setRotation(rotation);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getRotation: () => {
|
||||||
|
const api = registry.current.rotation?.api;
|
||||||
|
if (api?.getRotation) {
|
||||||
|
return api.getRotation();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchActions: SearchActions = {
|
||||||
|
search: (query: string) => {
|
||||||
|
const api = registry.current.search?.api;
|
||||||
|
if (api?.search) {
|
||||||
|
return api.search(query);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
next: () => {
|
||||||
|
const api = registry.current.search?.api;
|
||||||
|
if (api?.next) {
|
||||||
|
api.next();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
previous: () => {
|
||||||
|
const api = registry.current.search?.api;
|
||||||
|
if (api?.previous) {
|
||||||
|
api.previous();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clear: () => {
|
||||||
|
const api = registry.current.search?.api;
|
||||||
|
if (api?.clear) {
|
||||||
|
api.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportActions: ExportActions = {
|
||||||
|
download: () => {
|
||||||
|
const api = registry.current.export?.api;
|
||||||
|
if (api?.download) {
|
||||||
|
api.download();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
saveAsCopy: async () => {
|
||||||
|
const api = registry.current.export?.api;
|
||||||
|
if (api?.saveAsCopy) {
|
||||||
|
try {
|
||||||
|
const result = api.saveAsCopy();
|
||||||
|
return await result.toPromise();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save PDF copy:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrollActions,
|
||||||
|
zoomActions,
|
||||||
|
panActions,
|
||||||
|
selectionActions,
|
||||||
|
spreadActions,
|
||||||
|
rotationActions,
|
||||||
|
searchActions,
|
||||||
|
exportActions,
|
||||||
|
};
|
||||||
|
}
|
||||||
174
frontend/src/core/contexts/viewer/viewerBridges.ts
Normal file
174
frontend/src/core/contexts/viewer/viewerBridges.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import { SpreadMode } from '@embedpdf/plugin-spread/react';
|
||||||
|
|
||||||
|
export interface ScrollAPIWrapper {
|
||||||
|
scrollToPage: (params: { pageNumber: number }) => void;
|
||||||
|
scrollToPreviousPage: () => void;
|
||||||
|
scrollToNextPage: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZoomAPIWrapper {
|
||||||
|
zoomIn: () => void;
|
||||||
|
zoomOut: () => void;
|
||||||
|
toggleMarqueeZoom: () => void;
|
||||||
|
requestZoom: (level: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PanAPIWrapper {
|
||||||
|
enable: () => void;
|
||||||
|
disable: () => void;
|
||||||
|
toggle: () => void;
|
||||||
|
makePanDefault: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectionAPIWrapper {
|
||||||
|
copyToClipboard: () => void;
|
||||||
|
getSelectedText: () => string | any;
|
||||||
|
getFormattedSelection: () => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpreadAPIWrapper {
|
||||||
|
setSpreadMode: (mode: SpreadMode) => void;
|
||||||
|
getSpreadMode: () => SpreadMode | null;
|
||||||
|
toggleSpreadMode: () => void;
|
||||||
|
SpreadMode: typeof SpreadMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RotationAPIWrapper {
|
||||||
|
rotateForward: () => void;
|
||||||
|
rotateBackward: () => void;
|
||||||
|
setRotation: (rotation: number) => void;
|
||||||
|
getRotation: () => number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchAPIWrapper {
|
||||||
|
search: (query: string) => Promise<any>;
|
||||||
|
clear: () => void;
|
||||||
|
next: () => void;
|
||||||
|
previous: () => void;
|
||||||
|
goToResult: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThumbnailAPIWrapper {
|
||||||
|
renderThumb: (pageIndex: number, scale: number) => {
|
||||||
|
toPromise: () => Promise<Blob>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExportAPIWrapper {
|
||||||
|
download: () => void;
|
||||||
|
saveAsCopy: () => { toPromise: () => Promise<ArrayBuffer> };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScrollState {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZoomState {
|
||||||
|
currentZoom: number;
|
||||||
|
zoomPercent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PanState {
|
||||||
|
isPanning: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectionState {
|
||||||
|
hasSelection: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpreadState {
|
||||||
|
spreadMode: SpreadMode;
|
||||||
|
isDualPage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RotationState {
|
||||||
|
rotation: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
pageIndex: number;
|
||||||
|
rects: Array<{
|
||||||
|
origin: { x: number; y: number };
|
||||||
|
size: { width: number; height: number };
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchState {
|
||||||
|
results: SearchResult[] | null;
|
||||||
|
activeIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExportState {
|
||||||
|
canExport: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BridgeRef<TState = unknown, TApi = unknown> {
|
||||||
|
state: TState;
|
||||||
|
api: TApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BridgeStateMap {
|
||||||
|
scroll: ScrollState;
|
||||||
|
zoom: ZoomState;
|
||||||
|
pan: PanState;
|
||||||
|
selection: SelectionState;
|
||||||
|
spread: SpreadState;
|
||||||
|
rotation: RotationState;
|
||||||
|
search: SearchState;
|
||||||
|
thumbnail: unknown;
|
||||||
|
export: ExportState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BridgeApiMap {
|
||||||
|
scroll: ScrollAPIWrapper;
|
||||||
|
zoom: ZoomAPIWrapper;
|
||||||
|
pan: PanAPIWrapper;
|
||||||
|
selection: SelectionAPIWrapper;
|
||||||
|
spread: SpreadAPIWrapper;
|
||||||
|
rotation: RotationAPIWrapper;
|
||||||
|
search: SearchAPIWrapper;
|
||||||
|
thumbnail: ThumbnailAPIWrapper;
|
||||||
|
export: ExportAPIWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BridgeKey = keyof BridgeStateMap;
|
||||||
|
|
||||||
|
export type ViewerBridgeRegistry = {
|
||||||
|
[K in BridgeKey]: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createBridgeRegistry = (): ViewerBridgeRegistry => ({
|
||||||
|
scroll: null,
|
||||||
|
zoom: null,
|
||||||
|
pan: null,
|
||||||
|
selection: null,
|
||||||
|
spread: null,
|
||||||
|
rotation: null,
|
||||||
|
search: null,
|
||||||
|
thumbnail: null,
|
||||||
|
export: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function registerBridge<K extends BridgeKey>(
|
||||||
|
registry: ViewerBridgeRegistry,
|
||||||
|
type: K,
|
||||||
|
ref: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]>
|
||||||
|
): void {
|
||||||
|
registry[type] = ref as ViewerBridgeRegistry[K];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBridgeState<K extends BridgeKey>(
|
||||||
|
registry: ViewerBridgeRegistry,
|
||||||
|
type: K,
|
||||||
|
fallback: BridgeStateMap[K]
|
||||||
|
): BridgeStateMap[K] {
|
||||||
|
return registry[type]?.state ?? fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBridgeApi<K extends BridgeKey>(
|
||||||
|
registry: ViewerBridgeRegistry,
|
||||||
|
type: K
|
||||||
|
): BridgeApiMap[K] | null {
|
||||||
|
return registry[type]?.api ?? null;
|
||||||
|
}
|
||||||
@ -14,6 +14,8 @@ export interface ProcessedFilePage {
|
|||||||
pageNumber?: number;
|
pageNumber?: number;
|
||||||
rotation?: number;
|
rotation?: number;
|
||||||
splitBefore?: boolean;
|
splitBefore?: boolean;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
53
frontend/src/core/utils/pageMetadata.ts
Normal file
53
frontend/src/core/utils/pageMetadata.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
ProcessedFileMetadata,
|
||||||
|
ProcessedFilePage,
|
||||||
|
StirlingFileStub,
|
||||||
|
} from '@app/types/fileContext';
|
||||||
|
|
||||||
|
export interface PageDimensions {
|
||||||
|
width: number | null;
|
||||||
|
height: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPageDimensions(
|
||||||
|
page?: ProcessedFilePage | null
|
||||||
|
): PageDimensions {
|
||||||
|
const width =
|
||||||
|
typeof page?.width === 'number' && page.width > 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);
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ export interface ThumbnailWithMetadata {
|
|||||||
thumbnail: string; // Always returns a thumbnail (placeholder if needed)
|
thumbnail: string; // Always returns a thumbnail (placeholder if needed)
|
||||||
pageCount: number;
|
pageCount: number;
|
||||||
pageRotations?: number[]; // Rotation for each page (0, 90, 180, 270)
|
pageRotations?: number[]; // Rotation for each page (0, 90, 180, 270)
|
||||||
|
pageDimensions?: Array<{ width: number; height: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ColorScheme {
|
interface ColorScheme {
|
||||||
@ -402,12 +403,18 @@ export async function generateThumbnailWithMetadata(file: File, applyRotation: b
|
|||||||
|
|
||||||
const pageCount = pdf.numPages;
|
const pageCount = pdf.numPages;
|
||||||
const page = await pdf.getPage(1);
|
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 false, render without rotation (for CSS-based rotation)
|
||||||
// If applyRotation is true, let PDF.js apply rotation (for static display)
|
// If applyRotation is true, let PDF.js apply rotation (for static display)
|
||||||
const viewport = applyRotation
|
const viewport = applyRotation
|
||||||
? page.getViewport({ scale })
|
? page.getViewport({ scale })
|
||||||
: page.getViewport({ scale, rotation: 0 });
|
: 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");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
@ -428,10 +435,17 @@ export async function generateThumbnailWithMetadata(file: File, applyRotation: b
|
|||||||
const p = await pdf.getPage(i);
|
const p = await pdf.getPage(i);
|
||||||
const rotation = p.rotate || 0;
|
const rotation = p.rotate || 0;
|
||||||
pageRotations.push(rotation);
|
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);
|
pdfWorkerManager.destroyDocument(pdf);
|
||||||
return { thumbnail, pageCount, pageRotations };
|
return { thumbnail, pageCount, pageRotations, pageDimensions };
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.name === "PasswordException") {
|
if (error instanceof Error && error.name === "PasswordException") {
|
||||||
|
|||||||
188
frontend/src/core/utils/viewerZoom.ts
Normal file
188
frontend/src/core/utils/viewerZoom.ts
Normal file
@ -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<DOMRect | null> {
|
||||||
|
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rafId: number | null = null;
|
||||||
|
|
||||||
|
const waitForNextFrame = () =>
|
||||||
|
new Promise<void>((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 (timeoutId !== undefined) {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
requestFitWidthRef.current?.();
|
||||||
|
onDebouncedResizeRef.current?.();
|
||||||
|
}, debounceMs);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => {
|
||||||
|
if (timeoutId !== undefined) {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
}, [debounceMs]);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user