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.d.ts
|
||||||
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
||||||
packages/app-desktop/gui/MainScreen/commands/openNote.js.map
|
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.d.ts
|
||||||
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
||||||
packages/app-desktop/gui/MainScreen/commands/openTag.js.map
|
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.d.ts
|
||||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
||||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js.map
|
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.d.ts
|
||||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js.map
|
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.d.ts
|
||||||
packages/lib/versionInfo.js
|
packages/lib/versionInfo.js
|
||||||
packages/lib/versionInfo.js.map
|
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.d.ts
|
||||||
packages/pdf-viewer/Page.js
|
packages/pdf-viewer/Page.js
|
||||||
packages/pdf-viewer/Page.js.map
|
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.d.ts
|
||||||
packages/pdf-viewer/hooks/useScrollSaver.js
|
packages/pdf-viewer/hooks/useScrollSaver.js
|
||||||
packages/pdf-viewer/hooks/useScrollSaver.js.map
|
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.d.ts
|
||||||
packages/pdf-viewer/main.js
|
packages/pdf-viewer/main.js
|
||||||
packages/pdf-viewer/main.js.map
|
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.d.ts
|
||||||
packages/pdf-viewer/miniViewer.js
|
packages/pdf-viewer/miniViewer.js
|
||||||
packages/pdf-viewer/miniViewer.js.map
|
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.d.ts
|
||||||
packages/pdf-viewer/types.js
|
packages/pdf-viewer/types.js
|
||||||
packages/pdf-viewer/types.js.map
|
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.d.ts
|
||||||
packages/pdf-viewer/ui/IconButtons.js
|
packages/pdf-viewer/ui/IconButtons.js
|
||||||
packages/pdf-viewer/ui/IconButtons.js.map
|
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.d.ts
|
||||||
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
||||||
packages/app-desktop/gui/MainScreen/commands/openNote.js.map
|
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.d.ts
|
||||||
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
||||||
packages/app-desktop/gui/MainScreen/commands/openTag.js.map
|
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.d.ts
|
||||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
||||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js.map
|
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.d.ts
|
||||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js.map
|
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.d.ts
|
||||||
packages/lib/versionInfo.js
|
packages/lib/versionInfo.js
|
||||||
packages/lib/versionInfo.js.map
|
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.d.ts
|
||||||
packages/pdf-viewer/Page.js
|
packages/pdf-viewer/Page.js
|
||||||
packages/pdf-viewer/Page.js.map
|
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.d.ts
|
||||||
packages/pdf-viewer/hooks/useScrollSaver.js
|
packages/pdf-viewer/hooks/useScrollSaver.js
|
||||||
packages/pdf-viewer/hooks/useScrollSaver.js.map
|
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.d.ts
|
||||||
packages/pdf-viewer/main.js
|
packages/pdf-viewer/main.js
|
||||||
packages/pdf-viewer/main.js.map
|
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.d.ts
|
||||||
packages/pdf-viewer/miniViewer.js
|
packages/pdf-viewer/miniViewer.js
|
||||||
packages/pdf-viewer/miniViewer.js.map
|
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.d.ts
|
||||||
packages/pdf-viewer/types.js
|
packages/pdf-viewer/types.js
|
||||||
packages/pdf-viewer/types.js.map
|
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.d.ts
|
||||||
packages/pdf-viewer/ui/IconButtons.js
|
packages/pdf-viewer/ui/IconButtons.js
|
||||||
packages/pdf-viewer/ui/IconButtons.js.map
|
packages/pdf-viewer/ui/IconButtons.js.map
|
||||||
|
@ -15,6 +15,7 @@ import * as openFolder from './openFolder';
|
|||||||
import * as openFolderDialog from './openFolderDialog';
|
import * as openFolderDialog from './openFolderDialog';
|
||||||
import * as openItem from './openItem';
|
import * as openItem from './openItem';
|
||||||
import * as openNote from './openNote';
|
import * as openNote from './openNote';
|
||||||
|
import * as openPdfViewer from './openPdfViewer';
|
||||||
import * as openTag from './openTag';
|
import * as openTag from './openTag';
|
||||||
import * as print from './print';
|
import * as print from './print';
|
||||||
import * as renameFolder from './renameFolder';
|
import * as renameFolder from './renameFolder';
|
||||||
@ -55,6 +56,7 @@ const index:any[] = [
|
|||||||
openFolderDialog,
|
openFolderDialog,
|
||||||
openItem,
|
openItem,
|
||||||
openNote,
|
openNote,
|
||||||
|
openPdfViewer,
|
||||||
openTag,
|
openTag,
|
||||||
print,
|
print,
|
||||||
renameFolder,
|
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);
|
void CommandService.instance().execute(commandName, ...commandArgs);
|
||||||
} else if (msg === 'postMessageService.message') {
|
} else if (msg === 'postMessageService.message') {
|
||||||
void PostMessageService.instance().postMessage(arg0);
|
void PostMessageService.instance().postMessage(arg0);
|
||||||
|
} else if (msg === 'openPdfViewer') {
|
||||||
|
await CommandService.instance().execute('openPdfViewer', arg0.resourceId, arg0.pageNo);
|
||||||
} else {
|
} else {
|
||||||
await CommandService.instance().execute('openItem', msg);
|
await CommandService.instance().execute('openItem', msg);
|
||||||
// bridge().showErrorMessageBox(_('Unsupported link or message: %s', 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 SyncWizardDialog from './SyncWizard/Dialog';
|
||||||
import MasterPasswordDialog from './MasterPasswordDialog/Dialog';
|
import MasterPasswordDialog from './MasterPasswordDialog/Dialog';
|
||||||
import EditFolderDialog from './EditFolderDialog/Dialog';
|
import EditFolderDialog from './EditFolderDialog/Dialog';
|
||||||
|
import PdfViewer from './PdfViewer';
|
||||||
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
||||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||||
const { ResourceScreen } = require('./ResourceScreen.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}/>;
|
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`
|
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 => {
|
window.addEventListener('hashchange', webviewLib.logEnabledEventHandler(e => {
|
||||||
if (!window.location.hash) return;
|
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 * as React from 'react';
|
||||||
import useIsVisible from './hooks/useIsVisible';
|
import useIsVisible from './hooks/useIsVisible';
|
||||||
|
import useVisibleOnSelect, { VisibleOnSelect } from './hooks/useVisibleOnSelect';
|
||||||
import PdfDocument from './PdfDocument';
|
import PdfDocument from './PdfDocument';
|
||||||
import { ScaledSize } from './types';
|
import { ScaledSize, RenderRequest } from './types';
|
||||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const PageWrapper = styled.div`
|
|
||||||
|
require('./textLayer.css');
|
||||||
|
|
||||||
|
const PageWrapper = styled.div<{ isSelected?: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: hidden;
|
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);
|
background: rgb(233, 233, 233);
|
||||||
position: relative;
|
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;
|
position: absolute;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
left: 0.5rem;
|
left: 0.5rem;
|
||||||
padding: 0.3rem;
|
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;
|
border-radius: 0.3rem;
|
||||||
font-size: 0.8rem;
|
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);
|
backdrop-filter: blur(0.5rem);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -43,61 +46,64 @@ export interface PageProps {
|
|||||||
scaledSize: ScaledSize;
|
scaledSize: ScaledSize;
|
||||||
isDarkTheme: boolean;
|
isDarkTheme: boolean;
|
||||||
container: MutableRefObject<HTMLElement>;
|
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) {
|
export default function Page(props: PageProps) {
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [page, setPage] = useState(null);
|
|
||||||
const scaleRef = useRef<number>(null);
|
const scaleRef = useRef<number>(null);
|
||||||
const timestampRef = useRef<number>(null);
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const textRef = useRef<HTMLDivElement>(null);
|
||||||
const wrapperRef = 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(() => {
|
useEffect(() => {
|
||||||
if (!isVisible || !page || !props.scaledSize || (scaleRef.current && props.scaledSize.scale === scaleRef.current)) return;
|
const isCancelled = () => props.scaledSize.scale !== scaleRef.current;
|
||||||
try {
|
|
||||||
const viewport = page.getViewport({ scale: props.scaledSize.scale || 1.0 });
|
const renderPage = async () => {
|
||||||
const canvas = canvasRef.current;
|
try {
|
||||||
canvas.width = viewport.width;
|
const renderRequest: RenderRequest = {
|
||||||
canvas.height = viewport.height;
|
pageNo: props.pageNo,
|
||||||
const ctx = canvas.getContext('2d');
|
scaledSize: props.scaledSize,
|
||||||
const pageTimestamp = new Date().getTime();
|
getTextLayer: props.textSelectable,
|
||||||
timestampRef.current = pageTimestamp;
|
isCancelled,
|
||||||
page.render({
|
};
|
||||||
canvasContext: ctx,
|
const { canvas, textLayerDiv } = await props.pdfDocument.renderPage(renderRequest);
|
||||||
viewport,
|
|
||||||
// Used so that the page rendering is throttled to some extent.
|
wrapperRef.current.appendChild(canvas);
|
||||||
// https://stackoverflow.com/questions/18069448/halting-pdf-js-page-rendering
|
if (textLayerDiv) wrapperRef.current.appendChild(textLayerDiv);
|
||||||
continueCallback: function(cont: any) {
|
|
||||||
if (timestampRef.current !== pageTimestamp) {
|
if (canvasRef.current) canvasRef.current.remove();
|
||||||
return;
|
if (textRef.current) textRef.current.remove();
|
||||||
}
|
|
||||||
cont();
|
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;
|
scaleRef.current = props.scaledSize.scale;
|
||||||
|
void renderPage();
|
||||||
} catch (error) {
|
|
||||||
error.message = `Error rendering page no. ${props.pageNo}: ${error.message}`;
|
|
||||||
setError(error);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}, [page, props.scaledSize, isVisible, props.pageNo]);
|
|
||||||
|
|
||||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
}, [props.scaledSize, isVisible, props.textSelectable, props.pageNo, props.pdfDocument]);
|
||||||
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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.focusOnLoad) {
|
if (props.focusOnLoad) {
|
||||||
@ -106,6 +112,11 @@ export default function Page(props: PageProps) {
|
|||||||
}
|
}
|
||||||
}, [props.container, props.focusOnLoad]);
|
}, [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 = {};
|
let style: any = {};
|
||||||
if (props.scaledSize) {
|
if (props.scaledSize) {
|
||||||
style = {
|
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 (
|
return (
|
||||||
<PageWrapper ref={wrapperRef} style={style}>
|
<PageWrapper onDoubleClick={onDoubleClick} isSelected={!!props.isSelected} onContextMenu={onContextMenu} onClick={onClick} ref={wrapperRef} style={style}>
|
||||||
<canvas ref={canvasRef} className="page-canvas" style={style}>
|
{ error && <div>Error: {error}</div> }
|
||||||
<div>
|
{props.showPageNumbers && <PageInfo isSelected={!!props.isSelected}>{props.isAnchored ? '📌' : ''} Page {props.pageNo}</PageInfo>}
|
||||||
{error ? 'ERROR' : 'Loading..'}
|
|
||||||
</div>
|
|
||||||
Page {props.pageNo}
|
|
||||||
</canvas>
|
|
||||||
{props.showPageNumbers && <PageInfo>{props.isAnchored ? '📌' : ''} Page {props.pageNo}</PageInfo>}
|
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import * as pdfjsLib from 'pdfjs-dist';
|
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 {
|
export default class PdfDocument {
|
||||||
public url: string | Uint8Array;
|
public url: string | Uint8Array;
|
||||||
private doc: any = null;
|
private doc: any = null;
|
||||||
public pageCount: number = null;
|
public pageCount: number = null;
|
||||||
private pages: any = {};
|
private pages: any = {};
|
||||||
|
private rendererMutex: MutexInterface = null;
|
||||||
private pageSize: {
|
private pageSize: {
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
@ -14,6 +17,7 @@ export default class PdfDocument {
|
|||||||
|
|
||||||
public constructor(document: HTMLDocument) {
|
public constructor(document: HTMLDocument) {
|
||||||
this.document = document;
|
this.document = document;
|
||||||
|
this.rendererMutex = withTimeout(new Mutex(), 40 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadDoc = async (url: string | Uint8Array) => {
|
public loadDoc = async (url: string | Uint8Array) => {
|
||||||
@ -74,6 +78,66 @@ export default class PdfDocument {
|
|||||||
return Math.min(pageNo, this.pageCount);
|
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 = () => {
|
public printPdf = () => {
|
||||||
const frame = this.document.createElement('iframe');
|
const frame = this.document.createElement('iframe');
|
||||||
frame.style.position = 'fixed';
|
frame.style.position = 'fixed';
|
||||||
|
@ -12,8 +12,8 @@ const PagesHolder = styled.div<{ pageGap: number }>`
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
width: fit-content;
|
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
width: fit-content;
|
||||||
min-height: fit-content;
|
min-height: fit-content;
|
||||||
row-gap: ${(props)=> props.pageGap || 2}px;
|
row-gap: ${(props)=> props.pageGap || 2}px;
|
||||||
`;
|
`;
|
||||||
@ -22,13 +22,19 @@ export interface VerticalPagesProps {
|
|||||||
pdfDocument: PdfDocument;
|
pdfDocument: PdfDocument;
|
||||||
isDarkTheme: boolean;
|
isDarkTheme: boolean;
|
||||||
anchorPage?: number;
|
anchorPage?: number;
|
||||||
rememberScroll?: boolean;
|
rememberScroll: boolean;
|
||||||
pdfId?: string;
|
pdfId?: string;
|
||||||
zoom?: number;
|
zoom: number;
|
||||||
container: MutableRefObject<HTMLElement>;
|
container: MutableRefObject<HTMLElement>;
|
||||||
pageGap: number;
|
pageGap: number;
|
||||||
showPageNumbers?: boolean;
|
widthPercent?: number;
|
||||||
onActivePageChange: (page: number)=> void;
|
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) {
|
export default function VerticalPages(props: VerticalPagesProps) {
|
||||||
@ -63,7 +69,8 @@ export default function VerticalPages(props: VerticalPagesProps) {
|
|||||||
|
|
||||||
const updateWidth = () => {
|
const updateWidth = () => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setContainerWidth(props.container.current.clientWidth);
|
const factor = (props.widthPercent || 100) / 100;
|
||||||
|
setContainerWidth(props.container.current.clientWidth * factor);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onResize = () => {
|
const onResize = () => {
|
||||||
@ -85,7 +92,7 @@ export default function VerticalPages(props: VerticalPagesProps) {
|
|||||||
resizeTimer = null;
|
resizeTimer = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [props.container, props.pdfDocument]);
|
}, [props.container, props.pdfDocument, props.widthPercent]);
|
||||||
|
|
||||||
return (<PagesHolder pageGap={props.pageGap || 2} ref={innerContainerEl} >
|
return (<PagesHolder pageGap={props.pageGap || 2} ref={innerContainerEl} >
|
||||||
{scaledSize ?
|
{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}
|
return <Page pdfDocument={props.pdfDocument} pageNo={i + 1} focusOnLoad={scaledSize && props.anchorPage && props.anchorPage === i + 1}
|
||||||
isAnchored={props.anchorPage && props.anchorPage === i + 1}
|
isAnchored={props.anchorPage && props.anchorPage === i + 1}
|
||||||
showPageNumbers={props.showPageNumbers}
|
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} />;
|
isDarkTheme={props.isDarkTheme} scaledSize={scaledSize} container={props.container} key={i} />;
|
||||||
}
|
}
|
||||||
) : 'Calculating size...'
|
) : '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 useIsVisible = (elementRef: MutableRefObject<HTMLElement>, rootRef: MutableRefObject<HTMLElement>) => {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const lastVisible = useRef(0);
|
||||||
|
const invisibleOn = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let observer: IntersectionObserver = null;
|
let observer: IntersectionObserver = null;
|
||||||
|
let timeout: number = null;
|
||||||
if (elementRef.current) {
|
if (elementRef.current) {
|
||||||
observer = new IntersectionObserver((entries, _observer) => {
|
observer = new IntersectionObserver((entries, _observer) => {
|
||||||
let visible = false;
|
let visible = false;
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
visible = true;
|
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) {
|
if (!visible) {
|
||||||
|
invisibleOn.current = Date.now();
|
||||||
setIsVisible(false);
|
setIsVisible(false);
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
@ -29,6 +45,10 @@ const useIsVisible = (elementRef: MutableRefObject<HTMLElement>, rootRef: Mutabl
|
|||||||
if (observer) {
|
if (observer) {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
}
|
}
|
||||||
|
if (timeout) {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
timeout = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [elementRef, rootRef]);
|
}, [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 { render } from 'react-dom';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import MiniViewerApp from './miniViewer';
|
import MiniViewerApp from './miniViewer';
|
||||||
|
import MessageService from './messageService';
|
||||||
|
import FullViewer from './FullViewer';
|
||||||
|
|
||||||
require('./common.css');
|
require('./common.css');
|
||||||
|
|
||||||
@ -15,15 +17,27 @@ const type = window.frameElement.getAttribute('x-type');
|
|||||||
const appearance = window.frameElement.getAttribute('x-appearance');
|
const appearance = window.frameElement.getAttribute('x-appearance');
|
||||||
const anchorPage = Number(window.frameElement.getAttribute('x-anchorPage')) || null;
|
const anchorPage = Number(window.frameElement.getAttribute('x-anchorPage')) || null;
|
||||||
const pdfId = window.frameElement.getAttribute('id');
|
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);
|
document.documentElement.setAttribute('data-theme', appearance);
|
||||||
|
|
||||||
|
const messageService = new MessageService(type);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
if (type === 'mini') {
|
if (type === 'mini') {
|
||||||
return <MiniViewerApp pdfPath={url}
|
return <MiniViewerApp pdfPath={url}
|
||||||
isDarkTheme={appearance === 'dark'}
|
isDarkTheme={appearance === 'dark'}
|
||||||
anchorPage={anchorPage}
|
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>;
|
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 usePdfDocument from './hooks/usePdfDocument';
|
||||||
import VerticalPages from './VerticalPages';
|
import VerticalPages from './VerticalPages';
|
||||||
import ZoomControls from './ui/ZoomControls';
|
import ZoomControls from './ui/ZoomControls';
|
||||||
|
import MessageService from './messageService';
|
||||||
import { DownloadButton, PrintButton } from './ui/IconButtons';
|
import { DownloadButton, PrintButton } from './ui/IconButtons';
|
||||||
|
|
||||||
require('./miniViewer.css');
|
require('./miniViewer.css');
|
||||||
@ -12,6 +13,8 @@ export interface MiniViewerAppProps {
|
|||||||
isDarkTheme: boolean;
|
isDarkTheme: boolean;
|
||||||
anchorPage: number;
|
anchorPage: number;
|
||||||
pdfId: string;
|
pdfId: string;
|
||||||
|
resourceId?: string;
|
||||||
|
messageService: MessageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MiniViewerApp(props: MiniViewerAppProps) {
|
export default function MiniViewerApp(props: MiniViewerAppProps) {
|
||||||
@ -25,6 +28,10 @@ export default function MiniViewerApp(props: MiniViewerAppProps) {
|
|||||||
setActivePage(page);
|
setActivePage(page);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onDoubleClick = useCallback((pageNo: number) => {
|
||||||
|
props.messageService.openFullScreenViewer(props.resourceId, pageNo);
|
||||||
|
}, [props.messageService, props.resourceId]);
|
||||||
|
|
||||||
if (!pdfDocument) {
|
if (!pdfDocument) {
|
||||||
return (
|
return (
|
||||||
<div className="mini-app loading">
|
<div className="mini-app loading">
|
||||||
@ -44,6 +51,9 @@ export default function MiniViewerApp(props: MiniViewerAppProps) {
|
|||||||
container={containerEl}
|
container={containerEl}
|
||||||
showPageNumbers={true}
|
showPageNumbers={true}
|
||||||
zoom={zoom}
|
zoom={zoom}
|
||||||
|
textSelectable={true}
|
||||||
|
onTextSelect={props.messageService.textSelected}
|
||||||
|
onDoubleClick={onDoubleClick}
|
||||||
pageGap={2}
|
pageGap={2}
|
||||||
onActivePageChange={onActivePageChange} />
|
onActivePageChange={onActivePageChange} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@joplin/lib": "workspace:^",
|
"@joplin/lib": "workspace:^",
|
||||||
|
"async-mutex": "^0.4.0",
|
||||||
"pdfjs-dist": "^2.14.305",
|
"pdfjs-dist": "^2.14.305",
|
||||||
"react": "16.13.1",
|
"react": "16.13.1",
|
||||||
"react-dom": "16.9.0",
|
"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;
|
size?: number;
|
||||||
color?: string;
|
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 React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
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';
|
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) {
|
export function DownloadButton({ onClick, size, color }: IconButtonProps) {
|
||||||
return (
|
return (
|
||||||
<BaseButton onClick={onClick} icon={faDownload} name='Download' size={size} color={color} />
|
<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}"
|
return `<iframe src="${src}" x-url="${escapedResourcePath}"
|
||||||
x-appearance="${options.theme.appearance}" ${anchorPageNo ? `x-anchorPage="${anchorPageNo}"` : ''} id="${id}"
|
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>`;
|
class="media-player media-pdf"></iframe>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
17
yarn.lock
17
yarn.lock
@ -4139,6 +4139,7 @@ __metadata:
|
|||||||
"@types/react": 16.9.55
|
"@types/react": 16.9.55
|
||||||
"@types/react-dom": ^16.9.0
|
"@types/react-dom": ^16.9.0
|
||||||
"@types/styled-components": ^5.1.25
|
"@types/styled-components": ^5.1.25
|
||||||
|
async-mutex: ^0.4.0
|
||||||
babel-jest: ^28.1.3
|
babel-jest: ^28.1.3
|
||||||
css-loader: ^6.7.1
|
css-loader: ^6.7.1
|
||||||
jest: ^28.1.3
|
jest: ^28.1.3
|
||||||
@ -8640,6 +8641,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"async-settle@npm:^1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "async-settle@npm:1.0.0"
|
resolution: "async-settle@npm:1.0.0"
|
||||||
@ -32672,6 +32682,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"tsutils@npm:^3.21.0, tsutils@npm:^3.7.0":
|
||||||
version: 3.21.0
|
version: 3.21.0
|
||||||
resolution: "tsutils@npm:3.21.0"
|
resolution: "tsutils@npm:3.21.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user