Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Fix image reader navigation (tablet) #329

Merged
merged 2 commits into from
Apr 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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<number | null>(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
Expand Down Expand Up @@ -94,20 +112,46 @@ 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],
)

return (
<div className="relative flex h-full w-full items-center justify-center">
<SideBarControl position="left" onClick={() => handlePageChange(currentPage - 1)} />
<div
className="relative flex h-full w-full items-center justify-center"
{...(swipeEnabled ? swipeHandlers : {})}
>
<SideBarControl
fixed={fixSideNavigation}
position="left"
onClick={() => handlePageChange(currentPage - 1)}
/>
{/* TODO: better error handling for the loaded image */}
<img
className="z-30 max-h-full w-full select-none md:w-auto"
src={getPageUrl(currentPage)}
onLoad={(e) => {
const img = e.target as HTMLImageElement
setImageWidth(img.width)
}}
onError={(err) => {
// @ts-expect-error: is oke
err.target.src = '/favicon.png'
}}
onClick={() => setShowToolBar(!showToolBar)}
/>
<SideBarControl position="right" onClick={() => handlePageChange(currentPage + 1)} />
<SideBarControl
fixed={fixSideNavigation}
position="right"
onClick={() => handlePageChange(currentPage + 1)}
/>
</div>
)
}
Expand All @@ -117,20 +161,21 @@ type SideBarControlProps = {
onClick: () => void
/** The position of the sidebar control */
position: 'left' | 'right'
/** Whether the sidebar should be fixed to the screen */
fixed: boolean
}

/**
* A component that renders an invisible div on either the left or right side of the screen that, when
* 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 (
<div
className={clsx(
'z-50 h-full border border-transparent transition-all duration-300',
'fixed w-[10%] active:border-edge-200 active:bg-background-200 active:bg-opacity-50',
'sm:relative sm:flex sm:w-full sm:flex-shrink',
'z-50 h-full shrink-0 border border-transparent transition-all duration-300 active:border-edge-200 active:bg-background-200 active:bg-opacity-50',
fixed ? 'fixed w-[10%]' : 'relative flex flex-1 flex-grow',
{ 'right-0': position === 'right' },
{ 'left-0': position === 'left' },
)}
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { useDetectZoom } from './useDetectZoom'
export { useLayoutMode } from './useLayoutMode'
export { usePageParam } from './usePageParam'
export { usePreferences } from './usePreferences'
Expand Down
23 changes: 23 additions & 0 deletions packages/browser/src/hooks/useDetectZoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect, useState } from 'react'

/**
* A hook to detect the zoom level of the browser.
*/
export function useDetectZoom() {
const [zoom, setZoom] = useState<number>()

useEffect(() => {
const handleResize = () => {
setZoom(window.visualViewport?.scale)
}

window.visualViewport?.addEventListener('resize', handleResize)
handleResize()
return () => window.visualViewport?.removeEventListener('resize', handleResize)
}, [])

return {
isZoomed: zoom !== undefined && zoom > 1,
ratio: zoom,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,8 @@ export default function BookClubNavigation() {
key={tab.to}
underline={false}
className={cx('whitespace-nowrap border-b-2 px-1 py-3 text-sm font-medium', {
'border-brand-500 text-brand-600 dark:text-brand-400': tab.isActive,
'border-transparent text-gray-800 hover:border-gray-200 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-700 dark:hover:text-gray-200':
!tab.isActive,
'border-brand-500 text-brand-500': tab.isActive,
'border-transparent text-muted hover:border-edge': !tab.isActive,
})}
>
{tab.label}
Expand Down
5 changes: 2 additions & 3 deletions packages/browser/src/scenes/library/LibraryNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,8 @@ export default function LibraryNavigation() {
underline={false}
onMouseEnter={tab.onHover}
className={cn('whitespace-nowrap border-b-2 px-1 py-3 text-sm font-medium', {
'border-brand-500 text-brand-600 dark:text-brand-400': tab.isActive,
'border-transparent text-gray-800 hover:border-gray-200 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-700 dark:hover:text-gray-200':
!tab.isActive,
'border-brand-500 text-brand-500': tab.isActive,
'border-transparent text-muted hover:border-edge': !tab.isActive,
})}
>
{tab.label}
Expand Down
5 changes: 2 additions & 3 deletions packages/browser/src/scenes/series/SeriesNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,8 @@ export default function SeriesNavigation() {
className={cn(
'whitespace-nowrap border-b-2 px-1 py-3 text-sm font-medium',
{
'border-brand-500 text-brand-600 dark:text-brand-400': tab.isActive,
'border-transparent text-gray-800 hover:border-gray-200 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-700 dark:hover:text-gray-200':
!tab.isActive,
'border-brand-500 text-brand-500': tab.isActive,
'border-transparent text-muted hover:border-edge': !tab.isActive,
},
// {
// 'pointer-events-none !text-opacity-40': tab.disabled,
Expand Down
Loading