feat(pdf): support panning on PDFs (#2981)
This also closes #2978 and closes #2875.
This commit is contained in:
@@ -115,15 +115,17 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [progressInfoMode]);
|
||||
|
||||
const isMobile = appService?.isMobile || window.innerWidth < 640;
|
||||
|
||||
return (
|
||||
<div
|
||||
role='presentation'
|
||||
className={clsx(
|
||||
'progressinfo absolute flex items-center justify-between font-sans',
|
||||
'pointer-events-auto bottom-0',
|
||||
'progressinfo absolute bottom-0 flex items-center justify-between font-sans',
|
||||
isEink ? 'text-sm font-normal' : 'text-neutral-content text-xs font-extralight',
|
||||
isVertical ? 'writing-vertical-rl' : 'w-full',
|
||||
isScrolled && !isVertical && 'bg-base-100',
|
||||
isMobile ? 'pointer-events-auto' : 'pointer-events-none',
|
||||
)}
|
||||
onClick={() => cycleProgressInfoModes()}
|
||||
aria-label={[
|
||||
|
||||
@@ -38,7 +38,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ bookKey, setIsDropdownOpen }) => {
|
||||
const { user } = useAuth();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { getConfig, getBookData } = useBookDataStore();
|
||||
const { setSettingsDialogOpen } = useSettingsStore();
|
||||
const { setSettingsDialogOpen, setSettingsDialogBookKey } = useSettingsStore();
|
||||
const { getView, getViewSettings, getViewState, setViewSettings } = useReaderStore();
|
||||
const config = getConfig(bookKey)!;
|
||||
const bookData = getBookData(bookKey)!;
|
||||
@@ -62,6 +62,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ bookKey, setIsDropdownOpen }) => {
|
||||
|
||||
const openFontLayoutMenu = () => {
|
||||
setIsDropdownOpen?.(false);
|
||||
setSettingsDialogBookKey(bookKey);
|
||||
setSettingsDialogOpen(true);
|
||||
};
|
||||
|
||||
@@ -258,7 +259,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ bookKey, setIsDropdownOpen }) => {
|
||||
shortcut='Shift+J'
|
||||
Icon={isScrolledMode ? MdCheck : undefined}
|
||||
onClick={toggleScrolledMode}
|
||||
disabled={bookData.bookDoc?.rendition?.layout === 'pre-paginated'}
|
||||
disabled={bookData.isFixedLayout}
|
||||
/>
|
||||
|
||||
<hr aria-hidden='true' className='border-base-300 my-1' />
|
||||
|
||||
@@ -67,7 +67,7 @@ const DesktopFooterBar: React.FC<FooterBarChildProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute hidden h-full w-full items-center gap-x-4 px-4 sm:flex'
|
||||
className='absolute hidden h-8 w-full items-center gap-x-4 px-4 sm:flex'
|
||||
style={{ bottom: isMobile ? `${gridInsets.bottom * 0.33}px` : '0px' }}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -66,11 +66,11 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
);
|
||||
|
||||
const handleGoPrevPage = useCallback(() => {
|
||||
viewPagination(view, viewSettings, 'left');
|
||||
viewPagination(view, viewSettings, 'left', 'page');
|
||||
}, [view, viewSettings]);
|
||||
|
||||
const handleGoNextPage = useCallback(() => {
|
||||
viewPagination(view, viewSettings, 'right');
|
||||
viewPagination(view, viewSettings, 'right', 'page');
|
||||
}, [view, viewSettings]);
|
||||
|
||||
const handleGoPrevSection = useCallback(() => {
|
||||
@@ -211,7 +211,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
appService?.hasRoundedWindow && 'rounded-window-bottom-right',
|
||||
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-bottom-left',
|
||||
isHoveredAnim && 'hover-bar-anim',
|
||||
needHorizontalScroll && 'sm:!bottom-3 sm:!h-7',
|
||||
needHorizontalScroll && 'sm:!bottom-3 sm:!h-10 sm:justify-end',
|
||||
isVisible
|
||||
? 'pointer-events-auto translate-y-0 opacity-100'
|
||||
: 'pointer-events-none translate-y-full opacity-0 sm:translate-y-0',
|
||||
@@ -245,6 +245,9 @@ const FooterBar: React.FC<FooterBarProps> = ({
|
||||
<MobileFooterBar {...commonProps} />
|
||||
<DesktopFooterBar {...commonProps} />
|
||||
</div>
|
||||
{isVisible && needHorizontalScroll && (
|
||||
<div className='bg-base-100 pointer-events-none absolute bottom-0 left-0 hidden h-3 w-full sm:block' />
|
||||
)}
|
||||
|
||||
<TTSControl bookKey={bookKey} gridInsets={gridInsets} />
|
||||
</>
|
||||
|
||||
@@ -46,12 +46,26 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
|
||||
const goLeft = () => {
|
||||
const viewSettings = getViewSettings(sideBarBookKey ?? '');
|
||||
viewPagination(getView(sideBarBookKey), viewSettings, 'left');
|
||||
viewPagination(getView(sideBarBookKey), viewSettings, 'left', 'pan', distance);
|
||||
};
|
||||
|
||||
const goRight = () => {
|
||||
const viewSettings = getViewSettings(sideBarBookKey ?? '');
|
||||
viewPagination(getView(sideBarBookKey), viewSettings, 'right');
|
||||
viewPagination(getView(sideBarBookKey), viewSettings, 'right', 'pan', distance);
|
||||
};
|
||||
|
||||
const goUp = (event?: KeyboardEvent | MessageEvent) => {
|
||||
const view = getView(sideBarBookKey);
|
||||
const viewSettings = getViewSettings(sideBarBookKey ?? '');
|
||||
if (view?.renderer.scrolled && event instanceof MessageEvent) return;
|
||||
viewPagination(view, viewSettings, 'up', 'pan', distance);
|
||||
};
|
||||
|
||||
const goDown = (event?: KeyboardEvent | MessageEvent) => {
|
||||
const view = getView(sideBarBookKey);
|
||||
const viewSettings = getViewSettings(sideBarBookKey ?? '');
|
||||
if (view?.renderer.scrolled && event instanceof MessageEvent) return;
|
||||
viewPagination(view, viewSettings, 'down', 'pan', distance);
|
||||
};
|
||||
|
||||
const goPrevSection = () => {
|
||||
@@ -82,32 +96,6 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
getView(sideBarBookKey)?.next(distance);
|
||||
};
|
||||
|
||||
const goPrevArrowUp = (event?: KeyboardEvent | MessageEvent) => {
|
||||
const view = getView(sideBarBookKey);
|
||||
if (
|
||||
view?.renderer.scrolled &&
|
||||
event instanceof MessageEvent &&
|
||||
event.data.type === 'iframe-keydown'
|
||||
) {
|
||||
// already handled in the iframe for better smoothness
|
||||
return;
|
||||
}
|
||||
view?.prev(distance);
|
||||
};
|
||||
|
||||
const goNextArrowDown = (event?: KeyboardEvent | MessageEvent) => {
|
||||
const view = getView(sideBarBookKey);
|
||||
if (
|
||||
view?.renderer.scrolled &&
|
||||
event instanceof MessageEvent &&
|
||||
event.data.type === 'iframe-keydown'
|
||||
) {
|
||||
// already handled in the iframe for better smoothness
|
||||
return;
|
||||
}
|
||||
view?.next(distance);
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
getView(sideBarBookKey)?.history.back();
|
||||
};
|
||||
@@ -251,10 +239,10 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
onQuitApp: quitApp,
|
||||
onGoLeft: goLeft,
|
||||
onGoRight: goRight,
|
||||
onGoUp: goUp,
|
||||
onGoDown: goDown,
|
||||
onGoPrev: goPrev,
|
||||
onGoNext: goNext,
|
||||
onGoPrevArrowUp: goPrevArrowUp,
|
||||
onGoNextArrowDown: goNextArrowDown,
|
||||
onGoHalfPageDown: goHalfPageDown,
|
||||
onGoHalfPageUp: goHalfPageUp,
|
||||
onGoPrevSection: goPrevSection,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { tauriGetWindowLogicalPosition } from '@/utils/window';
|
||||
export type ScrollSource = 'touch' | 'mouse';
|
||||
|
||||
type PaginationSide = 'left' | 'right' | 'up' | 'down';
|
||||
type PaginationMode = 'page' | 'section';
|
||||
type PaginationMode = 'pan' | 'page' | 'section';
|
||||
|
||||
const swapLeftRight = (side: PaginationSide) => {
|
||||
if (side === 'left') return 'right';
|
||||
@@ -20,11 +20,36 @@ const swapLeftRight = (side: PaginationSide) => {
|
||||
return side;
|
||||
};
|
||||
|
||||
const isPanningView = (view: FoliateView | null, viewSettings: ViewSettings | null | undefined) => {
|
||||
if (!view || !viewSettings) return false;
|
||||
return (
|
||||
view.book.rendition?.layout === 'pre-paginated' &&
|
||||
(viewSettings.zoomLevel > 100 || viewSettings.zoomMode !== 'fit-page')
|
||||
);
|
||||
};
|
||||
|
||||
const hasHorizontalPanning = (
|
||||
view: FoliateView | null,
|
||||
viewSettings: ViewSettings | null | undefined,
|
||||
) => {
|
||||
if (!view || !viewSettings) return false;
|
||||
return isPanningView(view, viewSettings) && view.isOverflowX();
|
||||
};
|
||||
|
||||
const hasVerticalPanning = (
|
||||
view: FoliateView | null,
|
||||
viewSettings: ViewSettings | null | undefined,
|
||||
) => {
|
||||
if (!view || !viewSettings) return false;
|
||||
return isPanningView(view, viewSettings) && view.isOverflowY();
|
||||
};
|
||||
|
||||
export const viewPagination = (
|
||||
view: FoliateView | null,
|
||||
viewSettings: ViewSettings | null | undefined,
|
||||
side: PaginationSide,
|
||||
mode: PaginationMode = 'page',
|
||||
panDistance: number = 50,
|
||||
) => {
|
||||
if (!view || !viewSettings) return;
|
||||
const renderer = view.renderer;
|
||||
@@ -38,25 +63,37 @@ export const viewPagination = (
|
||||
const scrollingOverlap = viewSettings.scrollingOverlap;
|
||||
const distance = size - scrollingOverlap - (showHeader ? 44 : 0) - (showFooter ? 44 : 0);
|
||||
switch (mode) {
|
||||
case 'page':
|
||||
return side === 'left' || side === 'up' ? view.prev(distance) : view.next(distance);
|
||||
case 'section':
|
||||
if (side === 'left' || side === 'up') {
|
||||
return view.renderer.prevSection?.();
|
||||
} else {
|
||||
return view.renderer.nextSection?.();
|
||||
}
|
||||
case 'pan':
|
||||
case 'page':
|
||||
default:
|
||||
return side === 'left' || side === 'up' ? view.prev(distance) : view.next(distance);
|
||||
}
|
||||
} else if (mode === 'pan' && isPanningView(view, viewSettings)) {
|
||||
if (hasHorizontalPanning(view, viewSettings) && (side === 'left' || side === 'right')) {
|
||||
return view.pan(side === 'left' ? -panDistance : panDistance, 0);
|
||||
} else if (hasVerticalPanning(view, viewSettings) && (side === 'up' || side === 'down')) {
|
||||
return view.pan(0, side === 'up' ? -panDistance : panDistance);
|
||||
} else {
|
||||
return side === 'left' || side === 'up' ? view.prev() : view.next();
|
||||
}
|
||||
} else {
|
||||
switch (mode) {
|
||||
case 'page':
|
||||
return side === 'left' || side === 'up' ? view.prev() : view.next();
|
||||
case 'section':
|
||||
if (side === 'left' || side === 'up') {
|
||||
return view.renderer.prevSection?.();
|
||||
} else {
|
||||
return view.renderer.nextSection?.();
|
||||
}
|
||||
case 'pan':
|
||||
case 'page':
|
||||
default:
|
||||
return side === 'left' || side === 'up' ? view.prev() : view.next();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -142,7 +179,7 @@ export const usePagination = (
|
||||
} else if (
|
||||
msg.data.type === 'iframe-wheel' &&
|
||||
!viewSettings.scrolled &&
|
||||
(!bookData.isFixedLayout || viewSettings.zoomLevel <= 100)
|
||||
!isPanningView(viewRef.current, viewSettings)
|
||||
) {
|
||||
// The wheel event is handled by the iframe itself in scrolled mode.
|
||||
const { deltaY } = msg.data;
|
||||
@@ -172,7 +209,7 @@ export const usePagination = (
|
||||
} else if (
|
||||
msg.type === 'touch-swipe' &&
|
||||
bookData.isFixedLayout &&
|
||||
viewSettings!.zoomLevel <= 100
|
||||
!isPanningView(viewRef.current, viewSettings)
|
||||
) {
|
||||
const { deltaX, deltaY, deltaT } = msg.detail;
|
||||
const vx = Math.abs(deltaX / deltaT);
|
||||
|
||||
@@ -209,6 +209,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={isScrolledMode}
|
||||
disabled={bookData?.isFixedLayout}
|
||||
onChange={() => setScrolledMode(!isScrolledMode)}
|
||||
/>
|
||||
</div>
|
||||
@@ -218,6 +219,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
checked={isContinuousScroll}
|
||||
disabled={bookData?.isFixedLayout}
|
||||
onChange={() => setIsContinuousScroll(!isContinuousScroll)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -23,12 +23,12 @@ const DEFAULT_SHORTCUTS = {
|
||||
onToggleFullscreen: ['F11'],
|
||||
onCloseWindow: ['ctrl+w', 'cmd+w'],
|
||||
onQuitApp: ['ctrl+q', 'cmd+q'],
|
||||
onGoLeft: ['ArrowLeft', 'PageUp', 'h', 'shift+ '],
|
||||
onGoRight: ['ArrowRight', 'PageDown', 'l', ' '],
|
||||
onGoNext: ['j'],
|
||||
onGoPrev: ['k'],
|
||||
onGoNextArrowDown: ['ArrowDown'],
|
||||
onGoPrevArrowUp: ['ArrowUp'],
|
||||
onGoLeft: ['ArrowLeft', 'h', 'shift+ '],
|
||||
onGoRight: ['ArrowRight', 'l', ' '],
|
||||
onGoUp: ['ArrowUp', 'k'],
|
||||
onGoDown: ['ArrowDown', 'j'],
|
||||
onGoNext: ['shift+j', 'shift+ArrowRight', 'shift+ArrowDown', 'PageDown'],
|
||||
onGoPrev: ['shift+k', 'shift+ArrowLeft', 'shift+ArrowUp', 'PageUp'],
|
||||
onGoLeftSection: ['opt+ArrowLeft', 'alt+ArrowLeft'],
|
||||
onGoRightSection: ['opt+ArrowRight', 'alt+ArrowRight'],
|
||||
onGoPrevSection: ['opt+ArrowUp', 'alt+ArrowUp'],
|
||||
|
||||
@@ -14,6 +14,9 @@ export interface FoliateView extends HTMLElement {
|
||||
goToFraction: (fraction: number) => void;
|
||||
prev: (distance?: number) => void;
|
||||
next: (distance?: number) => void;
|
||||
pan: (dx: number, dy: number) => void;
|
||||
isOverflowX: () => boolean;
|
||||
isOverflowY: () => boolean;
|
||||
goLeft: () => void;
|
||||
goRight: () => void;
|
||||
getCFI: (index: number, range: Range) => string;
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: 47be9d814b...133ae252b9
Reference in New Issue
Block a user