From 6498f94c36fb455ca976ad60c57375ab8cc49a7a Mon Sep 17 00:00:00 2001 From: asrient <44570278+asrient@users.noreply.github.com> Date: Sun, 28 Aug 2022 16:48:51 +0530 Subject: [PATCH] Desktop: Add zoom feature on PDF viewer (#6748) --- .eslintignore | 12 +++-- .gitignore | 6 +++ package.json | 2 +- packages/pdf-viewer/VerticalPages.tsx | 49 ++++++------------- packages/pdf-viewer/hooks/useIsVisible.ts | 1 + packages/pdf-viewer/hooks/useScaledSize.ts | 11 +++-- packages/pdf-viewer/hooks/useScrollSaver.ts | 53 +++++++++++++++++++++ packages/pdf-viewer/miniViewer.tsx | 18 +++++-- packages/pdf-viewer/package.json | 3 ++ packages/pdf-viewer/ui/ZoomControls.tsx | 46 ++++++++++++++++++ packages/pdf-viewer/viewer.css | 19 ++------ yarn.lock | 53 ++++++++++++++++++++- 12 files changed, 210 insertions(+), 63 deletions(-) create mode 100644 packages/pdf-viewer/hooks/useScrollSaver.ts create mode 100644 packages/pdf-viewer/ui/ZoomControls.tsx diff --git a/.eslintignore b/.eslintignore index 16825e037..a1f722ba8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -842,12 +842,12 @@ packages/app-mobile/components/BackButtonDialogBox.js.map packages/app-mobile/components/CameraView.d.ts packages/app-mobile/components/CameraView.js packages/app-mobile/components/CameraView.js.map -packages/app-mobile/components/Dropdown.d.ts -packages/app-mobile/components/Dropdown.js -packages/app-mobile/components/Dropdown.js.map packages/app-mobile/components/CustomButton.d.ts packages/app-mobile/components/CustomButton.js packages/app-mobile/components/CustomButton.js.map +packages/app-mobile/components/Dropdown.d.ts +packages/app-mobile/components/Dropdown.js +packages/app-mobile/components/Dropdown.js.map packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.d.ts packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js.map @@ -1985,6 +1985,9 @@ packages/pdf-viewer/hooks/usePdfData.js.map packages/pdf-viewer/hooks/useScaledSize.d.ts packages/pdf-viewer/hooks/useScaledSize.js packages/pdf-viewer/hooks/useScaledSize.js.map +packages/pdf-viewer/hooks/useScrollSaver.d.ts +packages/pdf-viewer/hooks/useScrollSaver.js +packages/pdf-viewer/hooks/useScrollSaver.js.map packages/pdf-viewer/main.d.ts packages/pdf-viewer/main.js packages/pdf-viewer/main.js.map @@ -1997,6 +2000,9 @@ packages/pdf-viewer/pdfSource.js.map packages/pdf-viewer/pdfSource.test.d.ts packages/pdf-viewer/pdfSource.test.js packages/pdf-viewer/pdfSource.test.js.map +packages/pdf-viewer/ui/ZoomControls.d.ts +packages/pdf-viewer/ui/ZoomControls.js +packages/pdf-viewer/ui/ZoomControls.js.map packages/plugin-repo-cli/commands/updateRelease.d.ts packages/plugin-repo-cli/commands/updateRelease.js packages/plugin-repo-cli/commands/updateRelease.js.map diff --git a/.gitignore b/.gitignore index 3122a8dab..1e090f57a 100644 --- a/.gitignore +++ b/.gitignore @@ -1974,6 +1974,9 @@ packages/pdf-viewer/hooks/usePdfData.js.map packages/pdf-viewer/hooks/useScaledSize.d.ts packages/pdf-viewer/hooks/useScaledSize.js packages/pdf-viewer/hooks/useScaledSize.js.map +packages/pdf-viewer/hooks/useScrollSaver.d.ts +packages/pdf-viewer/hooks/useScrollSaver.js +packages/pdf-viewer/hooks/useScrollSaver.js.map packages/pdf-viewer/main.d.ts packages/pdf-viewer/main.js packages/pdf-viewer/main.js.map @@ -1986,6 +1989,9 @@ packages/pdf-viewer/pdfSource.js.map packages/pdf-viewer/pdfSource.test.d.ts packages/pdf-viewer/pdfSource.test.js packages/pdf-viewer/pdfSource.test.js.map +packages/pdf-viewer/ui/ZoomControls.d.ts +packages/pdf-viewer/ui/ZoomControls.js +packages/pdf-viewer/ui/ZoomControls.js.map packages/plugin-repo-cli/commands/updateRelease.d.ts packages/plugin-repo-cli/commands/updateRelease.js packages/plugin-repo-cli/commands/updateRelease.js.map diff --git a/package.json b/package.json index c8d241f8e..0988ff0ea 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "scripts": { "buildParallel": "yarn workspaces foreach --verbose --interlaced --parallel --jobs 2 run build && yarn run tsc", - "buildSequential": "yarn workspaces foreach --verbose --interlaced run build && yarn run tsc", + "buildSequential": "yarn workspaces foreach --verbose --interlaced --topological run build && yarn run tsc", "buildApiDoc": "yarn workspace joplin start apidoc ../../readme/api/references/rest_api.md", "buildCommandIndex": "gulp buildCommandIndex", "buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out ../joplin-website/docs/api/references/plugin_api packages/lib/services/plugins/api/", diff --git a/packages/pdf-viewer/VerticalPages.tsx b/packages/pdf-viewer/VerticalPages.tsx index 3ce54ec7f..4aa50b6e3 100644 --- a/packages/pdf-viewer/VerticalPages.tsx +++ b/packages/pdf-viewer/VerticalPages.tsx @@ -4,6 +4,7 @@ import { PdfData } from './pdfSource'; import Page from './Page'; import styled from 'styled-components'; import useScaledSize, { ScaledSizeParams } from './hooks/useScaledSize'; +import useScrollSaver, { ScrollSaver } from './hooks/useScrollSaver'; const PagesHolder = styled.div<{ pageGap: number }>` @@ -11,7 +12,8 @@ const PagesHolder = styled.div<{ pageGap: number }>` justify-content: center; align-items: center; flex-flow: column; - width: 100%; + width: fit-content; + min-width: 100%; min-height: 100%; row-gap: ${(props)=> props.pageGap || 2}px; `; @@ -22,15 +24,16 @@ export interface VerticalPagesProps { anchorPage?: number; rememberScroll?: boolean; pdfId?: string; + zoom?: number; container: MutableRefObject; pageGap?: number; showPageNumbers?: boolean; } - export default function VerticalPages(props: VerticalPagesProps) { const [containerWidth, setContainerWidth] = useState(null); const innerContainerEl = useRef(null); + const scaledSize = useScaledSize({ pdf: props.pdf, pdfId: props.pdfId, @@ -40,15 +43,23 @@ export default function VerticalPages(props: VerticalPagesProps) { container: props.container, innerContainerEl, pageGap: props.pageGap, + zoom: props.zoom, } as ScaledSizeParams); + useScrollSaver({ + container: props.container, + scaledSize, + pdfId: props.pdfId, + rememberScroll: props.rememberScroll, + } as ScrollSaver); + useEffect(() => { let resizeTimer: number = null; let cancelled = false; const updateWidth = () => { if (cancelled) return; - setContainerWidth(innerContainerEl.current.clientWidth); + setContainerWidth(props.container.current.clientWidth); }; const onResize = () => { @@ -70,37 +81,7 @@ export default function VerticalPages(props: VerticalPagesProps) { resizeTimer = null; } }; - }, [props.pdf]); - - useEffect(() => { - let scrollTimer: number = null; - - const saveScroll = () => { - const scrollTop = props.container.current.scrollTop; - if (props.rememberScroll && props.pdfId) { - sessionStorage.setItem(`pdf.${props.pdfId}.scrollTop`, `${scrollTop}`); - } - }; - - const onScroll = () => { - if (scrollTimer) { - clearTimeout(scrollTimer); - scrollTimer = null; - } - scrollTimer = window.setTimeout(saveScroll, 200); - }; - - props.container.current.addEventListener('scroll', onScroll); - - return () => { - props.container.current.removeEventListener('scroll', onScroll); - if (scrollTimer) { - clearTimeout(scrollTimer); - scrollTimer = null; - } - }; - - }, [props.container, props.pdfId, props.rememberScroll]); + }, [props.container, props.pdf]); return ( {scaledSize ? diff --git a/packages/pdf-viewer/hooks/useIsVisible.ts b/packages/pdf-viewer/hooks/useIsVisible.ts index 33b42caa9..036501f2e 100644 --- a/packages/pdf-viewer/hooks/useIsVisible.ts +++ b/packages/pdf-viewer/hooks/useIsVisible.ts @@ -3,6 +3,7 @@ import { useEffect, useState, MutableRefObject } from 'react'; const useIsVisible = (elementRef: MutableRefObject, rootRef: MutableRefObject) => { const [isVisible, setIsVisible] = useState(false); + useEffect(() => { let observer: IntersectionObserver = null; if (elementRef.current) { diff --git a/packages/pdf-viewer/hooks/useScaledSize.ts b/packages/pdf-viewer/hooks/useScaledSize.ts index 2b1f61e4c..5b01afc18 100644 --- a/packages/pdf-viewer/hooks/useScaledSize.ts +++ b/packages/pdf-viewer/hooks/useScaledSize.ts @@ -11,16 +11,18 @@ export interface ScaledSizeParams { container: MutableRefObject; innerContainerEl: MutableRefObject; pageGap: number; + zoom: number; } -const useScaledSize = ({ pdf, pdfId, containerWidth, rememberScroll, anchorPage, container, innerContainerEl, pageGap }: ScaledSizeParams) => { +const useScaledSize = ({ pdf, pdfId, containerWidth, rememberScroll, anchorPage, container, innerContainerEl, pageGap, zoom }: ScaledSizeParams) => { const [scaledSize, setScaledSize] = useState(null); const currentScaleSize = useRef(scaledSize); useAsyncEffect(async (event: AsyncEffectEvent) => { if (!pdf || !containerWidth) return; // console.log('scaledSize calculation triggered'); - const scaledSize_ = await pdf.getScaledSize(null, containerWidth - 10); + const effectiveWidth = Math.min(containerWidth - 20, 900) * (zoom || 1); + const scaledSize_ = await pdf.getScaledSize(null, effectiveWidth); if (event.cancelled) return; const oldScaleSize = currentScaleSize.current; @@ -40,11 +42,12 @@ const useScaledSize = ({ pdf, pdfId, containerWidth, rememberScroll, anchorPage, if (rememberScroll && pdfId && !oldScaleSize && !anchorPage) { const scrollOffset = parseInt(sessionStorage.getItem(`pdf.${pdfId}.scrollTop`), 10) || null; if (scrollOffset) { - container.current.scrollTop = scrollOffset; + // Adjusting it according to the new scale + container.current.scrollTop = scrollOffset * scaledSize_.scale; // console.log('scroll set',container.current.scrollTop); } } - }, [pdf, pdfId, rememberScroll, anchorPage, containerWidth]); + }, [pdf, pdfId, rememberScroll, anchorPage, containerWidth, zoom]); return scaledSize; }; diff --git a/packages/pdf-viewer/hooks/useScrollSaver.ts b/packages/pdf-viewer/hooks/useScrollSaver.ts new file mode 100644 index 000000000..c7c630747 --- /dev/null +++ b/packages/pdf-viewer/hooks/useScrollSaver.ts @@ -0,0 +1,53 @@ +import { useRef, useEffect, MutableRefObject } from 'react'; +import { ScaledSize } from '../pdfSource'; + +export interface ScrollSaver { + container: MutableRefObject; + scaledSize: ScaledSize; + pdfId: string; + rememberScroll: boolean; +} + +const useScrollSaver = ({ container, scaledSize, pdfId, rememberScroll }: ScrollSaver) => { + const currentScaleSize = useRef(scaledSize); + + useEffect(() => { + let scrollTimer: number = null; + const containerElement = container.current; + + const saveScroll = () => { + if (!currentScaleSize.current) return; + const scale = currentScaleSize.current.scale; + const scrollTop = container.current.scrollTop / scale; + if (rememberScroll && pdfId) { + sessionStorage.setItem(`pdf.${pdfId}.scrollTop`, `${scrollTop}`); + } + }; + + const onScroll = () => { + if (scrollTimer) { + clearTimeout(scrollTimer); + scrollTimer = null; + } + scrollTimer = window.setTimeout(saveScroll, 200); + }; + + containerElement.addEventListener('scroll', onScroll); + + return () => { + containerElement.removeEventListener('scroll', onScroll); + if (scrollTimer) { + clearTimeout(scrollTimer); + scrollTimer = null; + } + }; + }, [container, pdfId, rememberScroll, currentScaleSize]); + + useEffect(() => { + currentScaleSize.current = scaledSize; + } , [scaledSize]); + + return scaledSize; +}; + +export default useScrollSaver; diff --git a/packages/pdf-viewer/miniViewer.tsx b/packages/pdf-viewer/miniViewer.tsx index 12cfe972c..705c79da3 100644 --- a/packages/pdf-viewer/miniViewer.tsx +++ b/packages/pdf-viewer/miniViewer.tsx @@ -1,7 +1,8 @@ -import React, { useRef } from 'react'; +import React, { useRef, useState } from 'react'; import useIsFocused from './hooks/useIsFocused'; import usePdfData from './hooks/usePdfData'; import VerticalPages from './VerticalPages'; +import ZoomControls from './ui/ZoomControls'; export interface MiniViewerAppProps { pdfPath: string; @@ -13,6 +14,7 @@ export interface MiniViewerAppProps { export default function MiniViewerApp(props: MiniViewerAppProps) { const pdf = usePdfData(props.pdfPath); const isFocused = useIsFocused(); + const [zoom, setZoom] = useState(1); const containerEl = useRef(null); if (!pdf) { @@ -25,12 +27,20 @@ export default function MiniViewerApp(props: MiniViewerAppProps) { return (
- +
- {pdf.pageCount} pages +
{pdf.pageCount} pages
+
{isFocused ? '' : 'Click to enable scroll'}
diff --git a/packages/pdf-viewer/package.json b/packages/pdf-viewer/package.json index 06c82bcd3..86960fff6 100644 --- a/packages/pdf-viewer/package.json +++ b/packages/pdf-viewer/package.json @@ -36,6 +36,9 @@ "webpack-cli": "^4.10.0" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.1.2", + "@fortawesome/free-solid-svg-icons": "^6.1.2", + "@fortawesome/react-fontawesome": "^0.2.0", "@joplin/lib": "workspace:^", "pdfjs-dist": "^2.14.305", "react": "16.13.1", diff --git a/packages/pdf-viewer/ui/ZoomControls.tsx b/packages/pdf-viewer/ui/ZoomControls.tsx new file mode 100644 index 000000000..792e48b20 --- /dev/null +++ b/packages/pdf-viewer/ui/ZoomControls.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import styled from 'styled-components'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'; + +const ZoomGroup = styled.div<{ size: number }>` +display: flex; +justify-content: center; +align-items: center; +flex-flow: row; +color: var(--grey); +cursor: initial; +font-size: ${props => props.size}rem; +padding: 0.2rem 0.4rem; +user-select: none; +border-radius: 5px; +&:hover { + background: #7676764d; +} +svg:hover { + color: var(--secondary); +} +`; + +export interface ZoomControlsProps { + zoom: number; + onChange: (zoom: number)=> void; + size?: number; +} + +export default function ZoomControls(props: ZoomControlsProps) { + + const zoomIn = () => { + props.onChange(Math.min(props.zoom + 0.25, 2)); + }; + + const zoomOut = () => { + props.onChange(Math.max(props.zoom - 0.25, 0.5)); + }; + + return ( + + {props.zoom * 100}% + + ); +} diff --git a/packages/pdf-viewer/viewer.css b/packages/pdf-viewer/viewer.css index 95913b1e4..4e90af0f8 100644 --- a/packages/pdf-viewer/viewer.css +++ b/packages/pdf-viewer/viewer.css @@ -115,23 +115,9 @@ hr { color: var(--grey); } -.pdf-info>a { - padding: 0rem 0.4rem; - color: var(--blue); - cursor: pointer; - border: solid thin transparent; - border-radius: 5px; - user-select: none; -} - -.pdf-info>a:hover { - border-color: var(--grey); -} - .app-pages { - display: flex; - justify-content: center; - align-items: start; + display: block; + margin: 0px auto; overflow-x: hidden; width: 100%; height: 100%; @@ -154,4 +140,5 @@ hr { flex-direction: row; justify-content: space-around; align-items: center; + column-gap: 0.2rem; } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 7a3c59202..626159e20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3170,6 +3170,13 @@ __metadata: languageName: node linkType: hard +"@fortawesome/fontawesome-common-types@npm:6.1.2": + version: 6.1.2 + resolution: "@fortawesome/fontawesome-common-types@npm:6.1.2" + checksum: 16ba97f73256adef5cd680a027e6dbd3e76f34ba9227f7bd9e8c372cb4f337c7ccbf41f35dcfbc22b11320bbb579d01daccf1aa7ae47df5d0b17260c0097bb30 + languageName: node + linkType: hard + "@fortawesome/fontawesome-common-types@npm:^0.2.36": version: 0.2.36 resolution: "@fortawesome/fontawesome-common-types@npm:0.2.36" @@ -3193,6 +3200,15 @@ __metadata: languageName: node linkType: hard +"@fortawesome/fontawesome-svg-core@npm:^6.1.2": + version: 6.1.2 + resolution: "@fortawesome/fontawesome-svg-core@npm:6.1.2" + dependencies: + "@fortawesome/fontawesome-common-types": 6.1.2 + checksum: bb82ed1e79ade9977746b4bb28ac0d03de7557e7c923446adec78cb3447740aca0859e91d6310007d758055a1b8f3a6f740915f04cac738931135c9bdbfe5d32 + languageName: node + linkType: hard + "@fortawesome/free-regular-svg-icons@npm:^5.13.0": version: 5.15.4 resolution: "@fortawesome/free-regular-svg-icons@npm:5.15.4" @@ -3211,6 +3227,27 @@ __metadata: languageName: node linkType: hard +"@fortawesome/free-solid-svg-icons@npm:^6.1.2": + version: 6.1.2 + resolution: "@fortawesome/free-solid-svg-icons@npm:6.1.2" + dependencies: + "@fortawesome/fontawesome-common-types": 6.1.2 + checksum: b7258cd092304f9bd1e557cd3a59e9ff560165631b04f16785da14ce3148873fa8e688d62f95d68f668be32e1207c47a1fe556978ae7e9573a5b0fec09fc0f0c + languageName: node + linkType: hard + +"@fortawesome/react-fontawesome@npm:^0.2.0": + version: 0.2.0 + resolution: "@fortawesome/react-fontawesome@npm:0.2.0" + dependencies: + prop-types: ^15.8.1 + peerDependencies: + "@fortawesome/fontawesome-svg-core": ~1 || ~6 + react: ">=16.3" + checksum: f652a0c2172e7b209e2d9e7e511f9b8c17abad85f55e0bd09bb1175ea1927693215da47eb6cd95b1f3a23bd124368553c677907fa76cb17c5093afc1fcffe338 + languageName: node + linkType: hard + "@gar/promisify@npm:^1.0.1": version: 1.1.2 resolution: "@gar/promisify@npm:1.1.2" @@ -4293,6 +4330,9 @@ __metadata: version: 0.0.0-use.local resolution: "@joplin/pdf-viewer@workspace:packages/pdf-viewer" dependencies: + "@fortawesome/fontawesome-svg-core": ^6.1.2 + "@fortawesome/free-solid-svg-icons": ^6.1.2 + "@fortawesome/react-fontawesome": ^0.2.0 "@joplin/lib": "workspace:^" "@types/jest": ^28.1.6 "@types/pdfjs-dist": ^2.10.378 @@ -27614,6 +27654,17 @@ __metadata: languageName: node linkType: hard +"prop-types@npm:^15.8.1": + version: 15.8.1 + resolution: "prop-types@npm:15.8.1" + dependencies: + loose-envify: ^1.4.0 + object-assign: ^4.1.1 + react-is: ^16.13.1 + checksum: c056d3f1c057cb7ff8344c645450e14f088a915d078dcda795041765047fa080d38e5d626560ccaac94a4e16e3aa15f3557c1a9a8d1174530955e992c675e459 + languageName: node + linkType: hard + "proper-lockfile@npm:^2.0.1": version: 2.0.1 resolution: "proper-lockfile@npm:2.0.1" @@ -28036,7 +28087,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.12.0, react-is@npm:^16.7.0, react-is@npm:^16.8.1, react-is@npm:^16.8.6": +"react-is@npm:^16.12.0, react-is@npm:^16.13.1, react-is@npm:^16.7.0, react-is@npm:^16.8.1, react-is@npm:^16.8.6": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f