From 8e59763bba508ba82091234ac8a7f3d3e4568f31 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:07:14 +0200 Subject: [PATCH] Delete deprecated reader files --- src/App.tsx | 9 +- src/modules/manga/Manga.types.ts | 2 +- src/modules/metadata/Metadata.constants.ts | 1 - src/modules/reader/Reader.constants.ts | 1 - src/modules/reader/Reader.types.ts | 33 - .../reader/components/ReaderNavBar.tsx | 398 ------------ .../components/ReaderSettingsOptions.tsx | 147 ----- .../reader/components/page/DoublePage.tsx | 87 --- src/modules/reader/components/page/Page.tsx | 113 ---- .../reader/components/page/PageNumber.tsx | 39 -- .../components/pager/DoublePagedPager.tsx | 219 ------- .../components/pager/HorizontalPager.tsx | 208 ------- .../reader/components/pager/PagedPager.tsx | 123 ---- .../reader/components/pager/VerticalPager.tsx | 221 ------- .../reader/screens/DefaultReaderSettings.tsx | 77 --- src/modules/reader/screens/Reader.tsx | 576 ------------------ 16 files changed, 3 insertions(+), 2251 deletions(-) delete mode 100644 src/modules/reader/components/ReaderNavBar.tsx delete mode 100644 src/modules/reader/components/ReaderSettingsOptions.tsx delete mode 100644 src/modules/reader/components/page/DoublePage.tsx delete mode 100644 src/modules/reader/components/page/Page.tsx delete mode 100644 src/modules/reader/components/page/PageNumber.tsx delete mode 100644 src/modules/reader/components/pager/DoublePagedPager.tsx delete mode 100644 src/modules/reader/components/pager/HorizontalPager.tsx delete mode 100644 src/modules/reader/components/pager/PagedPager.tsx delete mode 100644 src/modules/reader/components/pager/VerticalPager.tsx delete mode 100644 src/modules/reader/screens/DefaultReaderSettings.tsx delete mode 100644 src/modules/reader/screens/Reader.tsx diff --git a/src/App.tsx b/src/App.tsx index 7621c3b8..1650d2f9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,7 +26,6 @@ const { Browse } = loadable(() => import('@/modules/browse/screens/Browse.tsx'), const { DownloadQueue } = loadable(() => import('@/modules/downloads/screens/DownloadQueue.tsx'), lazyLoadFallback); const { Library } = loadable(() => import('@/modules/library/screens/Library.tsx'), lazyLoadFallback); const { Manga } = loadable(() => import('@/modules/manga/screens/Manga.tsx'), lazyLoadFallback); -const { Reader } = loadable(() => import('@/modules/reader/screens/Reader.tsx'), lazyLoadFallback); const { SearchAll } = loadable(() => import('@/modules/global-search/screens/SearchAll.tsx'), lazyLoadFallback); const { Settings } = loadable(() => import('@/modules/settings/screens/Settings.tsx'), lazyLoadFallback); const { About } = loadable(() => import('@/modules/settings/screens/About.tsx'), lazyLoadFallback); @@ -35,10 +34,6 @@ const { CategorySettings } = loadable( () => import('@/modules/category/screens/CategorySettings.tsx'), lazyLoadFallback, ); -const { DefaultReaderSettings } = loadable( - () => import('@/modules/reader/screens/DefaultReaderSettings.tsx'), - lazyLoadFallback, -); const { SourceConfigure } = loadable(() => import('@/modules/source/screens/SourceConfigure.tsx'), lazyLoadFallback); const { SourceMangas } = loadable(() => import('@/modules/source/screens/SourceMangas.tsx'), lazyLoadFallback); const { Updates } = loadable(() => import('@/modules/updates/screens/Updates.tsx'), lazyLoadFallback); @@ -119,7 +114,7 @@ const MainApp = () => { } /> } /> } /> - } /> + } /> } /> @@ -164,7 +159,7 @@ const MainApp = () => { const ReaderApp = () => ( - } /> + diff --git a/src/modules/manga/Manga.types.ts b/src/modules/manga/Manga.types.ts index fd1f3e1a..5f4414d7 100644 --- a/src/modules/manga/Manga.types.ts +++ b/src/modules/manga/Manga.types.ts @@ -12,7 +12,7 @@ import { SelectableCollectionReturnType } from '@/modules/collection/hooks/useSe import { useManageMangaLibraryState } from '@/modules/manga/hooks/useManageMangaLibraryState.tsx'; import { ChapterType, MangaReaderFieldsFragment, MangaType, TrackRecordType } from '@/lib/graphql/generated/graphql.ts'; import { SingleModeProps } from '@/modules/manga/components/MangaActionMenuItems.tsx'; -import { IReaderSettings } from '@/modules/reader/Reader.types'; +import { IReaderSettings } from '@/modules/reader/Reader.types.ts'; import { GridLayout } from '@/modules/core/Core.types.ts'; export type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select' | 'duplicate'; diff --git a/src/modules/metadata/Metadata.constants.ts b/src/modules/metadata/Metadata.constants.ts index 94b49499..c56c0da0 100644 --- a/src/modules/metadata/Metadata.constants.ts +++ b/src/modules/metadata/Metadata.constants.ts @@ -17,7 +17,6 @@ const APP_METADATA_OBJECT: Record = { skipDupChapters: undefined, fitPageToWindow: undefined, scalePage: undefined, - readerType: undefined, offsetFirstPage: undefined, migration: undefined, deleteChaptersManuallyMarkedRead: undefined, diff --git a/src/modules/reader/Reader.constants.ts b/src/modules/reader/Reader.constants.ts index 3f3b400b..77b197cf 100644 --- a/src/modules/reader/Reader.constants.ts +++ b/src/modules/reader/Reader.constants.ts @@ -15,7 +15,6 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = { skipDupChapters: true, fitPageToWindow: true, scalePage: false, - readerType: 'ContinuesVertical', offsetFirstPage: false, readerWidth: 50, }; diff --git a/src/modules/reader/Reader.types.ts b/src/modules/reader/Reader.types.ts index 2cb99876..14967bac 100644 --- a/src/modules/reader/Reader.types.ts +++ b/src/modules/reader/Reader.types.ts @@ -6,20 +6,6 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { GetChaptersReaderQuery, MangaReaderFieldsFragment } from '@/lib/graphql/generated/graphql.ts'; - -export type ReaderType = - | 'ContinuesVertical' - | 'Webtoon' - | 'SingleVertical' - | 'SingleRTL' - | 'SingleLTR' - | 'DoubleVertical' - | 'DoubleRTL' - | 'DoubleLTR' - | 'ContinuesHorizontalLTR' - | 'ContinuesHorizontalRTL'; - export interface IReaderSettings { staticNav: boolean; showPageNumber: boolean; @@ -27,25 +13,6 @@ export interface IReaderSettings { skipDupChapters: boolean; fitPageToWindow: boolean; scalePage: boolean; - readerType: ReaderType; offsetFirstPage: boolean; readerWidth: number; } - -export interface IReaderPage { - index: number; - src: string; -} - -export interface IReaderProps { - pages: Array; - pageCount: number; - setCurPage: React.Dispatch>; - curPage: number; - initialPage: number; - settings: IReaderSettings; - manga: MangaReaderFieldsFragment; - chapter: GetChaptersReaderQuery['chapters']['nodes'][number]; - nextChapter: () => void; - prevChapter: () => void; -} diff --git a/src/modules/reader/components/ReaderNavBar.tsx b/src/modules/reader/components/ReaderNavBar.tsx deleted file mode 100644 index 66e18fde..00000000 --- a/src/modules/reader/components/ReaderNavBar.tsx +++ /dev/null @@ -1,398 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import IconButton from '@mui/material/IconButton'; -import CloseIcon from '@mui/icons-material/Close'; -import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft'; -import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; -import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import Typography from '@mui/material/Typography'; -import { useLocation, useNavigate } from 'react-router-dom'; -import Slide from '@mui/material/Slide'; -import Fade from '@mui/material/Fade'; -import Zoom from '@mui/material/Zoom'; -import Divider from '@mui/material/Divider'; -import FormControl from '@mui/material/FormControl'; -import MenuItem from '@mui/material/MenuItem'; -import Tooltip from '@mui/material/Tooltip'; -import { styled, useTheme } from '@mui/material/styles'; -import ListItem from '@mui/material/ListItem'; -import ListItemText from '@mui/material/ListItemText'; -import Collapse from '@mui/material/Collapse'; -import { useTranslation } from 'react-i18next'; -import { ReaderSettingsOptions } from '@/modules/reader/components/ReaderSettingsOptions.tsx'; -import { useBackButton } from '@/modules/core/hooks/useBackButton.ts'; -import { Select } from '@/modules/core/components/inputs/Select.tsx'; -import { useGetOptionForDirection } from '@/theme.tsx'; -import { ChapterType } from '@/lib/graphql/generated/graphql.ts'; -import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; -import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; -import { CustomIconButton } from '@/modules/core/components/buttons/CustomIconButton.tsx'; -import { IReaderSettings } from '@/modules/reader/Reader.types.ts'; -import { DirectionOffset } from '@/Base.types.ts'; -import { AllowedMetadataValueTypes } from '@/modules/metadata/Metadata.types.ts'; -import { MangaChapterCountInfo, MangaIdInfo } from '@/modules/manga/Manga.types.ts'; - -const Root = styled('div')({ - zIndex: 10, -}); - -const NavContainer = styled('div')(({ theme }) => ({ - position: 'fixed', - top: 0, - left: 0, - minWidth: '240px', - maxWidth: '400px', - height: '100vh', - overflowY: 'auto', - backgroundColor: theme.palette.background.default, - - '& header': { - backgroundColor: theme.palette.action.hover, - display: 'flex', - alignItems: 'center', - padding: `${theme.spacing(1)} ${theme.spacing(3)}`, - - transition: 'left 2s ease', - }, -})); - -const Navigation = styled('div')(({ theme }) => ({ - margin: `0 ${theme.spacing(2)}`, -})); - -const PageNavigation = styled('div')({ - display: 'flex', - flexWrap: 'wrap', - alignContent: 'center', - alignItems: 'center', - justifyContent: 'center', -}); - -const ChapterNavigation = styled('div')(({ theme }) => ({ - display: 'grid', - gridTemplateRows: 'auto auto auto', - gridTemplateColumns: '0fr 1fr 0fr', - gridTemplateAreas: '"pre current next"', - gridColumnGap: theme.spacing(0.5), - margin: `${theme.spacing(1)} 0`, - - '& a': { - textDecoration: 'none', - color: 'inherit', - display: 'flex', - flexWrap: 'wrap', - alignContent: 'center', - }, -})); - -interface IProps { - settings: IReaderSettings; - setSettingValue: (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist?: boolean) => void; - manga: MangaIdInfo & MangaChapterCountInfo; - chapter: Pick; - chapters: Pick[]; - curPage: number; - scrollToPage: (page: number) => void; - openNextChapter: (offset: DirectionOffset) => void; - retrievingNextChapter: boolean; -} - -export function ReaderNavBar(props: IProps) { - const { t } = useTranslation(); - const { setReaderNavBarWidth } = useNavBarContext(); - const theme = useTheme(); - const getOptionForDirection = useGetOptionForDirection(); - - const navigate = useNavigate(); - const location = useLocation<{ - prevDrawerOpen?: boolean; - prevSettingsCollapseOpen?: boolean; - }>(); - const { prevDrawerOpen, prevSettingsCollapseOpen } = location.state ?? {}; - - const navBarRef = useRef(null); - useResizeObserver( - navBarRef, - useCallback(() => { - if (navBarRef.current?.offsetWidth === undefined) { - return; - } - - setReaderNavBarWidth(navBarRef.current.offsetWidth); - }, [navBarRef.current]), - ); - useLayoutEffect(() => () => setReaderNavBarWidth(0), [navBarRef]); - - const { - settings, - setSettingValue, - manga, - chapter, - chapters, - curPage, - scrollToPage, - openNextChapter, - retrievingNextChapter, - } = props; - - const handleBack = useBackButton(); - - const hasMultipleScanlators = useMemo( - () => new Set(chapters.map(({ scanlator }) => scanlator)).size > 1, - [chapters], - ); - - const [drawerOpen, setDrawerOpen] = useState(settings.staticNav || prevDrawerOpen); - const [updateDrawerOnRender, setUpdateDrawerOnRender] = useState(true); - const [hideOpenButton, setHideOpenButton] = useState(settings.staticNav || prevDrawerOpen); - const [prevScrollPos, setPrevScrollPos] = useState(0); - const [settingsCollapseOpen, setSettingsCollapseOpen] = useState(prevSettingsCollapseOpen ?? true); - - const disableChapterNavButtons = retrievingNextChapter; - - const updateSettingValue = (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist?: boolean) => { - // prevent closing the navBar when updating the "staticNav" setting - setUpdateDrawerOnRender(key !== 'staticNav'); - setSettingValue(key, value, persist); - }; - - const updateDrawer = (open: boolean) => { - setDrawerOpen(open); - setHideOpenButton(open); - }; - - const handleScroll = () => { - const currentScrollPos = window.pageYOffset; - - if (Math.abs(currentScrollPos - prevScrollPos) > 20) { - setHideOpenButton(currentScrollPos > prevScrollPos); - setPrevScrollPos(currentScrollPos); - } - }; - - useEffect(() => { - if (updateDrawerOnRender) { - updateDrawer(settings.staticNav); - } - }, [settings.staticNav]); - - useEffect(() => { - window.addEventListener('scroll', handleScroll); - - const rootEl: HTMLDivElement = document.querySelector('#root')!; - const mainContainer: HTMLDivElement = document.querySelector('#appMainContainer')!; - - // main container and root div need to change styles... - rootEl.style.display = 'flex'; - rootEl.style.flexDirection = 'column'; - mainContainer.style.display = 'none'; - - return () => { - rootEl.style.display = 'block'; - mainContainer.style.display = 'block'; - window.removeEventListener('scroll', handleScroll); - }; - }, [handleScroll]); // handleScroll changes on every render - - return ( - - - -
- {!settings.staticNav && ( - - updateDrawer(false)} - size="large" - > - {getOptionForDirection(, )} - - - )} - - {chapter.name} - - - - - - -
- - - setSettingsCollapseOpen(!settingsCollapseOpen)} - size="large" - > - {settingsCollapseOpen && } - {!settingsCollapseOpen && } - - - - - - - - - {t('reader.page_info.label.currently_on_page')} - - - - {t('reader.page_info.label.of_max_pages', { maxPages: chapter.pageCount })} - - - - openNextChapter(DirectionOffset.PREVIOUS)} - > - {getOptionForDirection(, )} - - - - - - - = manga.chapters.totalCount - } - onClick={() => openNextChapter(DirectionOffset.NEXT)} - > - {getOptionForDirection(, )} - - - - -
-
- - - - updateDrawer(true)} - > - {getOptionForDirection(, )} - - - - -
- ); -} diff --git a/src/modules/reader/components/ReaderSettingsOptions.tsx b/src/modules/reader/components/ReaderSettingsOptions.tsx deleted file mode 100644 index 9f81e77b..00000000 --- a/src/modules/reader/components/ReaderSettingsOptions.tsx +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemText from '@mui/material/ListItemText'; -import Switch from '@mui/material/Switch'; -import MenuItem from '@mui/material/MenuItem'; -import { useTranslation } from 'react-i18next'; -import { NumberSetting } from '@/modules/core/components/settings/NumberSetting.tsx'; -import { isHorizontalReaderType } from '@/modules/reader/components/page/Page.tsx'; -import { Select } from '@/modules/core/components/inputs/Select.tsx'; -import { IReaderSettings } from '@/modules/reader/Reader.types.ts'; -import { AllowedMetadataValueTypes } from '@/modules/metadata/Metadata.types.ts'; - -interface IProps extends IReaderSettings { - setSettingValue: (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist?: boolean) => void; -} - -export function ReaderSettingsOptions({ - staticNav, - loadNextOnEnding, - readerType, - showPageNumber, - skipDupChapters, - setSettingValue, - fitPageToWindow, - scalePage, - offsetFirstPage, - readerWidth, -}: IProps) { - const { t } = useTranslation(); - const fitPageToWindowEligible = !isHorizontalReaderType(readerType); - return ( - - - - setSettingValue('staticNav', e.target.checked)} - /> - - - - setSettingValue('showPageNumber', e.target.checked)} - /> - - - - setSettingValue('loadNextOnEnding', e.target.checked)} - /> - - - - setSettingValue('skipDupChapters', e.target.checked)} - /> - - {fitPageToWindowEligible && ( - - - setSettingValue('fitPageToWindow', e.target.checked)} - /> - - )} - {fitPageToWindowEligible && fitPageToWindow && ( - - - setSettingValue('scalePage', e.target.checked)} - /> - - )} - {(readerType === 'DoubleLTR' || readerType === 'DoubleRTL') && ( - - - setSettingValue('offsetFirstPage', e.target.checked)} - /> - - )} - {fitPageToWindowEligible && !fitPageToWindow && ( - setSettingValue('readerWidth', width)} - handleLiveUpdate={(width: number) => setSettingValue('readerWidth', width, false)} - listItemTextSx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }} - /> - )} - - - - - - ); -} diff --git a/src/modules/reader/components/page/DoublePage.tsx b/src/modules/reader/components/page/DoublePage.tsx deleted file mode 100644 index f0181ff5..00000000 --- a/src/modules/reader/components/page/DoublePage.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { CSSProperties, forwardRef } from 'react'; -import Box from '@mui/material/Box'; -import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx'; -import { imageStyle } from '@/modules/reader/components/page/Page.tsx'; -import { getOptionForDirection } from '@/theme.tsx'; -import { IReaderSettings } from '@/modules/reader/Reader.types.ts'; - -interface IProps { - index: number; - image1src: string; - image2src: string; - onImageLoad?: () => void; - settings: IReaderSettings; -} - -export const DoublePage = forwardRef((props: IProps, ref: any) => { - const { image1src, image2src, index, onImageLoad, settings } = props; - - const baseImgStyle = imageStyle(settings); - const imgStyle = { - ...baseImgStyle, - width: settings.fitPageToWindow ? baseImgStyle.width : `calc(${baseImgStyle.width} * 0.5)`, - minWidth: - settings.fitPageToWindow && settings.scalePage - ? `calc(${baseImgStyle.minWidth} * 0.5)` - : baseImgStyle.minWidth, - maxWidth: settings.fitPageToWindow ? `calc(${baseImgStyle.maxWidth} * 0.5)` : baseImgStyle.maxWidth, - }; - - const spinnerStyle: CSSProperties = { - ...imgStyle, - height: '100vh', - width: '50%', - backgroundColor: '#525252', - }; - - return ( - - - - - ); -}); diff --git a/src/modules/reader/components/page/Page.tsx b/src/modules/reader/components/page/Page.tsx deleted file mode 100644 index ecc2ccf9..00000000 --- a/src/modules/reader/components/page/Page.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { CSSProperties, forwardRef } from 'react'; -import Box from '@mui/material/Box'; -import { useTheme } from '@mui/material/styles'; -import useMediaQuery from '@mui/material/useMediaQuery'; -import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx'; -import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; -import { IReaderSettings, ReaderType } from '@/modules/reader/Reader.types.ts'; -import { Priority } from '@/lib/Queue.ts'; - -export const isHorizontalReaderType = (readerType: ReaderType): boolean => - ['ContinuesHorizontalLTR', 'ContinuesHorizontalRTL'].includes(readerType); - -export function imageStyle(settings: IReaderSettings): CSSProperties { - const isDoublePageReader = ['DoubleRTL', 'DoubleLTR'].includes(settings.readerType); - const isVertical = settings.readerType === 'ContinuesVertical'; - const isHorizontal = isHorizontalReaderType(settings.readerType); - - const scrollbarHeight = MediaQuery.useGetScrollbarSize('height'); - - const baseStyling: CSSProperties = { - margin: 0, - width: `${settings.readerWidth}%`, - objectFit: 'contain', - }; - - const doublePageStyling: CSSProperties = {}; - - const continuesVerticalStyling: CSSProperties = { - marginBottom: '15px', - }; - - const continuesHorizontalStyling: CSSProperties = { - width: undefined, - minHeight: `calc(100vh - ${scrollbarHeight}px)`, - maxHeight: `calc(100vh - ${scrollbarHeight}px)`, - marginLeft: '7px', - marginRight: '7px', - }; - - const fitToPageStyling: CSSProperties = { - width: undefined, - height: undefined, - minWidth: settings.scalePage ? 'calc(100vw - (100vw - 100%))' : undefined, - maxWidth: 'calc(100vw - (100vw - 100%))', - minHeight: settings.scalePage ? `calc(100vh - ${scrollbarHeight}px)` : undefined, - maxHeight: `calc(100vh - ${scrollbarHeight}px)`, - }; - - return { - ...baseStyling, - ...(isDoublePageReader ? doublePageStyling : undefined), - ...(isHorizontal ? continuesHorizontalStyling : undefined), - ...(isVertical ? continuesVerticalStyling : undefined), - ...(settings.fitPageToWindow && !isHorizontal ? fitToPageStyling : undefined), - }; -} - -interface IProps { - src: string; - index: number; - onImageLoad: () => void; - settings: IReaderSettings; - display?: boolean; - priority?: Priority; -} - -export const Page = forwardRef((props: IProps, ref: any) => { - const { src, index, onImageLoad, settings, display = true, priority } = props; - - const theme = useTheme(); - const isMobileWidth = useMediaQuery(theme.breakpoints.down('md')); - - const imgStyle = imageStyle(settings); - - return ( - - - - ); -}); diff --git a/src/modules/reader/components/page/PageNumber.tsx b/src/modules/reader/components/page/PageNumber.tsx deleted file mode 100644 index 9cfaa686..00000000 --- a/src/modules/reader/components/page/PageNumber.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import Box from '@mui/material/Box'; - -import { IReaderSettings } from '@/modules/reader/Reader.types.ts'; - -interface IProps { - settings: IReaderSettings; - curPage: number; - pageCount: number; -} - -export function PageNumber(props: IProps) { - const { settings, curPage, pageCount } = props; - - return ( - - {`${curPage + 1} / ${pageCount}`} - - ); -} diff --git a/src/modules/reader/components/pager/DoublePagedPager.tsx b/src/modules/reader/components/pager/DoublePagedPager.tsx deleted file mode 100644 index 5da1accd..00000000 --- a/src/modules/reader/components/pager/DoublePagedPager.tsx +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { MouseEvent, useEffect, useMemo, useRef, useState } from 'react'; -import Box from '@mui/material/Box'; -import { Page } from '@/modules/reader/components/page/Page.tsx'; -import { IReaderProps } from '@/modules/reader/Reader.types.ts'; -import { Priority } from '@/lib/Queue.ts'; - -const isSpreadPage = (image: HTMLImageElement): boolean => { - const aspectRatio = image.height / image.width; - return aspectRatio < 1; -}; - -export function DoublePagedPager(props: IReaderProps) { - const { pages, settings, setCurPage, initialPage, curPage, nextChapter, prevChapter } = props; - - const selfRef = useRef(null); - - const pagesToDisplayStateRef = useRef([]); - - const [pagesToSpreadState, setPagesToSpreadState] = useState(Array(pages.length).fill(false)); - const [pagesLoadState, setPagesLoadState] = useState(Array(pages.length).fill(false)); - - // each spread page has to be counted as 2 pages and all trailing page numbers have to be increased by the count - // of leading spread pages - const pageToActualPageIndex = useMemo( - () => pagesToSpreadState.map((_, page) => page + pagesToSpreadState.slice(0, page).filter(Boolean).length), - [pagesToSpreadState], - ); - - function nextPage() { - const setNextPage = (page: number) => setCurPage(page === -1 ? pages.length - 1 : page); - - const isLastPageDisplayed = pagesToDisplayStateRef.current[pages.length - 1]; - if (isLastPageDisplayed && settings.loadNextOnEnding) { - // make sure to set last page as current page so that the chapter gets marked as read before opening the next chapter - const isLastPage = curPage === pages.length - 1; - if (!isLastPage) { - setNextPage(pages.length - 1); - } - - nextChapter(); - return; - } - - const page = pagesToDisplayStateRef.current.findIndex((displayed, index) => !displayed && index > curPage); - setNextPage(page); - } - - function prevPage() { - const setPrevPage = (page: number) => setCurPage(Math.max(page, 0)); - - const isFirstPageDisplayed = pagesToDisplayStateRef.current[0]; - if (isFirstPageDisplayed) { - // not important, but for consistency make sure that first page gets set as current page in case it was displayed - const isFirstPage = !curPage; - if (!isFirstPage) { - setPrevPage(0); - } - - prevChapter(); - return; - } - - const page = [...pagesToDisplayStateRef.current].slice(0, curPage).findLastIndex((displayed) => !displayed); - setPrevPage(page); - } - - function goLeft() { - if (settings.readerType === 'DoubleLTR') { - prevPage(); - } else { - nextPage(); - } - } - - function goRight() { - if (settings.readerType === 'DoubleLTR') { - nextPage(); - } else { - prevPage(); - } - } - - function keyboardControl(e: KeyboardEvent) { - switch (e.key) { - case 'Space': - e.preventDefault(); - nextPage(); - break; - case 'ArrowRight': - goRight(); - break; - case 'ArrowLeft': - goLeft(); - break; - default: - break; - } - } - - function clickControl(e: MouseEvent) { - if (e.clientX > window.innerWidth / 2) { - goRight(); - } else { - goLeft(); - } - } - - useEffect(() => { - document.addEventListener('keydown', keyboardControl); - - return () => { - document.removeEventListener('keydown', keyboardControl); - }; - }, [selfRef, curPage, settings.readerType, prevChapter, nextChapter, pagesLoadState, pagesToSpreadState]); - - useEffect(() => { - setCurPage(initialPage); - }, [initialPage]); - - return ( - - - {pages.map(({ index, src }) => { - /* - - | = page separator - + = double page - _ = double spread - - without double spreads: - without double spread offset: | 0 + 1 | 2 + 3 | 4 + 5 | 6 + 7 | 8 | - with double spread offset : | 0 | 1 + 2 | 3 + 4 | 5 + 6 | 7 + 8 | - - with double spreads - to handle double spreads: - each double spread has to count as 2 pages, thus, each page number after a double spread has to increase - by the number of leading double spreads - - without double spread offset: | 0 + 1 | 2 | _3/4_ | 5 + 6 | 7 + 8 | _9/10_ | 11 + 12 | 13 + 14 | - with double spread offset : | 0 | 1 + 2 | _3/4_ | 5 + 6 | 7 + 8 | _9/10_ | 11 + 12 | 13 + 14 | - - thus, to get the second page: - without offset: second page = current page number even ? +1 : -1 - with offset : second page = current page number even ? +1 : -1 - - the second page has to be ignored in case: - - double spreads are offset, and the current page is the first page - - either the current or second page is a double spread - */ - - const normalizedCurPage = pageToActualPageIndex[curPage]; - const isCurPageEven = !(normalizedCurPage % 2); - - const secondPageOffset = (() => { - const invert = settings.offsetFirstPage ? -1 : 1; - const offset = isCurPageEven ? 1 : -1; - - return offset * invert; - })(); - - const secondPage = curPage + secondPageOffset; - - const isFirstPage = curPage === 0; - const isCurPage = index === curPage; - const isSecondPage = index === secondPage; - - const isCurrentPageSpreadPage = pagesToSpreadState[curPage]; - const isSecondPageSpreadPage = pagesToSpreadState[secondPage]; - const hasSpreadPage = isCurrentPageSpreadPage || isSecondPageSpreadPage; - - const ignoreSecondPageDueToOffset = isFirstPage && settings.offsetFirstPage; - - const displaySecondPage = isSecondPage && !hasSpreadPage && !ignoreSecondPageDueToOffset; - const displayPage = isCurPage || displaySecondPage; - - pagesToDisplayStateRef.current[index] = displayPage; - - return ( - { - const img = new Image(); - img.onload = () => { - setPagesLoadState((prevState) => prevState.toSpliced(index, 1, true)); - setPagesToSpreadState((prevState) => - prevState.toSpliced(index, 1, isSpreadPage(img)), - ); - }; - img.src = src; - }} - settings={settings} - display={displayPage} - priority={displayPage ? Priority.HIGH : undefined} - /> - ); - })} - - - ); -} diff --git a/src/modules/reader/components/pager/HorizontalPager.tsx b/src/modules/reader/components/pager/HorizontalPager.tsx deleted file mode 100644 index 0b82829f..00000000 --- a/src/modules/reader/components/pager/HorizontalPager.tsx +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { MouseEvent as ReactMouseEvent, WheelEvent as ReactWheelEvent, useCallback, useEffect, useRef } from 'react'; -import Box from '@mui/material/Box'; -import { Page } from '@/modules/reader/components/page/Page.tsx'; -import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; -import { IReaderProps } from '@/modules/reader/Reader.types.ts'; -import { Priority } from '@/lib/Queue.ts'; - -const findCurrentPageIndex = (wrapper: HTMLDivElement): number => { - for (let i = 0; i < wrapper.children.length; i++) { - const child = wrapper.children.item(i); - if (child) { - const { left, right } = child.getBoundingClientRect(); - if (left <= window.innerWidth / 2 && right > window.innerWidth / 2) return i; - } - } - return -1; -}; - -const SCROLL_SAFE_ZONE = 5; // px -const isAtEnd = () => { - const visibleEnd = window.innerWidth + window.scrollX; - // SCROLL_SAFE_ZONE is here for special cases when window might be .5px shorter - // and math just dont add up correctly - return visibleEnd >= document.body.scrollWidth - SCROLL_SAFE_ZONE; -}; -const isAtStart = () => window.scrollX <= 0; - -export function HorizontalPager(props: IReaderProps) { - const { pages, curPage, initialPage, settings, setCurPage, prevChapter, nextChapter } = props; - - const currentPageRef = useRef(initialPage); - const selfRef = useRef(null); - const pagesRef = useRef([]); - - function nextPage() { - if (curPage < pages.length - 1) { - pagesRef.current[curPage + 1]?.scrollIntoView({ inline: 'center' }); - setCurPage((page) => page + 1); - } else if (settings.loadNextOnEnding) { - nextChapter(); - } - } - - function prevPage() { - if (curPage > 0) { - pagesRef.current[curPage - 1]?.scrollIntoView({ inline: 'center' }); - setCurPage(curPage - 1); - } else if (curPage === 0) { - prevChapter(); - } - } - - function goLeft() { - if (settings.readerType === 'ContinuesHorizontalLTR') { - prevPage(); - } else { - nextPage(); - } - } - - function goRight() { - if (settings.readerType === 'ContinuesHorizontalLTR') { - nextPage(); - } else { - prevPage(); - } - } - - const mouseXPos = useRef(0); - - function dragScreen(e: MouseEvent) { - window.scrollBy(mouseXPos.current - e.pageX, 0); - } - - function dragControl(e: MouseEvent) { - mouseXPos.current = e.pageX; - selfRef.current?.addEventListener('mousemove', dragScreen); - } - - function removeDragControl() { - selfRef.current?.removeEventListener('mousemove', dragScreen); - } - - function clickControl(e: ReactMouseEvent) { - if (e.clientX >= window.innerWidth * 0.85) { - goRight(); - } else if (e.clientX <= window.innerWidth * 0.15) { - goLeft(); - } - } - - function horizontalScroll(e: ReactWheelEvent) { - window.scrollBy({ - left: settings.readerType === 'ContinuesHorizontalLTR' ? e.deltaY : e.deltaY * -1, - }); - } - - const handleLoadNextonEnding = () => { - if (settings.readerType === 'ContinuesHorizontalLTR') { - if (window.scrollX + window.innerWidth >= document.body.scrollWidth) { - nextChapter(); - } - } else if (settings.readerType === 'ContinuesHorizontalRTL') { - if (window.scrollX <= window.innerWidth) { - nextChapter(); - } - } - }; - - useResizeObserver( - pagesRef.current[initialPage], - useCallback( - (_, resizeObserver) => { - const initialPageElement = pagesRef.current[initialPage]; - if (!initialPageElement?.offsetHeight) { - return; - } - - initialPageElement.scrollIntoView({ inline: 'center' }); - resizeObserver.disconnect(); - }, - [pagesRef.current[initialPage], initialPage], - ), - ); - - useEffect(() => { - selfRef.current?.addEventListener('mousedown', dragControl); - selfRef.current?.addEventListener('mouseup', removeDragControl); - - return () => { - selfRef.current?.removeEventListener('mousedown', dragControl); - selfRef.current?.removeEventListener('mouseup', removeDragControl); - }; - }, [selfRef]); - - useEffect(() => { - if (settings.loadNextOnEnding) { - document.addEventListener('scroll', handleLoadNextonEnding); - } - - return () => { - document.removeEventListener('scroll', handleLoadNextonEnding); - }; - }, [selfRef, curPage, prevChapter, nextChapter]); - - useEffect(() => { - const handleScroll = () => { - if (!selfRef.current) return; - - // Update current page in parent - const currentPage = findCurrentPageIndex(selfRef.current); - if (currentPage !== currentPageRef.current) { - currentPageRef.current = currentPage; - setCurPage(currentPage); - } - - // Special case if scroll is moved all the way to the edge - // This handles cases when last page is show, but is smaller then - // window, in which case it would never get marked as read. - // See https://github.com/Suwayomi/Suwayomi-WebUI/issues/14 for more info - if (settings.readerType === 'ContinuesHorizontalLTR' ? isAtEnd() : isAtStart()) { - currentPageRef.current = pages.length - 1; - setCurPage(currentPageRef.current); - } - }; - window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); - }, [settings.readerType]); - - return ( - - {pages.map((page) => ( - {}} - settings={settings} - ref={(e: HTMLDivElement) => { - pagesRef.current[page.index] = e; - }} - priority={page.index === curPage ? Priority.HIGH : undefined} - /> - ))} - - ); -} diff --git a/src/modules/reader/components/pager/PagedPager.tsx b/src/modules/reader/components/pager/PagedPager.tsx deleted file mode 100644 index a2418cc0..00000000 --- a/src/modules/reader/components/pager/PagedPager.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { MouseEvent, useEffect, useRef } from 'react'; -import Box from '@mui/material/Box'; -import { Page } from '@/modules/reader/components/page/Page.tsx'; -import { IReaderProps } from '@/modules/reader/Reader.types.ts'; -import { Priority } from '@/lib/Queue.ts'; - -export function PagedPager(props: IReaderProps) { - const { pages, settings, setCurPage, initialPage, curPage, nextChapter, prevChapter } = props; - - const selfRef = useRef(null); - - const changePage = (newPage: number) => { - setCurPage(newPage); - window.scroll({ top: 0 }); - }; - - function nextPage() { - if (curPage < pages.length - 1) { - changePage(curPage + 1); - } else if (settings.loadNextOnEnding) { - nextChapter(); - } - } - - function prevPage() { - if (curPage > 0) { - changePage(curPage - 1); - } else { - prevChapter(); - } - } - - function goLeft() { - if (settings.readerType === 'SingleLTR') { - prevPage(); - } else if (settings.readerType === 'SingleRTL') { - nextPage(); - } - } - - function goRight() { - if (settings.readerType === 'SingleLTR') { - nextPage(); - } else if (settings.readerType === 'SingleRTL') { - prevPage(); - } - } - - function keyboardControl(e: KeyboardEvent) { - switch (e.key) { - case 'Space': - e.preventDefault(); - nextPage(); - break; - case 'ArrowRight': - goRight(); - break; - case 'ArrowLeft': - goLeft(); - break; - default: - break; - } - } - - function clickControl(e: MouseEvent) { - if (e.clientX > window.innerWidth / 2) { - goRight(); - } else { - goLeft(); - } - } - - useEffect(() => { - document.addEventListener('keydown', keyboardControl); - - return () => { - document.removeEventListener('keydown', keyboardControl); - }; - }, [selfRef, curPage, settings.readerType, prevChapter, nextChapter]); - - useEffect(() => { - // Delay scrolling to next cycle - setTimeout(() => { - // scroll last read page into view when initialPage changes - changePage(initialPage); - }, 0); - }, [initialPage]); - - return ( - - {pages.map(({ index, src }) => ( - {}} - src={src} - settings={settings} - display={index === curPage} - priority={index === curPage ? Priority.HIGH : undefined} - /> - ))} - - ); -} diff --git a/src/modules/reader/components/pager/VerticalPager.tsx b/src/modules/reader/components/pager/VerticalPager.tsx deleted file mode 100644 index f5264861..00000000 --- a/src/modules/reader/components/pager/VerticalPager.tsx +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { useCallback, useEffect, useRef } from 'react'; -import Box from '@mui/material/Box'; -import { Page } from '@/modules/reader/components/page/Page.tsx'; -import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; -import { IReaderProps } from '@/modules/reader/Reader.types.ts'; -import { Priority } from '@/lib/Queue.ts'; - -const findCurrentPageIndex = (wrapper: HTMLDivElement): number => { - for (let i = 0; i < wrapper.children.length; i++) { - const child = wrapper.children.item(i); - if (child) { - const { top, bottom } = child.getBoundingClientRect(); - if (top <= window.innerHeight && bottom > 1) return i; - } - } - return -1; -}; - -// TODO: make configurable? -const SCROLL_SAFE_ZONE = 5; // px -const SCROLL_OFFSET = 0.95; -const SCROLL_OFFSET_SLIGHT = 0.25; -const SCROLL_BEHAVIOR: ScrollBehavior = 'smooth'; - -const isAtBottom = () => { - const visibleBottom = window.innerHeight + window.scrollY; - // SCROLL_SAFE_ZONE is here for special cases when window might be .5px shorter - // and math just dont add up correctly - return visibleBottom >= document.body.offsetHeight - SCROLL_SAFE_ZONE; -}; -const isAtTop = () => window.scrollY <= 0; - -export function VerticalPager(props: IReaderProps) { - const { curPage, pages, settings, setCurPage, initialPage, nextChapter, prevChapter } = props; - - const currentPageRef = useRef(initialPage); - const selfRef = useRef(null); - const pagesRef = useRef([]); - - useEffect(() => { - let handlingEndOfPage = false; - const handleScroll = () => { - if (!selfRef.current) return; - - if (isAtBottom()) { - if (handlingEndOfPage) { - return; - } - - handlingEndOfPage = true; - - // If scroll is moved all the way to the bottom - // This handles cases when last page is show, but is smaller then - // window, in which case it would never get marked as read. - // See https://github.com/Suwayomi/Suwayomi-WebUI/issues/14 for more info - currentPageRef.current = pages.length - 1; - setCurPage(currentPageRef.current); - - // Go to next chapter if configured to and at bottom - if (settings.loadNextOnEnding) { - nextChapter(); - } - } else { - handlingEndOfPage = false; - - // Update current page in parent - const currentPage = findCurrentPageIndex(selfRef.current); - if (currentPage !== currentPageRef.current) { - currentPageRef.current = currentPage; - setCurPage(currentPage); - } - } - }; - - window.addEventListener('scroll', handleScroll); - return () => { - window.removeEventListener('scroll', handleScroll); - }; - }, [settings.loadNextOnEnding, nextChapter]); - - const mouseYPos = useRef(0); - const didMouseMove = useRef(false); - - function dragScreen(e: MouseEvent) { - didMouseMove.current = true; - window.scrollBy(0, mouseYPos.current - e.pageY); - } - - function dragControl(e: MouseEvent) { - mouseYPos.current = e.pageY; - selfRef.current?.addEventListener('mousemove', dragScreen); - } - - function removeDragControl() { - selfRef.current?.removeEventListener('mousemove', dragScreen); - } - - useEffect(() => { - selfRef.current?.addEventListener('mousedown', dragControl); - selfRef.current?.addEventListener('mouseup', removeDragControl); - - return () => { - selfRef.current?.removeEventListener('mousedown', dragControl); - selfRef.current?.removeEventListener('mouseup', removeDragControl); - }; - }, [selfRef]); - - const go = useCallback( - (direction: 'up' | 'down', offset: number = SCROLL_OFFSET) => { - if (direction === 'down' && isAtBottom()) { - nextChapter(); - return; - } - - if (direction === 'up' && isAtTop()) { - prevChapter(); - return; - } - - window.scroll({ - top: window.scrollY + window.innerHeight * offset * (direction === 'up' ? -1 : 1), - behavior: SCROLL_BEHAVIOR, - }); - }, - [nextChapter, prevChapter], - ); - - useEffect(() => { - const handleKeyboard = (e: KeyboardEvent) => { - switch (e.key) { - case 'Space': - e.preventDefault(); - go(e.shiftKey ? 'up' : 'down'); - break; - case 'ArrowDown': - e.preventDefault(); - go(e.shiftKey ? 'up' : 'down', SCROLL_OFFSET_SLIGHT); - break; - case 'ArrowRight': - e.preventDefault(); - go(e.shiftKey ? 'up' : 'down'); - break; - case 'ArrowUp': - e.preventDefault(); - go(e.shiftKey ? 'down' : 'up', SCROLL_OFFSET_SLIGHT); - break; - case 'ArrowLeft': - e.preventDefault(); - go(e.shiftKey ? 'down' : 'up'); - break; - default: - break; - } - }; - - document.addEventListener('keydown', handleKeyboard, false); - return () => { - document.removeEventListener('keydown', handleKeyboard); - }; - }, [go]); - - useResizeObserver( - pagesRef.current[initialPage], - useCallback( - (_, resizeObserver) => { - const initialPageElement = pagesRef.current[initialPage]; - if (!initialPageElement?.offsetHeight) { - return; - } - - initialPageElement.scrollIntoView(); - resizeObserver.disconnect(); - }, - [pagesRef.current[initialPage], initialPage], - ), - ); - - return ( - { - if (didMouseMove.current) { - didMouseMove.current = false; - return; - } - - go(e.clientX > window.innerWidth / 2 ? 'down' : 'up'); - }} - > - {pages.map((page) => ( - {}} - settings={settings} - ref={(e: HTMLDivElement) => { - pagesRef.current[page.index] = e; - }} - priority={page.index === curPage ? Priority.HIGH : undefined} - /> - ))} - - ); -} diff --git a/src/modules/reader/screens/DefaultReaderSettings.tsx b/src/modules/reader/screens/DefaultReaderSettings.tsx deleted file mode 100644 index 3e6c925f..00000000 --- a/src/modules/reader/screens/DefaultReaderSettings.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { useContext, useLayoutEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { requestUpdateServerMetadata } from '@/modules/metadata/services/MetadataUpdater.ts'; -import { useDefaultReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts'; -import { ReaderSettingsOptions } from '@/modules/reader/components/ReaderSettingsOptions.tsx'; -import { makeToast } from '@/modules/core/utils/Toast.ts'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; -import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; -import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx'; -import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; -import { IReaderSettings } from '@/modules/reader/Reader.types.ts'; -import { AllowedMetadataValueTypes } from '@/modules/metadata/Metadata.types.ts'; - -export function DefaultReaderSettings() { - const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); - useLayoutEffect(() => { - setTitle(t('reader.settings.title.default_reader_settings')); - setAction(null); - - return () => { - setTitle(''); - setAction(null); - }; - }, [t]); - - const { - settings, - loading, - request: { error, refetch }, - } = useDefaultReaderSettings(); - - const setSettingValue = (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist: boolean = true) => { - if (persist) { - requestUpdateServerMetadata([[key, value]]).catch(() => - makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'), - ); - } - }; - - if (loading) { - return ; - } - - if (error) { - return ( - refetch().catch(defaultPromiseErrorHandler('DefaultReaderSettings::refetch'))} - /> - ); - } - - return ( - - ); -} diff --git a/src/modules/reader/screens/Reader.tsx b/src/modules/reader/screens/Reader.tsx deleted file mode 100644 index 298141ae..00000000 --- a/src/modules/reader/screens/Reader.tsx +++ /dev/null @@ -1,576 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import CircularProgress from '@mui/material/CircularProgress'; -import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { useLocation, useNavigate, useParams } from 'react-router-dom'; -import Box from '@mui/material/Box'; -import { useTranslation } from 'react-i18next'; -import { requestManager } from '@/lib/requests/RequestManager.ts'; -import { getReaderSettingsFor, useDefaultReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts'; -import { requestUpdateMangaMetadata } from '@/modules/metadata/services/MetadataUpdater.ts'; -import { HorizontalPager } from '@/modules/reader/components/pager/HorizontalPager.tsx'; -import { PageNumber } from '@/modules/reader/components/page/PageNumber.tsx'; -import { PagedPager } from '@/modules/reader/components/pager/PagedPager.tsx'; -import { DoublePagedPager } from '@/modules/reader/components/pager/DoublePagedPager.tsx'; -import { VerticalPager } from '@/modules/reader/components/pager/VerticalPager.tsx'; -import { ReaderNavBar } from '@/modules/reader/components/ReaderNavBar.tsx'; -import { makeToast } from '@/modules/core/utils/Toast.ts'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; -import { useDebounce } from '@/modules/core/hooks/useDebounce.ts'; -import { - GetChaptersReaderQuery, - GetChaptersReaderQueryVariables, - GetMangaReaderQuery, - UpdateChapterPatchInput, -} from '@/lib/graphql/generated/graphql.ts'; -import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; -import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; -import { ChapterIdInfo, Chapters } from '@/modules/chapter/services/Chapters.ts'; -import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx'; -import { GET_CHAPTERS_READER } from '@/lib/graphql/queries/ChapterQuery.ts'; -import { GET_MANGA_READER } from '@/lib/graphql/queries/MangaQuery.ts'; -import { CHAPTER_READER_FIELDS } from '@/lib/graphql/fragments/ChapterFragments.ts'; -import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; -import { IReaderSettings, ReaderType } from '@/modules/reader/Reader.types.ts'; -import { DirectionOffset } from '@/Base.types.ts'; -import { AllowedMetadataValueTypes } from '@/modules/metadata/Metadata.types.ts'; -import { TMangaReader } from '@/modules/manga/Manga.types.ts'; - -type TChapter = GetChaptersReaderQuery['chapters']['nodes'][number]; - -const getChapterFromCache = (id: ChapterIdInfo['id']): TChapter | null => - Chapters.getFromCache(id, CHAPTER_READER_FIELDS, 'CHAPTER_READER_FIELDS')!; - -const getReaderComponent = (readerType: ReaderType) => { - switch (readerType) { - case 'ContinuesVertical': - case 'Webtoon': - return VerticalPager; - break; - case 'SingleVertical': - case 'SingleRTL': - case 'SingleLTR': - return PagedPager; - break; - case 'DoubleVertical': - case 'DoubleRTL': - case 'DoubleLTR': - return DoublePagedPager; - break; - case 'ContinuesHorizontalLTR': - case 'ContinuesHorizontalRTL': - return HorizontalPager; - default: - return VerticalPager; - break; - } -}; - -const range = (n: number) => Array.from({ length: n }, (value, key) => key); -const fallbackChapter = { - pageCount: -1, - sourceOrder: -1, - chapterCount: 0, - lastPageRead: 0, - name: 'Loading...', - manga: { id: -1 }, -} as unknown as TChapter; - -export function Reader() { - const { t } = useTranslation(); - const navigate = useNavigate(); - const location = useLocation(); - - const { chapterIndex, mangaId } = useParams<{ chapterIndex: string; mangaId: string }>(); - - const fallbackManga = useMemo( - () => - ({ - id: +mangaId, - title: '', - thumbnailUrl: '', - genre: [], - inLibraryAt: 0, - lastReadAt: 0, - chapters: { totalCount: 0 }, - trackRecords: { totalCount: 0 }, - }) as unknown as TMangaReader, - [mangaId], - ); - - const { - data, - loading: isMangaLoading, - error: mangaError, - refetch: refetchManga, - } = requestManager.useGetManga(GET_MANGA_READER, mangaId); - const loadedChapter = useRef(null); - const isChapterLoaded = - Number(mangaId) === loadedChapter.current?.mangaId && - Number(chapterIndex) === loadedChapter.current?.sourceOrder && - loadedChapter.current?.pageCount !== -1; - const manga = data?.manga ?? fallbackManga; - const { - data: chaptersData, - loading: isChapterLoading, - error: chapterError, - refetch: fetchChapter, - } = requestManager.useGetChapters( - GET_CHAPTERS_READER, - { - condition: { mangaId: Number(mangaId), sourceOrder: Number(chapterIndex) }, - }, - { - notifyOnNetworkStatusChange: true, - }, - ); - const chapterData = chaptersData?.chapters.nodes[0]; - const arePagesUpdatedRef = useRef(false); - - const { - settings: { downloadAheadLimit }, - } = useMetadataServerSettings(); - const isDownloadAheadEnabled = !!downloadAheadLimit; - - const getLoadedChapter = () => { - const isAChapterLoaded = loadedChapter.current; - - const isSameAsLoadedChapter = isAChapterLoaded && isChapterLoaded; - if (isSameAsLoadedChapter) { - const didPageCountChange = chapterData && loadedChapter.current?.pageCount !== chapterData.pageCount; - return didPageCountChange ? chapterData! : loadedChapter.current; - } - - arePagesUpdatedRef.current = false; - - if (chapterData) { - return chapterData; - } - - return null; - }; - loadedChapter.current = getLoadedChapter(); - - const chapter = loadedChapter.current ?? fallbackChapter; - - const initialChapterRef = useRef(fallbackChapter); - if (initialChapterRef.current === fallbackChapter) { - initialChapterRef.current = chapter; - } - if (chapter.mangaId !== initialChapterRef.current?.mangaId) { - initialChapterRef.current = fallbackChapter; - } - - const [fetchPages, { loading: arePagesLoading, error: pagesError }] = requestManager.useGetChapterPagesFetch( - chapter.id, - ); - - const doFetchPages = () => { - if (isChapterLoading) { - return; - } - - fetchPages() - .then(() => { - arePagesUpdatedRef.current = true; - }) - .catch(defaultPromiseErrorHandler('Reader::fetchPages')); - }; - - useEffect(() => { - doFetchPages(); - }, [chapter.id]); - - const [wasLastPageReadSet, setWasLastPageReadSet] = useState(false); - const [curPage, setCurPage] = useState(0); - const isLastPage = curPage === chapter.pageCount - 1; - const curPageDebounced = useDebounce(curPage, isLastPage ? 0 : 1000); - const [pageToScrollTo, setPageToScrollTo] = useState(undefined); - const { setOverride, setTitle, readerNavBarWidth: currentNavBarWidth } = useContext(NavBarContext); - const [retrievingNextChapter, setRetrievingNextChapter] = useState(false); - const { - data: mangaChaptersData, - loading: areChaptersLoading, - error: chaptersError, - refetch: refetchChapters, - } = requestManager.useGetMangaChapters( - GET_CHAPTERS_READER, - mangaId, - { nextFetchPolicy: 'standby' }, - ); - const mangaChapters = mangaChaptersData?.chapters.nodes; - - const isLoading = - isMangaLoading || - isChapterLoading || - areChaptersLoading || - arePagesLoading || - (!arePagesUpdatedRef.current && !chapterError && !pagesError); - const error = mangaError ?? chapterError ?? chaptersError ?? pagesError; - - const { settings: defaultSettings, loading: areDefaultSettingsLoading } = useDefaultReaderSettings(); - const [settings, setSettings] = useState(defaultSettings); - - const { settings: metadataSettings } = useMetadataServerSettings(); - - const uniqueChapters = useMemo( - () => - settings.skipDupChapters - ? Chapters.removeDuplicates(initialChapterRef.current, mangaChapters ?? []) - : (mangaChapters ?? []), - [initialChapterRef.current, mangaChapters, settings.skipDupChapters], - ); - const prevChapters = useMemo( - () => - Chapters.getNextChapters(chapter, mangaChapters ?? [], { - offset: DirectionOffset.PREVIOUS, - skipDupe: settings.skipDupChapters, - skipDupeChapter: initialChapterRef.current, - }), - [chapter, initialChapterRef.current, mangaChapters, settings.skipDupChapters], - ); - const nextChapters = useMemo( - () => - Chapters.getNextChapters(chapter, mangaChapters ?? [], { - skipDupe: settings.skipDupChapters, - skipDupeChapter: initialChapterRef.current, - }), - [chapter, initialChapterRef.current, mangaChapters, settings.skipDupChapters], - ); - const prevChapter = useMemo( - () => - Chapters.getNextChapter(chapter, mangaChapters ?? [], { - offset: DirectionOffset.PREVIOUS, - skipDupe: settings.skipDupChapters, - skipDupeChapter: initialChapterRef.current, - }), - [chapter, initialChapterRef.current, mangaChapters, settings.skipDupChapters], - ); - const nextChapter = useMemo( - () => - Chapters.getNextChapter(chapter, mangaChapters ?? [], { - skipDupe: settings.skipDupChapters, - skipDupeChapter: initialChapterRef.current, - }), - [chapter, initialChapterRef.current, mangaChapters, settings.skipDupChapters], - ); - - const updateChapter = (patch: UpdateChapterPatchInput) => { - if (chapter === fallbackChapter) { - return; - } - - const getChapterIdsToDelete = () => { - const isAutoDeletionEnabled = !!patch.isRead && !!metadataSettings.deleteChaptersWhileReading; - if (!isAutoDeletionEnabled || !mangaChapters) { - return []; - } - - const chapterToDelete = [chapter, ...prevChapters][metadataSettings.deleteChaptersWhileReading - 1]; - - if (!chapterToDelete) { - return []; - } - - // chapter has to exist in the cache since the reader fetches all chapters of the manga - const chapterToDeleteUpToDateData = getChapterFromCache(chapterToDelete.id)!; - - const shouldDeleteChapter = - chapterToDeleteUpToDateData.isRead && - Chapters.isDeletable(chapterToDeleteUpToDateData, metadataSettings.deleteChaptersWithBookmark); - if (!shouldDeleteChapter) { - return []; - } - - if (!settings.skipDupChapters) { - return Chapters.getIds([chapterToDelete]); - } - - return Chapters.getIds(Chapters.addDuplicates([chapterToDelete], mangaChapters)); - }; - - const downloadAhead = () => { - const currentChapter = getChapterFromCache(chapter.id); - - const inDownloadRange = (patch.lastPageRead ?? 0) / chapter.pageCount > 0.25; - const shouldCheckDownloadAhead = - isDownloadAheadEnabled && manga.inLibrary && !!currentChapter?.isDownloaded && inDownloadRange; - - if (shouldCheckDownloadAhead) { - const nextChapterUpToDate = nextChapter ? getChapterFromCache(nextChapter.id) : null; - - if (!nextChapterUpToDate?.isDownloaded) { - return; - } - - const nextChaptersUpToDate = Chapters.getNonRead(nextChapters).map( - // the chapters have to be in the cache since the reader fetches the whole chapter list of the manga - (mangaChapter) => getChapterFromCache(mangaChapter.id)!, - ); - - const chapterIdsToDownload = nextChaptersUpToDate - // "settingsData" can't be undefined since this would not get executed otherwise - .slice(-downloadAheadLimit) - .filter((mangaChapter) => !mangaChapter.isDownloaded) - .map((mangaChapter) => mangaChapter.id) - .filter((id) => !Chapters.isDownloading(id)); - - if (!chapterIdsToDownload.length) { - return; - } - - Chapters.download(chapterIdsToDownload).catch( - defaultPromiseErrorHandler('Reader::updateChapter: shouldDownloadAhead'), - ); - } - }; - - downloadAhead(); - - const chapterIds = settings.skipDupChapters - ? Chapters.getIds(Chapters.addDuplicates([chapter], mangaChapters ?? [chapter])) - : [chapter.id]; - - requestManager - .updateChapters( - chapterIds, - { - ...patch, - chapterIdsToDelete: getChapterIdsToDelete(), - trackProgressMangaId: - metadataSettings.updateProgressAfterReading && patch.isRead && manga.trackRecords.totalCount - ? manga.id - : undefined, - }, - { errorPolicy: 'all' }, - ) - .response.catch(defaultPromiseErrorHandler('Reader::updateChapter')); - }; - - const setSettingValue = (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist: boolean = true) => { - setSettings({ ...settings, [key]: value }); - if (persist) { - requestUpdateMangaMetadata(manga, [[key, value]]).catch(() => - makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'), - ); - } - }; - - const openNextChapter = useCallback( - (offset: DirectionOffset) => { - const isOpenNextChapter = offset === DirectionOffset.NEXT; - const chapterToOpen = isOpenNextChapter ? nextChapter : prevChapter; - - if (!chapterToOpen) { - makeToast( - t( - isOpenNextChapter - ? 'reader.error.label.next_chapter_does_not_exist' - : 'reader.error.label.prev_chapter_does_not_exist', - ), - 'error', - ); - return; - } - - setRetrievingNextChapter(true); - setCurPage(0); - - navigate(`/manga/${manga.id}/chapter/${chapterToOpen.sourceOrder}`, { - replace: true, - state: location.state, - }); - - setRetrievingNextChapter(false); - }, - [manga.id, prevChapter?.id, nextChapter?.id], - ); - - useEffect(() => { - if (isLoading || !chapter) { - setCurPage(0); - return; - } - - setWasLastPageReadSet(true); - if (chapter.lastPageRead === chapter.pageCount - 1) { - // last page, also probably read = true, we will load the first page. - setCurPage(0); - } else setCurPage(chapter.lastPageRead); - }, [chapter, isLoading]); - - useLayoutEffect(() => { - if (!manga?.title || chapter.name === t('global.label.loading')) { - setTitle(t('reader.title')); - } else { - setTitle(`${manga.title}: ${chapter.name}`); - } - }, [t, manga, chapter]); - - useEffect(() => { - if (!areDefaultSettingsLoading && !isMangaLoading) { - setSettings(getReaderSettingsFor(manga, defaultSettings)); - } - }, [areDefaultSettingsLoading, isMangaLoading]); - - useLayoutEffect(() => { - // set the custom navbar - setOverride({ - status: true, - value: ( - - ), - }); - - // clean up for when we leave the reader - return () => setOverride({ status: false, value:
}); - }, [manga, chapter, settings, curPage, chapterIndex, retrievingNextChapter, openNextChapter, uniqueChapters]); - - useEffect(() => { - if (!wasLastPageReadSet) { - return; - } - - if (chapter === fallbackChapter) { - return; - } - - const chapterUpToDate = getChapterFromCache(chapter.id); - if (curPageDebounced === chapterUpToDate?.lastPageRead) { - return; - } - - // do not mutate the chapter, this will cause the page to jump around due to always scrolling to the last read page - const updateLastPageRead = curPageDebounced !== -1; - const updateIsRead = curPageDebounced === chapter.pageCount - 1; - const shouldUpdateChapter = updateLastPageRead || updateIsRead; - if (!shouldUpdateChapter) { - return; - } - - updateChapter({ - lastPageRead: updateLastPageRead ? curPageDebounced : undefined, - isRead: updateIsRead ? true : undefined, - }); - }, [curPageDebounced, isDownloadAheadEnabled]); - - const loadNextChapter = useCallback(() => { - updateChapter({ - lastPageRead: chapter.pageCount - 1, - isRead: true, - }); - - openNextChapter(DirectionOffset.NEXT); - }, [chapter.pageCount, openNextChapter, chapter, manga]); - - const loadPrevChapter = useCallback(() => { - openNextChapter(DirectionOffset.PREVIOUS); - }, [openNextChapter]); - - const scrollbarHeight = MediaQuery.useGetScrollbarSize('height'); - - if (isLoading) { - return ( - - - - ); - } - - if (error) { - return ( - { - if (mangaError) { - refetchManga().catch(defaultPromiseErrorHandler('Reader::refetchManga')); - } - - if (chapterError) { - fetchChapter().catch(defaultPromiseErrorHandler('Reader::refetchChapter')); - } - - if (chaptersError) { - refetchChapters().catch(defaultPromiseErrorHandler('Reader::refetchChapters')); - } - - if (pagesError) { - doFetchPages(); - } - }} - /> - ); - } - - if (!chapter.pageCount) { - return ; - } - - const pages = range(chapter.pageCount).map((index) => ({ - index, - src: requestManager.getChapterPageUrl(mangaId, chapterIndex, index), - })); - - const ReaderComponent = getReaderComponent(settings.readerType); - - // last page, also probably read = true, we will load the first page. - const initialPage = pageToScrollTo ?? (chapter.lastPageRead === chapter.pageCount - 1 ? 0 : chapter.lastPageRead); - - const navBarWidth = settings.staticNav ? currentNavBarWidth : 0; - - return ( - - - - - - - ); -}