mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
Desktop: Add PDF full screen viewer (#6821)
This commit is contained in:
parent
e3ba02281b
commit
f6a8bf9ea2
@ -333,6 +333,9 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js.map
|
||||
@ -597,6 +600,9 @@ packages/app-desktop/gui/OneDriveLoginScreen.js.map
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.d.ts
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js.map
|
||||
packages/app-desktop/gui/PdfViewer.d.ts
|
||||
packages/app-desktop/gui/PdfViewer.js
|
||||
packages/app-desktop/gui/PdfViewer.js.map
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.d.ts
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js.map
|
||||
@ -2019,6 +2025,9 @@ packages/lib/uuid.js.map
|
||||
packages/lib/versionInfo.d.ts
|
||||
packages/lib/versionInfo.js
|
||||
packages/lib/versionInfo.js.map
|
||||
packages/pdf-viewer/FullViewer.d.ts
|
||||
packages/pdf-viewer/FullViewer.js
|
||||
packages/pdf-viewer/FullViewer.js.map
|
||||
packages/pdf-viewer/Page.d.ts
|
||||
packages/pdf-viewer/Page.js
|
||||
packages/pdf-viewer/Page.js.map
|
||||
@ -2043,9 +2052,15 @@ 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/hooks/useVisibleOnSelect.d.ts
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.js
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.js.map
|
||||
packages/pdf-viewer/main.d.ts
|
||||
packages/pdf-viewer/main.js
|
||||
packages/pdf-viewer/main.js.map
|
||||
packages/pdf-viewer/messageService.d.ts
|
||||
packages/pdf-viewer/messageService.js
|
||||
packages/pdf-viewer/messageService.js.map
|
||||
packages/pdf-viewer/miniViewer.d.ts
|
||||
packages/pdf-viewer/miniViewer.js
|
||||
packages/pdf-viewer/miniViewer.js.map
|
||||
@ -2055,6 +2070,9 @@ packages/pdf-viewer/pdfSource.test.js.map
|
||||
packages/pdf-viewer/types.d.ts
|
||||
packages/pdf-viewer/types.js
|
||||
packages/pdf-viewer/types.js.map
|
||||
packages/pdf-viewer/ui/GotoPage.d.ts
|
||||
packages/pdf-viewer/ui/GotoPage.js
|
||||
packages/pdf-viewer/ui/GotoPage.js.map
|
||||
packages/pdf-viewer/ui/IconButtons.d.ts
|
||||
packages/pdf-viewer/ui/IconButtons.js
|
||||
packages/pdf-viewer/ui/IconButtons.js.map
|
||||
|
18
.gitignore
vendored
18
.gitignore
vendored
@ -321,6 +321,9 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js.map
|
||||
@ -585,6 +588,9 @@ packages/app-desktop/gui/OneDriveLoginScreen.js.map
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.d.ts
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js.map
|
||||
packages/app-desktop/gui/PdfViewer.d.ts
|
||||
packages/app-desktop/gui/PdfViewer.js
|
||||
packages/app-desktop/gui/PdfViewer.js.map
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.d.ts
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js.map
|
||||
@ -2007,6 +2013,9 @@ packages/lib/uuid.js.map
|
||||
packages/lib/versionInfo.d.ts
|
||||
packages/lib/versionInfo.js
|
||||
packages/lib/versionInfo.js.map
|
||||
packages/pdf-viewer/FullViewer.d.ts
|
||||
packages/pdf-viewer/FullViewer.js
|
||||
packages/pdf-viewer/FullViewer.js.map
|
||||
packages/pdf-viewer/Page.d.ts
|
||||
packages/pdf-viewer/Page.js
|
||||
packages/pdf-viewer/Page.js.map
|
||||
@ -2031,9 +2040,15 @@ 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/hooks/useVisibleOnSelect.d.ts
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.js
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.js.map
|
||||
packages/pdf-viewer/main.d.ts
|
||||
packages/pdf-viewer/main.js
|
||||
packages/pdf-viewer/main.js.map
|
||||
packages/pdf-viewer/messageService.d.ts
|
||||
packages/pdf-viewer/messageService.js
|
||||
packages/pdf-viewer/messageService.js.map
|
||||
packages/pdf-viewer/miniViewer.d.ts
|
||||
packages/pdf-viewer/miniViewer.js
|
||||
packages/pdf-viewer/miniViewer.js.map
|
||||
@ -2043,6 +2058,9 @@ packages/pdf-viewer/pdfSource.test.js.map
|
||||
packages/pdf-viewer/types.d.ts
|
||||
packages/pdf-viewer/types.js
|
||||
packages/pdf-viewer/types.js.map
|
||||
packages/pdf-viewer/ui/GotoPage.d.ts
|
||||
packages/pdf-viewer/ui/GotoPage.js
|
||||
packages/pdf-viewer/ui/GotoPage.js.map
|
||||
packages/pdf-viewer/ui/IconButtons.d.ts
|
||||
packages/pdf-viewer/ui/IconButtons.js
|
||||
packages/pdf-viewer/ui/IconButtons.js.map
|
||||
|
@ -15,6 +15,7 @@ import * as openFolder from './openFolder';
|
||||
import * as openFolderDialog from './openFolderDialog';
|
||||
import * as openItem from './openItem';
|
||||
import * as openNote from './openNote';
|
||||
import * as openPdfViewer from './openPdfViewer';
|
||||
import * as openTag from './openTag';
|
||||
import * as print from './print';
|
||||
import * as renameFolder from './renameFolder';
|
||||
@ -55,6 +56,7 @@ const index:any[] = [
|
||||
openFolderDialog,
|
||||
openItem,
|
||||
openNote,
|
||||
openPdfViewer,
|
||||
openTag,
|
||||
print,
|
||||
renameFolder,
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'openPdfViewer',
|
||||
label: () => _('Open PDF viewer'),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, resourceId: string, pageNo: number) => {
|
||||
|
||||
const resource = await Resource.load(resourceId);
|
||||
if (!resource) throw new Error(`No such resource: ${resourceId}`);
|
||||
if (resource.mime !== 'application/pdf') throw new Error(`Not a PDF: ${resource.mime}`);
|
||||
console.log('Opening PDF', resource);
|
||||
context.dispatch({
|
||||
type: 'DIALOG_OPEN',
|
||||
name: 'pdfViewer',
|
||||
props: {
|
||||
resource,
|
||||
pageNo: pageNo,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
@ -52,6 +52,8 @@ export default function useMessageHandler(scrollWhenReady: any, setScrollWhenRea
|
||||
void CommandService.instance().execute(commandName, ...commandArgs);
|
||||
} else if (msg === 'postMessageService.message') {
|
||||
void PostMessageService.instance().postMessage(arg0);
|
||||
} else if (msg === 'openPdfViewer') {
|
||||
await CommandService.instance().execute('openPdfViewer', arg0.resourceId, arg0.pageNo);
|
||||
} else {
|
||||
await CommandService.instance().execute('openItem', msg);
|
||||
// bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));
|
||||
|
101
packages/app-desktop/gui/PdfViewer.tsx
Normal file
101
packages/app-desktop/gui/PdfViewer.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useRef, useEffect } from 'react';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import bridge from '../services/bridge';
|
||||
import contextMenu from './NoteEditor/utils/contextMenu';
|
||||
import { ContextMenuItemType, ContextMenuOptions } from './NoteEditor/utils/contextMenuUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import styled from 'styled-components';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
|
||||
const Window = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
z-index: 999;
|
||||
background-color: ${(props: any) => props.theme.backgroundColor};
|
||||
color: ${(props: any) => props.theme.color};
|
||||
`;
|
||||
|
||||
const IFrame = styled.iframe`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
dispatch: Function;
|
||||
resource: any;
|
||||
pageNo: number;
|
||||
}
|
||||
|
||||
export default function PdfViewer(props: Props) {
|
||||
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
props.dispatch({
|
||||
type: 'DIALOG_CLOSE',
|
||||
name: 'pdfViewer',
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
const openExternalViewer = useCallback(async () => {
|
||||
await CommandService.instance().execute('openItem', `joplin://${props.resource.id}`);
|
||||
}, [props.resource.id]);
|
||||
|
||||
const textSelected = useCallback(async (text: string) => {
|
||||
if (!text) return;
|
||||
const itemType = ContextMenuItemType.Text;
|
||||
const menu = await contextMenu({
|
||||
itemType,
|
||||
resourceId: null,
|
||||
filename: null,
|
||||
mime: 'text/plain',
|
||||
textToCopy: text,
|
||||
linkToCopy: null,
|
||||
htmlToCopy: '',
|
||||
insertContent: () => { console.warn('insertContent() not implemented'); },
|
||||
} as ContextMenuOptions, props.dispatch);
|
||||
|
||||
menu.popup(bridge().window());
|
||||
}, [props.dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const onMessage_ = async (event: any) =>{
|
||||
if (!event.data || !event.data.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.name === 'close') {
|
||||
onClose();
|
||||
} else if (event.data.name === 'externalViewer') {
|
||||
await openExternalViewer();
|
||||
} else if (event.data.name === 'textSelected') {
|
||||
await textSelected(event.data.text);
|
||||
} else {
|
||||
console.error('Unknown event received', event.data.name);
|
||||
}
|
||||
};
|
||||
const iframe = iframeRef.current;
|
||||
iframe.contentWindow.addEventListener('message', onMessage_);
|
||||
return () => {
|
||||
iframe.contentWindow.removeEventListener('message', onMessage_);
|
||||
};
|
||||
}, [onClose, openExternalViewer, textSelected]);
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
return (
|
||||
<Window theme={theme}>
|
||||
<IFrame src="./vendor/lib/@joplin/pdf-viewer/index.html" x-url={Resource.fullPath(props.resource)}
|
||||
x-appearance={theme.appearance} ref={iframeRef}
|
||||
x-title={props.resource.title}
|
||||
x-anchorpage={props.pageNo}
|
||||
x-type="full"></IFrame>
|
||||
</Window>
|
||||
);
|
||||
}
|
@ -22,6 +22,7 @@ import Dialog from './Dialog';
|
||||
import SyncWizardDialog from './SyncWizard/Dialog';
|
||||
import MasterPasswordDialog from './MasterPasswordDialog/Dialog';
|
||||
import EditFolderDialog from './EditFolderDialog/Dialog';
|
||||
import PdfViewer from './PdfViewer';
|
||||
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||
const { ResourceScreen } = require('./ResourceScreen.js');
|
||||
@ -75,6 +76,11 @@ const registeredDialogs: Record<string, RegisteredDialog> = {
|
||||
return <EditFolderDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
|
||||
},
|
||||
},
|
||||
pdfViewer: {
|
||||
render: (props: RegisteredDialogProps, customProps: any) => {
|
||||
return <PdfViewer key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
|
@ -579,6 +579,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
ipc.textSelected = function(event) {
|
||||
ipcProxySendToHost('contextMenu', {
|
||||
type: 'text',
|
||||
textToCopy: event.text,
|
||||
});
|
||||
}
|
||||
|
||||
ipc.openPdfViewer = function(event) {
|
||||
ipcProxySendToHost('openPdfViewer', { resourceId: event.resourceId, mime: 'application/pdf', pageNo: event.pageNo || 1 });
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', webviewLib.logEnabledEventHandler(e => {
|
||||
if (!window.location.hash) return;
|
||||
|
||||
|
127
packages/pdf-viewer/FullViewer.tsx
Normal file
127
packages/pdf-viewer/FullViewer.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import { useRef, useState, useCallback } from 'react';
|
||||
import * as React from 'react';
|
||||
import usePdfDocument from './hooks/usePdfDocument';
|
||||
import VerticalPages from './VerticalPages';
|
||||
import MessageService from './messageService';
|
||||
import { DownloadButton, PrintButton, OpenLinkButton, CloseButton } from './ui/IconButtons';
|
||||
import ZoomControls from './ui/ZoomControls';
|
||||
import styled from 'styled-components';
|
||||
import GotoInput from './ui/GotoPage';
|
||||
|
||||
require('./fullScreen.css');
|
||||
|
||||
const TitleWrapper = styled.div`
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
min-width: 10rem;
|
||||
max-width: 18rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--secondary);
|
||||
padding: 0.2rem 0.6rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const Title = styled.div`
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.2rem;
|
||||
color: var(--primary);
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
|
||||
export interface FullViewerProps {
|
||||
pdfPath: string;
|
||||
isDarkTheme: boolean;
|
||||
messageService: MessageService;
|
||||
startPage: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function FullViewer(props: FullViewerProps) {
|
||||
const pdfDocument = usePdfDocument(props.pdfPath);
|
||||
const [zoom, setZoom] = useState<number>(1);
|
||||
const [startPage, setStartPage] = useState<number>(props.startPage || 1);
|
||||
const [selectedPage, setSelectedPage] = useState<number>(startPage);
|
||||
const mainViewerRef = useRef<HTMLDivElement>(null);
|
||||
const thubmnailRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onActivePageChange = useCallback((pageNo: number) => {
|
||||
setSelectedPage(pageNo);
|
||||
}, []);
|
||||
|
||||
const goToPage = useCallback((pageNo: number) => {
|
||||
if (pageNo < 1 || pageNo > pdfDocument.pageCount || pageNo === selectedPage) return;
|
||||
setSelectedPage(pageNo);
|
||||
setStartPage(pageNo);
|
||||
}, [pdfDocument, selectedPage]);
|
||||
|
||||
if (!pdfDocument) {
|
||||
return (
|
||||
<div className="full-app loading">
|
||||
<div>Loading pdf..</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="full-app">
|
||||
<div className="top-bar">
|
||||
<div>
|
||||
<TitleWrapper>
|
||||
<Title title={props.title}>{props.title}</Title>
|
||||
<div>{selectedPage} of {pdfDocument.pageCount} pages</div>
|
||||
</TitleWrapper>
|
||||
</div>
|
||||
<div>
|
||||
<ZoomControls onChange={setZoom} zoom={zoom} size={1} />
|
||||
<OpenLinkButton onClick={props.messageService.openExternalViewer} size={1.3} />
|
||||
<PrintButton onClick={pdfDocument?.printPdf} size={1.3}/>
|
||||
<DownloadButton onClick={pdfDocument?.downloadPdf} size={1.3}/>
|
||||
<GotoInput onChange={goToPage} size={1.3} pageCount={pdfDocument.pageCount} currentPage={selectedPage} />
|
||||
</div>
|
||||
<div>
|
||||
<CloseButton onClick={props.messageService.close} size={1.3} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="viewers dark-bg">
|
||||
<div className="pane thumbnail-pane" ref={thubmnailRef}>
|
||||
<VerticalPages
|
||||
pdfDocument={pdfDocument}
|
||||
isDarkTheme={true}
|
||||
rememberScroll={false}
|
||||
container={thubmnailRef}
|
||||
pageGap={16}
|
||||
widthPercent={86}
|
||||
showPageNumbers={true}
|
||||
selectedPage={selectedPage}
|
||||
onPageClick={goToPage}
|
||||
textSelectable={false}
|
||||
zoom={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="pane main-pane" ref={mainViewerRef}>
|
||||
<VerticalPages
|
||||
pdfDocument={pdfDocument}
|
||||
isDarkTheme={true}
|
||||
rememberScroll={false}
|
||||
container={mainViewerRef}
|
||||
zoom={zoom}
|
||||
pageGap={5}
|
||||
anchorPage={startPage}
|
||||
onActivePageChange={onActivePageChange}
|
||||
textSelectable={true}
|
||||
onTextSelect={props.messageService.textSelected}
|
||||
showPageNumbers={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,32 +1,35 @@
|
||||
import { useEffect, useRef, useState, MutableRefObject } from 'react';
|
||||
import { useEffect, useRef, useState, MutableRefObject, useCallback } from 'react';
|
||||
import * as React from 'react';
|
||||
import useIsVisible from './hooks/useIsVisible';
|
||||
import useVisibleOnSelect, { VisibleOnSelect } from './hooks/useVisibleOnSelect';
|
||||
import PdfDocument from './PdfDocument';
|
||||
import { ScaledSize } from './types';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { ScaledSize, RenderRequest } from './types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const PageWrapper = styled.div`
|
||||
|
||||
require('./textLayer.css');
|
||||
|
||||
const PageWrapper = styled.div<{ isSelected?: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
border: solid thin rgba(120, 120, 120, 0.498);
|
||||
border: ${props => props.isSelected ? 'solid 5px #0079ff' : 'solid thin rgba(120, 120, 120, 0.498)'};
|
||||
background: rgb(233, 233, 233);
|
||||
position: relative;
|
||||
border-radius: 0px;
|
||||
border-radius: ${props => props.isSelected ? '0.3rem' : '0px'};
|
||||
`;
|
||||
|
||||
const PageInfo = styled.div`
|
||||
const PageInfo = styled.div<{ isSelected?: boolean }>`
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
padding: 0.3rem;
|
||||
background: rgba(203, 203, 203, 0.509);
|
||||
background: ${props => props.isSelected ? '#0079ff' : 'rgba(203, 203, 203, 0.509)'};
|
||||
border-radius: 0.3rem;
|
||||
font-size: 0.8rem;
|
||||
color: rgba(91, 91, 91, 0.829);
|
||||
color: ${props => props.isSelected ? 'white' : 'rgba(91, 91, 91, 0.829)'};
|
||||
backdrop-filter: blur(0.5rem);
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
@ -43,61 +46,64 @@ export interface PageProps {
|
||||
scaledSize: ScaledSize;
|
||||
isDarkTheme: boolean;
|
||||
container: MutableRefObject<HTMLElement>;
|
||||
showPageNumbers?: boolean;
|
||||
showPageNumbers: boolean;
|
||||
isSelected: boolean;
|
||||
textSelectable: boolean;
|
||||
onTextSelect?: (text: string)=> void;
|
||||
onClick?: (page: number)=> void;
|
||||
onDoubleClick?: (page: number)=> void;
|
||||
}
|
||||
|
||||
|
||||
export default function Page(props: PageProps) {
|
||||
const [error, setError] = useState(null);
|
||||
const [page, setPage] = useState(null);
|
||||
const scaleRef = useRef<number>(null);
|
||||
const timestampRef = useRef<number>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const isVisible = useIsVisible(canvasRef, props.container);
|
||||
const isVisible = useIsVisible(wrapperRef, props.container);
|
||||
useVisibleOnSelect({
|
||||
isVisible,
|
||||
isSelected: props.isSelected,
|
||||
container: props.container,
|
||||
wrapperRef,
|
||||
} as VisibleOnSelect);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible || !page || !props.scaledSize || (scaleRef.current && props.scaledSize.scale === scaleRef.current)) return;
|
||||
try {
|
||||
const viewport = page.getViewport({ scale: props.scaledSize.scale || 1.0 });
|
||||
const canvas = canvasRef.current;
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const pageTimestamp = new Date().getTime();
|
||||
timestampRef.current = pageTimestamp;
|
||||
page.render({
|
||||
canvasContext: ctx,
|
||||
viewport,
|
||||
// Used so that the page rendering is throttled to some extent.
|
||||
// https://stackoverflow.com/questions/18069448/halting-pdf-js-page-rendering
|
||||
continueCallback: function(cont: any) {
|
||||
if (timestampRef.current !== pageTimestamp) {
|
||||
return;
|
||||
}
|
||||
cont();
|
||||
},
|
||||
});
|
||||
const isCancelled = () => props.scaledSize.scale !== scaleRef.current;
|
||||
|
||||
const renderPage = async () => {
|
||||
try {
|
||||
const renderRequest: RenderRequest = {
|
||||
pageNo: props.pageNo,
|
||||
scaledSize: props.scaledSize,
|
||||
getTextLayer: props.textSelectable,
|
||||
isCancelled,
|
||||
};
|
||||
const { canvas, textLayerDiv } = await props.pdfDocument.renderPage(renderRequest);
|
||||
|
||||
wrapperRef.current.appendChild(canvas);
|
||||
if (textLayerDiv) wrapperRef.current.appendChild(textLayerDiv);
|
||||
|
||||
if (canvasRef.current) canvasRef.current.remove();
|
||||
if (textRef.current) textRef.current.remove();
|
||||
|
||||
canvasRef.current = canvas;
|
||||
if (textLayerDiv) textRef.current = textLayerDiv;
|
||||
} catch (error) {
|
||||
if (isCancelled()) return;
|
||||
error.message = `Error rendering page no. ${props.pageNo}: ${error.message}`;
|
||||
setError(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
if (isVisible && props.scaledSize && (props.scaledSize.scale !== scaleRef.current)) {
|
||||
scaleRef.current = props.scaledSize.scale;
|
||||
|
||||
} catch (error) {
|
||||
error.message = `Error rendering page no. ${props.pageNo}: ${error.message}`;
|
||||
setError(error);
|
||||
throw error;
|
||||
void renderPage();
|
||||
}
|
||||
}, [page, props.scaledSize, isVisible, props.pageNo]);
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
if (page || !isVisible || !props.pdfDocument) return;
|
||||
try {
|
||||
const _page = await props.pdfDocument.getPage(props.pageNo);
|
||||
if (event.cancelled) return;
|
||||
setPage(_page);
|
||||
} catch (error) {
|
||||
console.error('Page load error', props.pageNo, error);
|
||||
setError(error);
|
||||
}
|
||||
}, [page, props.scaledSize, isVisible]);
|
||||
}, [props.scaledSize, isVisible, props.textSelectable, props.pageNo, props.pdfDocument]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.focusOnLoad) {
|
||||
@ -106,6 +112,11 @@ export default function Page(props: PageProps) {
|
||||
}
|
||||
}, [props.container, props.focusOnLoad]);
|
||||
|
||||
|
||||
const onClick = useCallback(async (_e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (props.onClick) props.onClick(props.pageNo);
|
||||
}, [props.onClick, props.pageNo]);
|
||||
|
||||
let style: any = {};
|
||||
if (props.scaledSize) {
|
||||
style = {
|
||||
@ -115,15 +126,23 @@ export default function Page(props: PageProps) {
|
||||
};
|
||||
}
|
||||
|
||||
const onContextMenu = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!props.textSelectable || !props.onTextSelect || !window.getSelection()) return;
|
||||
const text = window.getSelection().toString();
|
||||
if (!text) return;
|
||||
props.onTextSelect(text);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, [props.textSelectable, props.onTextSelect]);
|
||||
|
||||
const onDoubleClick = useCallback(() => {
|
||||
if (props.onDoubleClick) props.onDoubleClick(props.pageNo);
|
||||
}, [props.onDoubleClick, props.pageNo]);
|
||||
|
||||
return (
|
||||
<PageWrapper ref={wrapperRef} style={style}>
|
||||
<canvas ref={canvasRef} className="page-canvas" style={style}>
|
||||
<div>
|
||||
{error ? 'ERROR' : 'Loading..'}
|
||||
</div>
|
||||
Page {props.pageNo}
|
||||
</canvas>
|
||||
{props.showPageNumbers && <PageInfo>{props.isAnchored ? '📌' : ''} Page {props.pageNo}</PageInfo>}
|
||||
<PageWrapper onDoubleClick={onDoubleClick} isSelected={!!props.isSelected} onContextMenu={onContextMenu} onClick={onClick} ref={wrapperRef} style={style}>
|
||||
{ error && <div>Error: {error}</div> }
|
||||
{props.showPageNumbers && <PageInfo isSelected={!!props.isSelected}>{props.isAnchored ? '📌' : ''} Page {props.pageNo}</PageInfo>}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { ScaledSize } from './types';
|
||||
import { ScaledSize, RenderRequest, RenderResult } from './types';
|
||||
import { Mutex, MutexInterface, withTimeout } from 'async-mutex';
|
||||
|
||||
|
||||
export default class PdfDocument {
|
||||
public url: string | Uint8Array;
|
||||
private doc: any = null;
|
||||
public pageCount: number = null;
|
||||
private pages: any = {};
|
||||
private rendererMutex: MutexInterface = null;
|
||||
private pageSize: {
|
||||
height: number;
|
||||
width: number;
|
||||
@ -14,6 +17,7 @@ export default class PdfDocument {
|
||||
|
||||
public constructor(document: HTMLDocument) {
|
||||
this.document = document;
|
||||
this.rendererMutex = withTimeout(new Mutex(), 40 * 1000);
|
||||
}
|
||||
|
||||
public loadDoc = async (url: string | Uint8Array) => {
|
||||
@ -74,6 +78,66 @@ export default class PdfDocument {
|
||||
return Math.min(pageNo, this.pageCount);
|
||||
};
|
||||
|
||||
private renderPageImpl = async ({ pageNo, scaledSize, getTextLayer, isCancelled }: RenderRequest): Promise<RenderResult> => {
|
||||
|
||||
const checkCancelled = () => {
|
||||
if (isCancelled()) {
|
||||
throw new Error(`Render cancelled, page: ${pageNo}`);
|
||||
}
|
||||
};
|
||||
|
||||
const page = await this.getPage(pageNo);
|
||||
checkCancelled();
|
||||
|
||||
const canvas = this.document.createElement('canvas');
|
||||
const viewport = page.getViewport({ scale: scaledSize.scale || 1.0 });
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Could not get canvas context');
|
||||
}
|
||||
|
||||
await page.render({
|
||||
canvasContext: ctx,
|
||||
viewport,
|
||||
}).promise;
|
||||
checkCancelled();
|
||||
|
||||
let textLayerDiv = null;
|
||||
if (getTextLayer) {
|
||||
textLayerDiv = this.document.createElement('div');
|
||||
textLayerDiv.classList.add('textLayer');
|
||||
const textFragment = this.document.createDocumentFragment();
|
||||
const txtContext = await page.getTextContent();
|
||||
checkCancelled();
|
||||
// Pass the data to the method for rendering of text over the pdf canvas.
|
||||
textLayerDiv.style.height = `${viewport.height}px`;
|
||||
textLayerDiv.style.width = `${viewport.width}px`;
|
||||
await pdfjsLib.renderTextLayer({
|
||||
textContent: txtContext,
|
||||
enhanceTextSelection: true,
|
||||
container: textFragment,
|
||||
viewport: viewport,
|
||||
textDivs: [],
|
||||
}).promise;
|
||||
textLayerDiv.appendChild(textFragment);
|
||||
}
|
||||
|
||||
canvas.style.height = '100%';
|
||||
canvas.style.width = '100%';
|
||||
|
||||
return { canvas, textLayerDiv };
|
||||
};
|
||||
|
||||
public async renderPage(task: RenderRequest): Promise<RenderResult> {
|
||||
// We're using a render mutex to avoid rendering too many pages at the same time
|
||||
// Which can cause the pdfjs library to abandon some of the in-progress rendering unexpectedly
|
||||
return await this.rendererMutex.runExclusive(async () => {
|
||||
return await this.renderPageImpl(task);
|
||||
});
|
||||
}
|
||||
|
||||
public printPdf = () => {
|
||||
const frame = this.document.createElement('iframe');
|
||||
frame.style.position = 'fixed';
|
||||
|
@ -12,8 +12,8 @@ const PagesHolder = styled.div<{ pageGap: number }>`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-flow: column;
|
||||
width: fit-content;
|
||||
min-width: 100%;
|
||||
width: fit-content;
|
||||
min-height: fit-content;
|
||||
row-gap: ${(props)=> props.pageGap || 2}px;
|
||||
`;
|
||||
@ -22,13 +22,19 @@ export interface VerticalPagesProps {
|
||||
pdfDocument: PdfDocument;
|
||||
isDarkTheme: boolean;
|
||||
anchorPage?: number;
|
||||
rememberScroll?: boolean;
|
||||
rememberScroll: boolean;
|
||||
pdfId?: string;
|
||||
zoom?: number;
|
||||
zoom: number;
|
||||
container: MutableRefObject<HTMLElement>;
|
||||
pageGap: number;
|
||||
showPageNumbers?: boolean;
|
||||
onActivePageChange: (page: number)=> void;
|
||||
widthPercent?: number;
|
||||
showPageNumbers: boolean;
|
||||
selectedPage?: number;
|
||||
textSelectable: boolean;
|
||||
onTextSelect?: (text: string)=> void;
|
||||
onPageClick?: (page: number)=> void;
|
||||
onActivePageChange?: (page: number)=> void;
|
||||
onDoubleClick?: (page: number)=> void;
|
||||
}
|
||||
|
||||
export default function VerticalPages(props: VerticalPagesProps) {
|
||||
@ -63,7 +69,8 @@ export default function VerticalPages(props: VerticalPagesProps) {
|
||||
|
||||
const updateWidth = () => {
|
||||
if (cancelled) return;
|
||||
setContainerWidth(props.container.current.clientWidth);
|
||||
const factor = (props.widthPercent || 100) / 100;
|
||||
setContainerWidth(props.container.current.clientWidth * factor);
|
||||
};
|
||||
|
||||
const onResize = () => {
|
||||
@ -85,7 +92,7 @@ export default function VerticalPages(props: VerticalPagesProps) {
|
||||
resizeTimer = null;
|
||||
}
|
||||
};
|
||||
}, [props.container, props.pdfDocument]);
|
||||
}, [props.container, props.pdfDocument, props.widthPercent]);
|
||||
|
||||
return (<PagesHolder pageGap={props.pageGap || 2} ref={innerContainerEl} >
|
||||
{scaledSize ?
|
||||
@ -94,6 +101,11 @@ export default function VerticalPages(props: VerticalPagesProps) {
|
||||
return <Page pdfDocument={props.pdfDocument} pageNo={i + 1} focusOnLoad={scaledSize && props.anchorPage && props.anchorPage === i + 1}
|
||||
isAnchored={props.anchorPage && props.anchorPage === i + 1}
|
||||
showPageNumbers={props.showPageNumbers}
|
||||
isSelected={scaledSize && props.selectedPage && props.selectedPage === i + 1}
|
||||
onClick={props.onPageClick}
|
||||
textSelectable={props.textSelectable}
|
||||
onTextSelect={props.onTextSelect}
|
||||
onDoubleClick={props.onDoubleClick}
|
||||
isDarkTheme={props.isDarkTheme} scaledSize={scaledSize} container={props.container} key={i} />;
|
||||
}
|
||||
) : 'Calculating size...'
|
||||
|
64
packages/pdf-viewer/fullScreen.css
Normal file
64
packages/pdf-viewer/fullScreen.css
Normal file
@ -0,0 +1,64 @@
|
||||
.full-app{
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: 2.8rem auto;
|
||||
}
|
||||
|
||||
.viewers{
|
||||
width: 100%;
|
||||
height: calc(100vh - 2.8rem);
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-columns: 11rem auto;
|
||||
padding-right: 0.4rem;
|
||||
}
|
||||
|
||||
.pane{
|
||||
display: block;
|
||||
margin: 0px auto;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0rem 0.2rem;
|
||||
position: relative;
|
||||
}
|
||||
.thumbnail-pane{
|
||||
background-color: var(--tertiary);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.top-bar{
|
||||
display: grid;
|
||||
grid-template-columns: 18rem auto 3rem;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
background-color: var(--tertiary);
|
||||
flex-direction: row;
|
||||
}
|
||||
.top-bar > div{
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 0.2rem;
|
||||
flex-direction: row;
|
||||
column-gap: 0.6rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 30rem;
|
||||
margin: 0rem auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 840px) {
|
||||
.viewers{
|
||||
grid-template-columns: 9rem auto;
|
||||
}
|
||||
.top-bar{
|
||||
grid-template-columns: 10rem auto 3rem;
|
||||
}
|
||||
}
|
@ -1,21 +1,37 @@
|
||||
import { useEffect, useState, MutableRefObject } from 'react';
|
||||
import { useEffect, useState, MutableRefObject, useRef } from 'react';
|
||||
|
||||
|
||||
const useIsVisible = (elementRef: MutableRefObject<HTMLElement>, rootRef: MutableRefObject<HTMLElement>) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const lastVisible = useRef(0);
|
||||
const invisibleOn = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
let observer: IntersectionObserver = null;
|
||||
let timeout: number = null;
|
||||
if (elementRef.current) {
|
||||
observer = new IntersectionObserver((entries, _observer) => {
|
||||
let visible = false;
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
visible = true;
|
||||
setIsVisible(true);
|
||||
lastVisible.current = Date.now();
|
||||
if ((invisibleOn.current - lastVisible.current) > 300) {
|
||||
setIsVisible(true);
|
||||
} else {
|
||||
if (!timeout) {
|
||||
timeout = window.setTimeout(() => {
|
||||
if (invisibleOn.current < lastVisible.current) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
timeout = null;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!visible) {
|
||||
invisibleOn.current = Date.now();
|
||||
setIsVisible(false);
|
||||
}
|
||||
}, {
|
||||
@ -29,6 +45,10 @@ const useIsVisible = (elementRef: MutableRefObject<HTMLElement>, rootRef: Mutabl
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
if (timeout) {
|
||||
window.clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
};
|
||||
}, [elementRef, rootRef]);
|
||||
|
||||
|
27
packages/pdf-viewer/hooks/useVisibleOnSelect.ts
Normal file
27
packages/pdf-viewer/hooks/useVisibleOnSelect.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { useRef, useEffect, MutableRefObject } from 'react';
|
||||
|
||||
export interface VisibleOnSelect {
|
||||
container: MutableRefObject<HTMLElement>;
|
||||
wrapperRef: MutableRefObject<HTMLElement>;
|
||||
isVisible: boolean;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
// Used in thumbnail view, to scroll to the newly selected page.
|
||||
|
||||
const useVisibleOnSelect = ({ container, wrapperRef, isVisible, isSelected }: VisibleOnSelect) => {
|
||||
const isVisibleRef = useRef(isVisible);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected && !isVisibleRef.current) {
|
||||
container.current.scrollTop = wrapperRef.current.offsetTop;
|
||||
}
|
||||
}, [isSelected, isVisibleRef, container, wrapperRef]);
|
||||
|
||||
useEffect(() => {
|
||||
isVisibleRef.current = isVisible;
|
||||
} , [isVisible]);
|
||||
|
||||
};
|
||||
|
||||
export default useVisibleOnSelect;
|
@ -4,6 +4,8 @@ shim.setReact(React);
|
||||
import { render } from 'react-dom';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import MiniViewerApp from './miniViewer';
|
||||
import MessageService from './messageService';
|
||||
import FullViewer from './FullViewer';
|
||||
|
||||
require('./common.css');
|
||||
|
||||
@ -15,15 +17,27 @@ const type = window.frameElement.getAttribute('x-type');
|
||||
const appearance = window.frameElement.getAttribute('x-appearance');
|
||||
const anchorPage = Number(window.frameElement.getAttribute('x-anchorPage')) || null;
|
||||
const pdfId = window.frameElement.getAttribute('id');
|
||||
const resourceId = window.frameElement.getAttribute('x-resourceid');
|
||||
const title = window.frameElement.getAttribute('x-title');
|
||||
|
||||
document.documentElement.setAttribute('data-theme', appearance);
|
||||
|
||||
const messageService = new MessageService(type);
|
||||
|
||||
function App() {
|
||||
if (type === 'mini') {
|
||||
return <MiniViewerApp pdfPath={url}
|
||||
isDarkTheme={appearance === 'dark'}
|
||||
anchorPage={anchorPage}
|
||||
pdfId={pdfId} />;
|
||||
pdfId={pdfId}
|
||||
resourceId={resourceId}
|
||||
messageService={messageService}/>;
|
||||
} else if (type === 'full') {
|
||||
return <FullViewer pdfPath={url}
|
||||
isDarkTheme={appearance === 'dark'}
|
||||
startPage={anchorPage || 1}
|
||||
title={title}
|
||||
messageService={messageService} />;
|
||||
}
|
||||
return <div>Error: Unknown app type "{type}"</div>;
|
||||
}
|
||||
|
35
packages/pdf-viewer/messageService.ts
Normal file
35
packages/pdf-viewer/messageService.ts
Normal file
@ -0,0 +1,35 @@
|
||||
export default class MessageService {
|
||||
private viewerType: string;
|
||||
public constructor(type: string) {
|
||||
this.viewerType = type;
|
||||
}
|
||||
private sendMessage = (name: string, data?: any) => {
|
||||
if (this.viewerType === 'full') {
|
||||
const message = {
|
||||
name,
|
||||
...data,
|
||||
};
|
||||
window.postMessage(message, '*');
|
||||
} else if (this.viewerType === 'mini') {
|
||||
const message = {
|
||||
name,
|
||||
data,
|
||||
target: 'webview',
|
||||
};
|
||||
window.parent.postMessage(message, '*');
|
||||
}
|
||||
};
|
||||
public textSelected = (text: string) => {
|
||||
this.sendMessage('textSelected', { text });
|
||||
};
|
||||
public close = () => {
|
||||
this.sendMessage('close');
|
||||
};
|
||||
public openExternalViewer = () => {
|
||||
this.sendMessage('externalViewer');
|
||||
};
|
||||
|
||||
public openFullScreenViewer = (resourceId: string, pageNo: number) => {
|
||||
this.sendMessage('openPdfViewer', { resourceId, pageNo });
|
||||
};
|
||||
}
|
@ -3,6 +3,7 @@ import useIsFocused from './hooks/useIsFocused';
|
||||
import usePdfDocument from './hooks/usePdfDocument';
|
||||
import VerticalPages from './VerticalPages';
|
||||
import ZoomControls from './ui/ZoomControls';
|
||||
import MessageService from './messageService';
|
||||
import { DownloadButton, PrintButton } from './ui/IconButtons';
|
||||
|
||||
require('./miniViewer.css');
|
||||
@ -12,6 +13,8 @@ export interface MiniViewerAppProps {
|
||||
isDarkTheme: boolean;
|
||||
anchorPage: number;
|
||||
pdfId: string;
|
||||
resourceId?: string;
|
||||
messageService: MessageService;
|
||||
}
|
||||
|
||||
export default function MiniViewerApp(props: MiniViewerAppProps) {
|
||||
@ -25,6 +28,10 @@ export default function MiniViewerApp(props: MiniViewerAppProps) {
|
||||
setActivePage(page);
|
||||
}, []);
|
||||
|
||||
const onDoubleClick = useCallback((pageNo: number) => {
|
||||
props.messageService.openFullScreenViewer(props.resourceId, pageNo);
|
||||
}, [props.messageService, props.resourceId]);
|
||||
|
||||
if (!pdfDocument) {
|
||||
return (
|
||||
<div className="mini-app loading">
|
||||
@ -44,6 +51,9 @@ export default function MiniViewerApp(props: MiniViewerAppProps) {
|
||||
container={containerEl}
|
||||
showPageNumbers={true}
|
||||
zoom={zoom}
|
||||
textSelectable={true}
|
||||
onTextSelect={props.messageService.textSelected}
|
||||
onDoubleClick={onDoubleClick}
|
||||
pageGap={2}
|
||||
onActivePageChange={onActivePageChange} />
|
||||
</div>
|
||||
|
@ -40,6 +40,7 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@joplin/lib": "workspace:^",
|
||||
"async-mutex": "^0.4.0",
|
||||
"pdfjs-dist": "^2.14.305",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.9.0",
|
||||
|
91
packages/pdf-viewer/textLayer.css
Normal file
91
packages/pdf-viewer/textLayer.css
Normal file
@ -0,0 +1,91 @@
|
||||
/* Copyright 2014 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.textLayer {
|
||||
position: absolute;
|
||||
text-align: initial;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
text-size-adjust: none;
|
||||
}
|
||||
|
||||
.textLayer span,
|
||||
.textLayer br {
|
||||
color: transparent;
|
||||
position: absolute;
|
||||
white-space: pre;
|
||||
cursor: text;
|
||||
transform-origin: 0% 0%;
|
||||
}
|
||||
|
||||
/* Only necessary in Google Chrome, see issue 14205, and most unfortunately
|
||||
* the problem doesn't show up in "text" reference tests. */
|
||||
.textLayer span.markedContent {
|
||||
top: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.textLayer .highlight {
|
||||
margin: -1px;
|
||||
padding: 1px;
|
||||
background-color: rgba(180, 0, 170, 1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.textLayer .highlight.appended {
|
||||
position: initial;
|
||||
}
|
||||
|
||||
.textLayer .highlight.begin {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.textLayer .highlight.end {
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.textLayer .highlight.middle {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.textLayer .highlight.selected {
|
||||
background-color: rgba(0, 100, 0, 1);
|
||||
}
|
||||
|
||||
/* Avoids https://github.com/mozilla/pdf.js/issues/13840 in Chrome */
|
||||
.textLayer br::selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.textLayer .endOfContent {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.textLayer .endOfContent.active {
|
||||
top: 0;
|
||||
}
|
||||
|
@ -9,3 +9,15 @@ export interface IconButtonProps {
|
||||
size?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface RenderRequest {
|
||||
pageNo: number;
|
||||
scaledSize: ScaledSize;
|
||||
getTextLayer: boolean;
|
||||
isCancelled: ()=> boolean;
|
||||
}
|
||||
|
||||
export interface RenderResult {
|
||||
canvas: HTMLCanvasElement;
|
||||
textLayerDiv: HTMLDivElement;
|
||||
}
|
||||
|
67
packages/pdf-viewer/ui/GotoPage.tsx
Normal file
67
packages/pdf-viewer/ui/GotoPage.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faAngleRight, faAngleLeft } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
const Group = 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;
|
||||
svg:hover {
|
||||
color: var(--secondary);
|
||||
}
|
||||
`;
|
||||
|
||||
const GoToInput = styled.input`
|
||||
ont-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
max-width: 3rem;
|
||||
padding: 0.4rem 0.3rem;
|
||||
background: var(--bg);
|
||||
border: solid 2px transparent;
|
||||
border-radius: 3.5rem;
|
||||
color: var(--tertialry);
|
||||
text-align: center;
|
||||
margin: auto 0.6rem;
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: solid 2px var(--blue);
|
||||
}
|
||||
`;
|
||||
|
||||
export interface GotoInputProps {
|
||||
onChange: (pageNo: number)=> void;
|
||||
size?: number;
|
||||
pageCount: number;
|
||||
currentPage: number;
|
||||
}
|
||||
|
||||
export default function GotoInput(props: GotoInputProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const inputFocus = useCallback(() => {
|
||||
inputRef.current?.select();
|
||||
} , []);
|
||||
|
||||
const onPageNoInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.value.length <= 0) return;
|
||||
const pageNo = parseInt(e.target.value, 10);
|
||||
if (pageNo < 1 || pageNo > props.pageCount || pageNo === props.currentPage) return;
|
||||
props.onChange(pageNo);
|
||||
}, [props.onChange, props.pageCount, props.currentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current.value = props.currentPage.toString();
|
||||
} , [props.currentPage]);
|
||||
|
||||
return (<Group size={props.size || 1}>
|
||||
<FontAwesomeIcon icon={faAngleLeft} title="Previous Page" style={{ cursor: 'pointer' }} onClick={() => props.onChange(props.currentPage - 1)} />
|
||||
<GoToInput onChange={onPageNoInput} placeholder="Page" ref={inputRef} onFocus={inputFocus} />
|
||||
<FontAwesomeIcon icon={faAngleRight} title="Next Page" style={{ cursor: 'pointer' }} onClick={() => props.onChange(props.currentPage + 1)} />
|
||||
</Group>);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPrint, faDownload, IconDefinition } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faPrint, faDownload, faSquareArrowUpRight, faXmark, IconDefinition } from '@fortawesome/free-solid-svg-icons';
|
||||
import { IconButtonProps } from '../types';
|
||||
|
||||
|
||||
@ -43,6 +43,18 @@ function BaseButton({ onClick, icon, name, size, color, hoverColor }: BaseButton
|
||||
);
|
||||
}
|
||||
|
||||
export function OpenLinkButton({ onClick, size, color }: IconButtonProps) {
|
||||
return (
|
||||
<BaseButton onClick={onClick} icon={faSquareArrowUpRight} name='Open in another app' size={size} color={color} />
|
||||
);
|
||||
}
|
||||
|
||||
export function CloseButton({ onClick, size, color }: IconButtonProps) {
|
||||
return (
|
||||
<BaseButton onClick={onClick} icon={faXmark} name='Close' size={size} color={color} hoverColor={'red'} />
|
||||
);
|
||||
}
|
||||
|
||||
export function DownloadButton({ onClick, size, color }: IconButtonProps) {
|
||||
return (
|
||||
<BaseButton onClick={onClick} icon={faDownload} name='Download' size={size} color={color} />
|
||||
|
@ -69,7 +69,7 @@ export default function(link: Link, options: Options, linkIndexes: LinkIndexes)
|
||||
|
||||
return `<iframe src="${src}" x-url="${escapedResourcePath}"
|
||||
x-appearance="${options.theme.appearance}" ${anchorPageNo ? `x-anchorPage="${anchorPageNo}"` : ''} id="${id}"
|
||||
x-type="mini"
|
||||
x-type="mini" x-resourceid="${resourceId}"
|
||||
class="media-player media-pdf"></iframe>`;
|
||||
}
|
||||
|
||||
|
17
yarn.lock
17
yarn.lock
@ -4139,6 +4139,7 @@ __metadata:
|
||||
"@types/react": 16.9.55
|
||||
"@types/react-dom": ^16.9.0
|
||||
"@types/styled-components": ^5.1.25
|
||||
async-mutex: ^0.4.0
|
||||
babel-jest: ^28.1.3
|
||||
css-loader: ^6.7.1
|
||||
jest: ^28.1.3
|
||||
@ -8640,6 +8641,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"async-mutex@npm:^0.4.0":
|
||||
version: 0.4.0
|
||||
resolution: "async-mutex@npm:0.4.0"
|
||||
dependencies:
|
||||
tslib: ^2.4.0
|
||||
checksum: 813a71728b35a4fbfd64dba719f04726d9133c67b577fcd951b7028c4a675a13ee34e69beb82d621f87bf81f5d4f135c4c44be0448550c7db728547244ef71fc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"async-settle@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "async-settle@npm:1.0.0"
|
||||
@ -32672,6 +32682,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tslib@npm:^2.4.0":
|
||||
version: 2.4.0
|
||||
resolution: "tslib@npm:2.4.0"
|
||||
checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tsutils@npm:^3.21.0, tsutils@npm:^3.7.0":
|
||||
version: 3.21.0
|
||||
resolution: "tsutils@npm:3.21.0"
|
||||
|
Loading…
Reference in New Issue
Block a user