mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Viewer update and autozoom (#4800)
Updated embed PDF Added Autozoom Added file page size to metadata for use in calculations for autozoom, will come in handy elsewhere. --------- Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
co-authored by
James Brunton
parent
3cf89b6ede
commit
ce6b2460d8
@@ -0,0 +1,311 @@
|
||||
import { MutableRefObject } from 'react';
|
||||
import { SpreadMode } from '@embedpdf/plugin-spread/react';
|
||||
import {
|
||||
ViewerBridgeRegistry,
|
||||
ScrollState,
|
||||
ZoomState,
|
||||
} from '@app/contexts/viewer/viewerBridges';
|
||||
|
||||
export interface ScrollActions {
|
||||
scrollToPage: (page: number) => void;
|
||||
scrollToFirstPage: () => void;
|
||||
scrollToPreviousPage: () => void;
|
||||
scrollToNextPage: () => void;
|
||||
scrollToLastPage: () => void;
|
||||
}
|
||||
|
||||
export interface ZoomActions {
|
||||
zoomIn: () => void;
|
||||
zoomOut: () => void;
|
||||
toggleMarqueeZoom: () => void;
|
||||
requestZoom: (level: number) => void;
|
||||
}
|
||||
|
||||
export interface PanActions {
|
||||
enablePan: () => void;
|
||||
disablePan: () => void;
|
||||
togglePan: () => void;
|
||||
}
|
||||
|
||||
export interface SelectionActions {
|
||||
copyToClipboard: () => void;
|
||||
getSelectedText: () => string;
|
||||
getFormattedSelection: () => any;
|
||||
}
|
||||
|
||||
export interface SpreadActions {
|
||||
setSpreadMode: (mode: SpreadMode) => void;
|
||||
getSpreadMode: () => SpreadMode | null;
|
||||
toggleSpreadMode: () => void;
|
||||
}
|
||||
|
||||
export interface RotationActions {
|
||||
rotateForward: () => void;
|
||||
rotateBackward: () => void;
|
||||
setRotation: (rotation: number) => void;
|
||||
getRotation: () => number;
|
||||
}
|
||||
|
||||
export interface SearchActions {
|
||||
search: (query: string) => Promise<any> | undefined;
|
||||
next: () => void;
|
||||
previous: () => void;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
export interface ExportActions {
|
||||
download: () => void;
|
||||
saveAsCopy: () => Promise<ArrayBuffer | null>;
|
||||
}
|
||||
|
||||
export interface ViewerActionsBundle {
|
||||
scrollActions: ScrollActions;
|
||||
zoomActions: ZoomActions;
|
||||
panActions: PanActions;
|
||||
selectionActions: SelectionActions;
|
||||
spreadActions: SpreadActions;
|
||||
rotationActions: RotationActions;
|
||||
searchActions: SearchActions;
|
||||
exportActions: ExportActions;
|
||||
}
|
||||
|
||||
interface ViewerActionDependencies {
|
||||
registry: MutableRefObject<ViewerBridgeRegistry>;
|
||||
getScrollState: () => ScrollState;
|
||||
getZoomState: () => ZoomState;
|
||||
triggerImmediateZoomUpdate: (percent: number) => void;
|
||||
}
|
||||
|
||||
export function createViewerActions({
|
||||
registry,
|
||||
getScrollState,
|
||||
getZoomState,
|
||||
triggerImmediateZoomUpdate,
|
||||
}: ViewerActionDependencies): ViewerActionsBundle {
|
||||
const scrollActions: ScrollActions = {
|
||||
scrollToPage: (page: number) => {
|
||||
const api = registry.current.scroll?.api;
|
||||
if (api?.scrollToPage) {
|
||||
api.scrollToPage({ pageNumber: page });
|
||||
}
|
||||
},
|
||||
scrollToFirstPage: () => {
|
||||
const api = registry.current.scroll?.api;
|
||||
if (api?.scrollToPage) {
|
||||
api.scrollToPage({ pageNumber: 1 });
|
||||
}
|
||||
},
|
||||
scrollToPreviousPage: () => {
|
||||
const api = registry.current.scroll?.api;
|
||||
if (api?.scrollToPreviousPage) {
|
||||
api.scrollToPreviousPage();
|
||||
}
|
||||
},
|
||||
scrollToNextPage: () => {
|
||||
const api = registry.current.scroll?.api;
|
||||
if (api?.scrollToNextPage) {
|
||||
api.scrollToNextPage();
|
||||
}
|
||||
},
|
||||
scrollToLastPage: () => {
|
||||
const api = registry.current.scroll?.api;
|
||||
const state = getScrollState();
|
||||
if (api?.scrollToPage && state.totalPages > 0) {
|
||||
api.scrollToPage({ pageNumber: state.totalPages });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const zoomActions: ZoomActions = {
|
||||
zoomIn: () => {
|
||||
const api = registry.current.zoom?.api;
|
||||
if (api?.zoomIn) {
|
||||
const currentState = getZoomState();
|
||||
const newPercent = Math.min(
|
||||
Math.round(currentState.zoomPercent * 1.2),
|
||||
300
|
||||
);
|
||||
triggerImmediateZoomUpdate(newPercent);
|
||||
api.zoomIn();
|
||||
}
|
||||
},
|
||||
zoomOut: () => {
|
||||
const api = registry.current.zoom?.api;
|
||||
if (api?.zoomOut) {
|
||||
const currentState = getZoomState();
|
||||
const newPercent = Math.max(
|
||||
Math.round(currentState.zoomPercent / 1.2),
|
||||
20
|
||||
);
|
||||
triggerImmediateZoomUpdate(newPercent);
|
||||
api.zoomOut();
|
||||
}
|
||||
},
|
||||
toggleMarqueeZoom: () => {
|
||||
const api = registry.current.zoom?.api;
|
||||
if (api?.toggleMarqueeZoom) {
|
||||
api.toggleMarqueeZoom();
|
||||
}
|
||||
},
|
||||
requestZoom: (level: number) => {
|
||||
const api = registry.current.zoom?.api;
|
||||
if (api?.requestZoom) {
|
||||
api.requestZoom(level);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const panActions: PanActions = {
|
||||
enablePan: () => {
|
||||
const api = registry.current.pan?.api;
|
||||
if (api?.enable) {
|
||||
api.enable();
|
||||
}
|
||||
},
|
||||
disablePan: () => {
|
||||
const api = registry.current.pan?.api;
|
||||
if (api?.disable) {
|
||||
api.disable();
|
||||
}
|
||||
},
|
||||
togglePan: () => {
|
||||
const api = registry.current.pan?.api;
|
||||
if (api?.toggle) {
|
||||
api.toggle();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const selectionActions: SelectionActions = {
|
||||
copyToClipboard: () => {
|
||||
const api = registry.current.selection?.api;
|
||||
if (api?.copyToClipboard) {
|
||||
api.copyToClipboard();
|
||||
}
|
||||
},
|
||||
getSelectedText: () => {
|
||||
const api = registry.current.selection?.api;
|
||||
if (api?.getSelectedText) {
|
||||
return api.getSelectedText() ?? '';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
getFormattedSelection: () => {
|
||||
const api = registry.current.selection?.api;
|
||||
if (api?.getFormattedSelection) {
|
||||
return api.getFormattedSelection();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const spreadActions: SpreadActions = {
|
||||
setSpreadMode: (mode: SpreadMode) => {
|
||||
const api = registry.current.spread?.api;
|
||||
if (api?.setSpreadMode) {
|
||||
api.setSpreadMode(mode);
|
||||
}
|
||||
},
|
||||
getSpreadMode: () => {
|
||||
const api = registry.current.spread?.api;
|
||||
if (api?.getSpreadMode) {
|
||||
return api.getSpreadMode();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
toggleSpreadMode: () => {
|
||||
const api = registry.current.spread?.api;
|
||||
if (api?.toggleSpreadMode) {
|
||||
api.toggleSpreadMode();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const rotationActions: RotationActions = {
|
||||
rotateForward: () => {
|
||||
const api = registry.current.rotation?.api;
|
||||
if (api?.rotateForward) {
|
||||
api.rotateForward();
|
||||
}
|
||||
},
|
||||
rotateBackward: () => {
|
||||
const api = registry.current.rotation?.api;
|
||||
if (api?.rotateBackward) {
|
||||
api.rotateBackward();
|
||||
}
|
||||
},
|
||||
setRotation: (rotation: number) => {
|
||||
const api = registry.current.rotation?.api;
|
||||
if (api?.setRotation) {
|
||||
api.setRotation(rotation);
|
||||
}
|
||||
},
|
||||
getRotation: () => {
|
||||
const api = registry.current.rotation?.api;
|
||||
if (api?.getRotation) {
|
||||
return api.getRotation();
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
};
|
||||
|
||||
const searchActions: SearchActions = {
|
||||
search: (query: string) => {
|
||||
const api = registry.current.search?.api;
|
||||
if (api?.search) {
|
||||
return api.search(query);
|
||||
}
|
||||
},
|
||||
next: () => {
|
||||
const api = registry.current.search?.api;
|
||||
if (api?.next) {
|
||||
api.next();
|
||||
}
|
||||
},
|
||||
previous: () => {
|
||||
const api = registry.current.search?.api;
|
||||
if (api?.previous) {
|
||||
api.previous();
|
||||
}
|
||||
},
|
||||
clear: () => {
|
||||
const api = registry.current.search?.api;
|
||||
if (api?.clear) {
|
||||
api.clear();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const exportActions: ExportActions = {
|
||||
download: () => {
|
||||
const api = registry.current.export?.api;
|
||||
if (api?.download) {
|
||||
api.download();
|
||||
}
|
||||
},
|
||||
saveAsCopy: async () => {
|
||||
const api = registry.current.export?.api;
|
||||
if (api?.saveAsCopy) {
|
||||
try {
|
||||
const result = api.saveAsCopy();
|
||||
return await result.toPromise();
|
||||
} catch (error) {
|
||||
console.error('Failed to save PDF copy:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
scrollActions,
|
||||
zoomActions,
|
||||
panActions,
|
||||
selectionActions,
|
||||
spreadActions,
|
||||
rotationActions,
|
||||
searchActions,
|
||||
exportActions,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { SpreadMode } from '@embedpdf/plugin-spread/react';
|
||||
|
||||
export interface ScrollAPIWrapper {
|
||||
scrollToPage: (params: { pageNumber: number }) => void;
|
||||
scrollToPreviousPage: () => void;
|
||||
scrollToNextPage: () => void;
|
||||
}
|
||||
|
||||
export interface ZoomAPIWrapper {
|
||||
zoomIn: () => void;
|
||||
zoomOut: () => void;
|
||||
toggleMarqueeZoom: () => void;
|
||||
requestZoom: (level: number) => void;
|
||||
}
|
||||
|
||||
export interface PanAPIWrapper {
|
||||
enable: () => void;
|
||||
disable: () => void;
|
||||
toggle: () => void;
|
||||
makePanDefault: () => void;
|
||||
}
|
||||
|
||||
export interface SelectionAPIWrapper {
|
||||
copyToClipboard: () => void;
|
||||
getSelectedText: () => string | any;
|
||||
getFormattedSelection: () => any;
|
||||
}
|
||||
|
||||
export interface SpreadAPIWrapper {
|
||||
setSpreadMode: (mode: SpreadMode) => void;
|
||||
getSpreadMode: () => SpreadMode | null;
|
||||
toggleSpreadMode: () => void;
|
||||
SpreadMode: typeof SpreadMode;
|
||||
}
|
||||
|
||||
export interface RotationAPIWrapper {
|
||||
rotateForward: () => void;
|
||||
rotateBackward: () => void;
|
||||
setRotation: (rotation: number) => void;
|
||||
getRotation: () => number;
|
||||
}
|
||||
|
||||
export interface SearchAPIWrapper {
|
||||
search: (query: string) => Promise<any>;
|
||||
clear: () => void;
|
||||
next: () => void;
|
||||
previous: () => void;
|
||||
goToResult: (index: number) => void;
|
||||
}
|
||||
|
||||
export interface ThumbnailAPIWrapper {
|
||||
renderThumb: (pageIndex: number, scale: number) => {
|
||||
toPromise: () => Promise<Blob>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExportAPIWrapper {
|
||||
download: () => void;
|
||||
saveAsCopy: () => { toPromise: () => Promise<ArrayBuffer> };
|
||||
}
|
||||
|
||||
export interface ScrollState {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface ZoomState {
|
||||
currentZoom: number;
|
||||
zoomPercent: number;
|
||||
}
|
||||
|
||||
export interface PanState {
|
||||
isPanning: boolean;
|
||||
}
|
||||
|
||||
export interface SelectionState {
|
||||
hasSelection: boolean;
|
||||
}
|
||||
|
||||
export interface SpreadState {
|
||||
spreadMode: SpreadMode;
|
||||
isDualPage: boolean;
|
||||
}
|
||||
|
||||
export interface RotationState {
|
||||
rotation: number;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
pageIndex: number;
|
||||
rects: Array<{
|
||||
origin: { x: number; y: number };
|
||||
size: { width: number; height: number };
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface SearchState {
|
||||
results: SearchResult[] | null;
|
||||
activeIndex: number;
|
||||
}
|
||||
|
||||
export interface ExportState {
|
||||
canExport: boolean;
|
||||
}
|
||||
|
||||
export interface BridgeRef<TState = unknown, TApi = unknown> {
|
||||
state: TState;
|
||||
api: TApi;
|
||||
}
|
||||
|
||||
export interface BridgeStateMap {
|
||||
scroll: ScrollState;
|
||||
zoom: ZoomState;
|
||||
pan: PanState;
|
||||
selection: SelectionState;
|
||||
spread: SpreadState;
|
||||
rotation: RotationState;
|
||||
search: SearchState;
|
||||
thumbnail: unknown;
|
||||
export: ExportState;
|
||||
}
|
||||
|
||||
export interface BridgeApiMap {
|
||||
scroll: ScrollAPIWrapper;
|
||||
zoom: ZoomAPIWrapper;
|
||||
pan: PanAPIWrapper;
|
||||
selection: SelectionAPIWrapper;
|
||||
spread: SpreadAPIWrapper;
|
||||
rotation: RotationAPIWrapper;
|
||||
search: SearchAPIWrapper;
|
||||
thumbnail: ThumbnailAPIWrapper;
|
||||
export: ExportAPIWrapper;
|
||||
}
|
||||
|
||||
export type BridgeKey = keyof BridgeStateMap;
|
||||
|
||||
export type ViewerBridgeRegistry = {
|
||||
[K in BridgeKey]: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]> | null;
|
||||
};
|
||||
|
||||
export const createBridgeRegistry = (): ViewerBridgeRegistry => ({
|
||||
scroll: null,
|
||||
zoom: null,
|
||||
pan: null,
|
||||
selection: null,
|
||||
spread: null,
|
||||
rotation: null,
|
||||
search: null,
|
||||
thumbnail: null,
|
||||
export: null,
|
||||
});
|
||||
|
||||
export function registerBridge<K extends BridgeKey>(
|
||||
registry: ViewerBridgeRegistry,
|
||||
type: K,
|
||||
ref: BridgeRef<BridgeStateMap[K], BridgeApiMap[K]>
|
||||
): void {
|
||||
registry[type] = ref as ViewerBridgeRegistry[K];
|
||||
}
|
||||
|
||||
export function getBridgeState<K extends BridgeKey>(
|
||||
registry: ViewerBridgeRegistry,
|
||||
type: K,
|
||||
fallback: BridgeStateMap[K]
|
||||
): BridgeStateMap[K] {
|
||||
return registry[type]?.state ?? fallback;
|
||||
}
|
||||
|
||||
export function getBridgeApi<K extends BridgeKey>(
|
||||
registry: ViewerBridgeRegistry,
|
||||
type: K
|
||||
): BridgeApiMap[K] | null {
|
||||
return registry[type]?.api ?? null;
|
||||
}
|
||||
Reference in New Issue
Block a user