{
+ /**
+ * A callback to navigate backward in the book, wrt the natural reading
+ * progression direction.
+ *
+ * If the reading direction is RTL, then the backward navigation is actually
+ * forward in the book.
+ */
+ const onBackwardNavigation = useCallback(() => {
if (invertControls) {
onPaginateForward()
} else {
@@ -30,7 +35,14 @@ export default function EpubNavigationControls({ children }: Props) {
}
}, [invertControls, onPaginateBackward, onPaginateForward])
- const onRightNavigate = useCallback(() => {
+ /**
+ * A callback to navigate forward in the book, wrt the natural reading
+ * progression direction.
+ *
+ * If the reading direction is RTL, then the forward navigation is actually
+ * backwards in the book.
+ */
+ const onForwardNavigation = useCallback(() => {
if (invertControls) {
onPaginateBackward()
} else {
@@ -38,16 +50,21 @@ export default function EpubNavigationControls({ children }: Props) {
}
}, [invertControls, onPaginateBackward, onPaginateForward])
+ /**
+ * A swipe handler to navigate forward or backward in the book.
+ *
+ * Note that the swip handler function semantics are inverted wrt the reading direction.
+ */
const swipeHandlers = useSwipeable({
- onSwipedLeft: onLeftNavigate,
- onSwipedRight: onRightNavigate,
+ onSwipedLeft: onForwardNavigation,
+ onSwipedRight: onBackwardNavigation,
preventScrollOnSwipe: true,
})
return (
-
+
@@ -58,7 +75,7 @@ export default function EpubNavigationControls({ children }: Props) {
/>
{children}
-
+
diff --git a/packages/browser/src/components/readers/image-based/PagedReader.tsx b/packages/browser/src/components/readers/image-based/PagedReader.tsx
index b0ac3e00e..4cd8049a6 100644
--- a/packages/browser/src/components/readers/image-based/PagedReader.tsx
+++ b/packages/browser/src/components/readers/image-based/PagedReader.tsx
@@ -2,9 +2,12 @@ import { mediaQueryKeys } from '@stump/api'
import { queryClient } from '@stump/client'
import type { Media } from '@stump/types'
import clsx from 'clsx'
-import React, { memo, useEffect } from 'react'
+import React, { memo, useEffect, useMemo } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
+import { useSwipeable } from 'react-swipeable'
+import { useMediaMatch, useWindowSize } from 'rooks'
+import { useDetectZoom } from '@/hooks/useDetectZoom'
import { useReaderStore } from '@/stores'
export type PagedReaderProps = {
@@ -32,6 +35,21 @@ function PagedReader({ currentPage, media, onPageChange, getPageUrl }: PagedRead
setShowToolBar: state.setShowToolBar,
showToolBar: state.showToolBar,
}))
+ const { innerWidth } = useWindowSize()
+ const { isZoomed } = useDetectZoom()
+
+ const isMobile = useMediaMatch('(max-width: 768px)')
+ const [imageWidth, setImageWidth] = React.useState(null)
+ /**
+ * If the image width is >= 80% of the screen width, we want to fix the side navigation
+ */
+ const fixSideNavigation = useMemo(() => {
+ if (imageWidth && innerWidth) {
+ return imageWidth >= innerWidth * 0.8
+ } else {
+ return isMobile
+ }
+ }, [imageWidth, innerWidth, isMobile])
/**
* This effect is responsible for updating the current page ref when the current page changes. This was
@@ -42,18 +60,20 @@ function PagedReader({ currentPage, media, onPageChange, getPageUrl }: PagedRead
}, [currentPage])
/**
- * This effect is responsibe for invalidating the in-progress media query when the component unmounts.
- * This is done to ensure that when the user navigates away from the reader, the in-progress media is
- * accurately reflected with the latest reading session.
+ * This effect is primarily responsible for two cleanup tasks:
*
- * Note: This honestly isn't needed, as the cache time is not long enough to warrant this. However, I
- * like being cautious.
+ * 1. Hiding the toolbar when the component unmounts. This is done to ensure that the toolbar is not
+ * visible when the user navigates *back* to a reader again at some point.
+ * 2. Invalidating the in-progress media query when the component unmounts. This is done to ensure that
+ * when the user navigates away from the reader, the in-progress media is accurately reflected with
+ * the latest reading session.
*/
useEffect(() => {
return () => {
- queryClient.invalidateQueries([mediaQueryKeys.getInProgressMedia])
+ setShowToolBar(false)
+ queryClient.invalidateQueries([mediaQueryKeys.getInProgressMedia], { exact: false })
}
- }, [])
+ }, [setShowToolBar])
/**
* A simple function that does a little bit of validation before calling the onPageChange callback.
@@ -94,20 +114,47 @@ function PagedReader({ currentPage, media, onPageChange, getPageUrl }: PagedRead
}
})
+ const swipeHandlers = useSwipeable({
+ delta: 150,
+ onSwipedLeft: () => handlePageChange(currentPage + 1),
+ onSwipedRight: () => handlePageChange(currentPage - 1),
+ preventScrollOnSwipe: true,
+ })
+ const swipeEnabled = useMemo(
+ () => !isZoomed && !showToolBar && isMobile,
+ [isZoomed, showToolBar, isMobile],
+ )
+
+ // TODO: when preloading images, cache the dimensions of the images to better support dynamic resizing
return (
-
)
}
@@ -117,6 +164,8 @@ type SideBarControlProps = {
onClick: () => void
/** The position of the sidebar control */
position: 'left' | 'right'
+ /** Whether the sidebar should be fixed to the screen */
+ fixed: boolean
}
/**
@@ -124,13 +173,12 @@ type SideBarControlProps = {
* clicked, will call the onClick callback. This is used in the `PagedReader` component for
* navigating to the next/previous page.
*/
-function SideBarControl({ onClick, position }: SideBarControlProps) {
+function SideBarControl({ onClick, position, fixed }: SideBarControlProps) {
return (