From 382bf7f89906ccefb96ae0c4ccd3167550b48867 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Tue, 21 Jan 2025 20:26:03 +0100 Subject: [PATCH 1/3] Extract app page history from nav bar context --- src/modules/core/contexts/AppContext.tsx | 33 ++++++++++--------- .../core/contexts/AppPageHistoryContext.tsx | 13 ++++++++ .../AppPageHistoryContextProvider.tsx | 17 ++++++++++ src/modules/core/hooks/useBackButton.ts | 4 +-- .../navigation-bar/NavigationBar.types.ts | 2 -- .../contexts/NavBarContextProvider.tsx | 5 --- .../navigation-bar/contexts/NavbarContext.tsx | 1 - 7 files changed, 50 insertions(+), 25 deletions(-) create mode 100644 src/modules/core/contexts/AppPageHistoryContext.tsx create mode 100644 src/modules/core/contexts/AppPageHistoryContextProvider.tsx diff --git a/src/modules/core/contexts/AppContext.tsx b/src/modules/core/contexts/AppContext.tsx index bc0646ed91..d14539ed49 100644 --- a/src/modules/core/contexts/AppContext.tsx +++ b/src/modules/core/contexts/AppContext.tsx @@ -27,6 +27,7 @@ import { ReaderContextProvider } from '@/modules/reader/contexts/ReaderContextPr import { DIRECTION_TO_CACHE } from '@/modules/theme/ThemeDirectionCache.ts'; import { AppHotkeysProvider } from '@/modules/hotkeys/contexts/AppHotkeysProvider.tsx'; import { SnackbarWithDescription } from '@/modules/core/components/snackbar/SnackbarWithDescription.tsx'; +import { AppPageHistoryContextProvider } from '@/modules/core/contexts/AppPageHistoryContextProvider.tsx'; interface Props { children: React.ReactNode; @@ -93,21 +94,23 @@ export const AppContext: React.FC = ({ children }) => { - - - - {children} - - - + + + + + {children} + + + + diff --git a/src/modules/core/contexts/AppPageHistoryContext.tsx b/src/modules/core/contexts/AppPageHistoryContext.tsx new file mode 100644 index 0000000000..1c087693e4 --- /dev/null +++ b/src/modules/core/contexts/AppPageHistoryContext.tsx @@ -0,0 +1,13 @@ +/* + * 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 { createContext, useContext } from 'react'; + +export const AppPageHistoryContext = createContext([]); + +export const useAppPageHistoryContext = () => useContext(AppPageHistoryContext); diff --git a/src/modules/core/contexts/AppPageHistoryContextProvider.tsx b/src/modules/core/contexts/AppPageHistoryContextProvider.tsx new file mode 100644 index 0000000000..08221b3c6e --- /dev/null +++ b/src/modules/core/contexts/AppPageHistoryContextProvider.tsx @@ -0,0 +1,17 @@ +/* + * 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 React from 'react'; +import { AppPageHistoryContext } from '@/modules/core/contexts/AppPageHistoryContext.tsx'; +import { useHistory } from '@/modules/core/hooks/useHistory.ts'; + +export const AppPageHistoryContextProvider = ({ children }: { children: React.ReactNode }) => { + const history = useHistory(); + + return {children}; +}; diff --git a/src/modules/core/hooks/useBackButton.ts b/src/modules/core/hooks/useBackButton.ts index 0e8c0abd6b..d87e4f137d 100644 --- a/src/modules/core/hooks/useBackButton.ts +++ b/src/modules/core/hooks/useBackButton.ts @@ -8,15 +8,15 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { useCallback } from 'react'; -import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; +import { useAppPageHistoryContext } from '@/modules/core/contexts/AppPageHistoryContext.tsx'; const PAGES_TO_IGNORE: readonly RegExp[] = [/\/manga\/[0-9]+\/chapter\/[0-9]+/g]; export const useBackButton = () => { const navigate = useNavigate(); const location = useLocation(); - const { history } = useNavBarContext(); + const history = useAppPageHistoryContext(); return useCallback(() => { const isHistoryEmpty = !history.length; diff --git a/src/modules/navigation-bar/NavigationBar.types.ts b/src/modules/navigation-bar/NavigationBar.types.ts index b8d2975a5e..7455917215 100644 --- a/src/modules/navigation-bar/NavigationBar.types.ts +++ b/src/modules/navigation-bar/NavigationBar.types.ts @@ -24,8 +24,6 @@ export interface NavbarItem { } export type NavbarContextType = { - history: string[]; - // AppBar title title: string | React.ReactNode; setTitle: (title: NavbarContextType['title'], browserTitle?: string) => void; diff --git a/src/modules/navigation-bar/contexts/NavBarContextProvider.tsx b/src/modules/navigation-bar/contexts/NavBarContextProvider.tsx index 4c4897c15b..38d1907d38 100644 --- a/src/modules/navigation-bar/contexts/NavBarContextProvider.tsx +++ b/src/modules/navigation-bar/contexts/NavBarContextProvider.tsx @@ -8,7 +8,6 @@ import React, { useCallback, useMemo, useState } from 'react'; import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; -import { useHistory } from '@/modules/core/hooks/useHistory.ts'; import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx'; import { INavbarOverride } from '@/modules/navigation-bar/NavigationBar.types.ts'; @@ -29,8 +28,6 @@ export function NavBarContextProvider({ children }: IProps) { const [readerNavBarWidth, setReaderNavBarWidth] = useState(0); const [bottomBarHeight, setBottomBarHeight] = useState(0); - const history = useHistory(); - const updateTitle = useCallback( (newTitle: string | React.ReactNode, browserTitle: string = typeof newTitle === 'string' ? newTitle : '') => { document.title = `${browserTitle} - Suwayomi`; @@ -41,7 +38,6 @@ export function NavBarContextProvider({ children }: IProps) { const value = useMemo( () => ({ - history, title, setTitle: updateTitle, appBarHeight, @@ -60,7 +56,6 @@ export function NavBarContextProvider({ children }: IProps) { setBottomBarHeight, }), [ - history, title, updateTitle, appBarHeight, diff --git a/src/modules/navigation-bar/contexts/NavbarContext.tsx b/src/modules/navigation-bar/contexts/NavbarContext.tsx index df2250dd79..9ff05cc43f 100644 --- a/src/modules/navigation-bar/contexts/NavbarContext.tsx +++ b/src/modules/navigation-bar/contexts/NavbarContext.tsx @@ -11,7 +11,6 @@ import React, { useContext } from 'react'; import { NavbarContextType } from '@/modules/navigation-bar/NavigationBar.types.ts'; export const NavBarContext = React.createContext({ - history: [], title: 'Suwayomi', setTitle: (): void => {}, appBarHeight: 0, From b47563c2947e0eebc10aec47266cfa0e938ffd39 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Tue, 21 Jan 2025 20:34:17 +0100 Subject: [PATCH 2/3] Use "useNavBarContext" hook everywhere --- src/modules/backup/screens/Backup.tsx | 6 +++--- src/modules/browse/screens/Browse.tsx | 6 +++--- src/modules/browse/screens/BrowseSettings.tsx | 6 +++--- src/modules/category/screens/CategorySettings.tsx | 6 +++--- src/modules/device/screens/DeviceSetting.tsx | 4 ++-- src/modules/downloads/screens/DownloadQueue.tsx | 6 +++--- src/modules/downloads/screens/DownloadSettings.tsx | 6 +++--- src/modules/extension/screens/Extensions.tsx | 6 +++--- src/modules/global-search/screens/SearchAll.tsx | 6 +++--- src/modules/library/screens/Library.tsx | 6 +++--- src/modules/library/screens/LibraryDuplicates.tsx | 6 +++--- src/modules/library/screens/LibrarySettings.tsx | 6 +++--- src/modules/manga/screens/Manga.tsx | 6 +++--- src/modules/migration/screens/Migrate.tsx | 6 +++--- src/modules/navigation-bar/components/DefaultNavBar.tsx | 6 +++--- src/modules/settings/screens/About.tsx | 6 +++--- src/modules/settings/screens/Appearance.tsx | 4 ++-- src/modules/settings/screens/ServerSettings.tsx | 6 +++--- src/modules/settings/screens/Settings.tsx | 6 +++--- src/modules/settings/screens/WebUISettings.tsx | 6 +++--- src/modules/source/screens/SourceConfigure.tsx | 6 +++--- src/modules/source/screens/SourceMangas.tsx | 6 +++--- src/modules/source/screens/Sources.tsx | 6 +++--- src/modules/tracker/screens/TrackingSettings.tsx | 7 +++---- src/modules/updates/screens/Updates.tsx | 6 +++--- 25 files changed, 73 insertions(+), 74 deletions(-) diff --git a/src/modules/backup/screens/Backup.tsx b/src/modules/backup/screens/Backup.tsx index 7893a7673d..631629e12a 100644 --- a/src/modules/backup/screens/Backup.tsx +++ b/src/modules/backup/screens/Backup.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useContext, useEffect, useLayoutEffect, useState } from 'react'; +import { useEffect, useLayoutEffect, useState } from 'react'; import List from '@mui/material/List'; import ListItemText from '@mui/material/ListItemText'; import { fromEvent } from 'file-selector'; @@ -25,7 +25,6 @@ import { Link } from 'react-router-dom'; import Stack from '@mui/material/Stack'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { makeToast } from '@/modules/core/utils/Toast.ts'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { BackupRestoreState, ValidateBackupQuery } from '@/lib/graphql/generated/graphql.ts'; import { Progress } from '@/modules/core/components/Progress.tsx'; import { TextSetting } from '@/modules/core/components/settings/text/TextSetting.tsx'; @@ -37,6 +36,7 @@ import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts' import { ServerSettings } from '@/modules/settings/Settings.types.ts'; import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; type BackupSettingsType = Pick; @@ -59,7 +59,7 @@ let backupRestoreId: string | undefined; export function Backup() { const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); useLayoutEffect(() => { setTitle(t('settings.backup.title')); setAction(null); diff --git a/src/modules/browse/screens/Browse.tsx b/src/modules/browse/screens/Browse.tsx index 12f5503da6..619795c23b 100644 --- a/src/modules/browse/screens/Browse.tsx +++ b/src/modules/browse/screens/Browse.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useCallback, useContext, useLayoutEffect, useRef, useState } from 'react'; +import { useCallback, useLayoutEffect, useRef, useState } from 'react'; import Tab from '@mui/material/Tab'; import { useTranslation } from 'react-i18next'; import { StringParam, useQueryParam } from 'use-query-params'; @@ -16,8 +16,8 @@ import { TabPanel } from '@/modules/core/components/tabs/TabPanel.tsx'; import { TabsWrapper } from '@/modules/core/components/tabs/TabsWrapper.tsx'; import { TabsMenu } from '@/modules/core/components/tabs/TabsMenu.tsx'; import { Migration } from '@/modules/migration/screens/Migration.tsx'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; enum Tabs { SOURCE = 'source', @@ -27,7 +27,7 @@ enum Tabs { export function Browse() { const { t } = useTranslation(); - const { setTitle } = useContext(NavBarContext); + const { setTitle } = useNavBarContext(); useLayoutEffect(() => { setTitle(t('global.label.browse')); diff --git a/src/modules/browse/screens/BrowseSettings.tsx b/src/modules/browse/screens/BrowseSettings.tsx index 21a9b9741f..6d20d8ec20 100644 --- a/src/modules/browse/screens/BrowseSettings.tsx +++ b/src/modules/browse/screens/BrowseSettings.tsx @@ -7,12 +7,11 @@ */ import { Trans, useTranslation } from 'react-i18next'; -import { useContext, useLayoutEffect } from 'react'; +import { useLayoutEffect } from 'react'; 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 { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { NumberSetting } from '@/modules/core/components/settings/NumberSetting.tsx'; import { MutableListSetting } from '@/modules/core/components/settings/MutableListSetting.tsx'; @@ -29,6 +28,7 @@ import { makeToast } from '@/modules/core/utils/Toast.ts'; import { MetadataBrowseSettings } from '@/modules/browse/Browse.types.ts'; import { ServerSettings as GqlServerSettings } from '@/modules/settings/Settings.types.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; type ExtensionsSettings = Pick; @@ -40,7 +40,7 @@ const extractBrowseSettings = (settings: GqlServerSettings): ExtensionsSettings export const BrowseSettings = () => { const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); useLayoutEffect(() => { setTitle(t('settings.browse.title')); diff --git a/src/modules/category/screens/CategorySettings.tsx b/src/modules/category/screens/CategorySettings.tsx index 5b4d5b05db..9954dbddfe 100644 --- a/src/modules/category/screens/CategorySettings.tsx +++ b/src/modules/category/screens/CategorySettings.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useContext, useLayoutEffect, useMemo, useState } from 'react'; +import { useLayoutEffect, useMemo, useState } from 'react'; import { DragDropContext, Draggable, DropResult } from 'react-beautiful-dnd'; import { useTheme } from '@mui/material/styles'; import Fab from '@mui/material/Fab'; @@ -24,7 +24,6 @@ import Box from '@mui/material/Box'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { StrictModeDroppable } from '@/modules/core/components/StrictModeDroppable.tsx'; import { DEFAULT_FULL_FAB_HEIGHT } from '@/modules/core/components/buttons/StyledFab.tsx'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; @@ -33,11 +32,12 @@ import { GET_CATEGORIES_SETTINGS } from '@/lib/graphql/queries/CategoryQuery.ts' import { CategorySettingsCard } from '@/modules/category/components/CategorySettingsCard.tsx'; import { CategoryIdInfo } from '@/modules/category/Category.types.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; export function CategorySettings() { const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); useLayoutEffect(() => { setTitle(t('category.title.category_other')); setAction(null); diff --git a/src/modules/device/screens/DeviceSetting.tsx b/src/modules/device/screens/DeviceSetting.tsx index 4ba127f697..3159b87569 100644 --- a/src/modules/device/screens/DeviceSetting.tsx +++ b/src/modules/device/screens/DeviceSetting.tsx @@ -18,7 +18,6 @@ import { } from '@/modules/settings/services/ServerSettingsMetadata.ts'; import { makeToast } from '@/modules/core/utils/Toast.ts'; import { MutableListSetting } from '@/modules/core/components/settings/MutableListSetting.tsx'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { DeviceContext } from '@/modules/device/contexts/DeviceContext.tsx'; import { Select } from '@/modules/core/components/inputs/Select.tsx'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; @@ -27,10 +26,11 @@ import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts' import { DEFAULT_DEVICE } from '@/modules/device/services/Device.ts'; import { MetadataServerSettingKeys, MetadataServerSettings } from '@/modules/settings/Settings.types.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; export const DeviceSetting = () => { const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); useLayoutEffect(() => { setTitle(t('settings.device.title.settings')); diff --git a/src/modules/downloads/screens/DownloadQueue.tsx b/src/modules/downloads/screens/DownloadQueue.tsx index 5d97b771c4..5a5f144fd6 100644 --- a/src/modules/downloads/screens/DownloadQueue.tsx +++ b/src/modules/downloads/screens/DownloadQueue.tsx @@ -16,7 +16,7 @@ import Stack from '@mui/material/Stack'; import Box, { BoxProps } from '@mui/material/Box'; import Tooltip from '@mui/material/Tooltip'; import IconButton from '@mui/material/IconButton'; -import React, { memo, useCallback, useContext, useEffect, useLayoutEffect } from 'react'; +import React, { memo, useCallback, useEffect, useLayoutEffect } from 'react'; import { DragDropContext, Draggable, DraggableProvided, DropResult } from 'react-beautiful-dnd'; import Typography from '@mui/material/Typography'; import { Link } from 'react-router-dom'; @@ -30,13 +30,13 @@ import { StrictModeDroppable } from '@/modules/core/components/StrictModeDroppab import { makeToast } from '@/modules/core/utils/Toast.ts'; import { DownloadStateIndicator } from '@/modules/core/components/DownloadStateIndicator.tsx'; import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { ChapterDownloadStatus, ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts'; import { DownloaderState, DownloadState } from '@/lib/graphql/generated/graphql.ts'; import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; const HeightPreservingItem = ({ children, ...props }: BoxProps) => ( // the height is necessary to prevent the item container from collapsing, which confuses Virtuoso measurements @@ -143,7 +143,7 @@ export const DownloadQueue: React.FC = () => { const status = downloaderData?.state ?? 'STARTED'; const isQueueEmpty = !queue.length; - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); const clearQueue = async () => { try { diff --git a/src/modules/downloads/screens/DownloadSettings.tsx b/src/modules/downloads/screens/DownloadSettings.tsx index a0c5b4bfff..e7e3772886 100644 --- a/src/modules/downloads/screens/DownloadSettings.tsx +++ b/src/modules/downloads/screens/DownloadSettings.tsx @@ -7,14 +7,13 @@ */ import { useTranslation } from 'react-i18next'; -import { useContext, useLayoutEffect } from 'react'; +import { useLayoutEffect } from 'react'; 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 ListSubheader from '@mui/material/ListSubheader'; import { TextSetting } from '@/modules/core/components/settings/text/TextSetting.tsx'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { DownloadAheadSetting } from '@/modules/downloads/components/DownloadAheadSetting.tsx'; import { @@ -33,6 +32,7 @@ import { GET_CATEGORIES_SETTINGS } from '@/lib/graphql/queries/CategoryQuery.ts' import { MetadataDownloadSettings } from '@/modules/downloads/Downloads.types.ts'; import { ServerSettings } from '@/modules/settings/Settings.types.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; type DownloadSettingsType = Pick< ServerSettings, @@ -55,7 +55,7 @@ const extractDownloadSettings = (settings: ServerSettings): DownloadSettingsType export const DownloadSettings = () => { const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); useLayoutEffect(() => { setTitle(t('download.settings.title')); diff --git a/src/modules/extension/screens/Extensions.tsx b/src/modules/extension/screens/Extensions.tsx index 9ce656a308..c33eb05976 100644 --- a/src/modules/extension/screens/Extensions.tsx +++ b/src/modules/extension/screens/Extensions.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { fromEvent } from 'file-selector'; import IconButton from '@mui/material/IconButton'; import AddIcon from '@mui/icons-material/Add'; @@ -25,7 +25,6 @@ import { LoadingPlaceholder } from '@/modules/core/components/placeholder/Loadin import { makeToast } from '@/modules/core/utils/Toast.ts'; import { LangSelect } from '@/modules/core/components/inputs/LangSelect.tsx'; import { ExtensionCard } from '@/modules/extension/components/ExtensionCard.tsx'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { StyledGroupedVirtuoso } from '@/modules/core/components/virtuoso/StyledGroupedVirtuoso.tsx'; import { StyledGroupHeader } from '@/modules/core/components/virtuoso/StyledGroupHeader.tsx'; import { StyledGroupItemWrapper } from '@/modules/core/components/virtuoso/StyledGroupItemWrapper.tsx'; @@ -41,13 +40,14 @@ import { ExtensionAction, ExtensionGroupState, ExtensionState } from '@/modules/ import { EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP } from '@/modules/extension/Extensions.constants.ts'; import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; const LANGUAGE = 0; const EXTENSIONS = 1; export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) { const { t } = useTranslation(); - const { setAction } = useContext(NavBarContext); + const { setAction } = useNavBarContext(); const { data: serverSettingsData, diff --git a/src/modules/global-search/screens/SearchAll.tsx b/src/modules/global-search/screens/SearchAll.tsx index 2ef2cec0b0..6b58066ad3 100644 --- a/src/modules/global-search/screens/SearchAll.tsx +++ b/src/modules/global-search/screens/SearchAll.tsx @@ -9,7 +9,7 @@ import Card from '@mui/material/Card'; import CardActionArea from '@mui/material/CardActionArea'; import Typography from '@mui/material/Typography'; -import React, { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { StringParam, useQueryParam } from 'use-query-params'; import { useTranslation } from 'react-i18next'; @@ -20,7 +20,6 @@ import { langSortCmp, sourceDefualtLangs, sourceForcedDefaultLangs } from '@/mod import { AppbarSearch } from '@/modules/core/components/AppbarSearch.tsx'; import { LangSelect } from '@/modules/core/components/inputs/LangSelect.tsx'; import { useDebounce } from '@/modules/core/hooks/useDebounce.ts'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { MangaCardProps } from '@/modules/manga/Manga.types.ts'; import { EmptyView } from '@/modules/core/components/placeholder/EmptyView.tsx'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; @@ -31,6 +30,7 @@ import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder import { translateExtensionLanguage } from '@/modules/extension/Extensions.utils.ts'; import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean }; type SourceToLoadingStateMap = Map; @@ -197,7 +197,7 @@ const SourceSearchPreview = React.memo( export const SearchAll: React.FC = () => { const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); const { pathname, state } = useLocation<{ mangaTitle?: string }>(); const isMigrateMode = pathname.startsWith('/migrate/source'); diff --git a/src/modules/library/screens/Library.tsx b/src/modules/library/screens/Library.tsx index 7ad7de8646..49ace56ef6 100644 --- a/src/modules/library/screens/Library.tsx +++ b/src/modules/library/screens/Library.tsx @@ -9,7 +9,7 @@ import Chip, { ChipProps } from '@mui/material/Chip'; import Tab from '@mui/material/Tab'; import { styled } from '@mui/material/styles'; -import { useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react'; +import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { useQueryParam, NumberParam } from 'use-query-params'; import { useTranslation } from 'react-i18next'; import { requestManager } from '@/lib/requests/RequestManager.ts'; @@ -20,7 +20,6 @@ import { LibraryToolbarMenu } from '@/modules/library/components/LibraryToolbarM import { LibraryMangaGrid } from '@/modules/library/components/LibraryMangaGrid.tsx'; import { AppbarSearch } from '@/modules/core/components/AppbarSearch.tsx'; import { UpdateChecker } from '@/modules/core/components/UpdateChecker.tsx'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { useSelectableCollection } from '@/modules/collection/hooks/useSelectableCollection.ts'; import { SelectableCollectionSelectMode } from '@/modules/collection/components/SelectableCollectionSelectMode.tsx'; import { useGetVisibleLibraryMangas } from '@/modules/library/hooks/useGetVisibleLibraryMangas.ts'; @@ -44,6 +43,7 @@ import { useLibraryOptionsContext } from '@/modules/library/contexts/LibraryOpti import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; import { getCategoryMetadata } from '@/modules/category/services/CategoryMetadata.ts'; import { GET_LIBRARY_MANGA_COUNT } from '@/lib/graphql/queries/MangaQuery.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; const TitleWithSizeTag = styled('span')({ display: 'flex', @@ -166,7 +166,7 @@ export function Library() { ); }, [isSelectModeActive, selectedMangas]); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); useLayoutEffect(() => { const title = t('library.title'); const navBarTitle = ( diff --git a/src/modules/library/screens/LibraryDuplicates.tsx b/src/modules/library/screens/LibraryDuplicates.tsx index 70677df1db..e98f8aab8d 100644 --- a/src/modules/library/screens/LibraryDuplicates.tsx +++ b/src/modules/library/screens/LibraryDuplicates.tsx @@ -7,7 +7,7 @@ */ import { useTranslation } from 'react-i18next'; -import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; import IconButton from '@mui/material/IconButton'; import SettingsIcon from '@mui/icons-material/Settings'; import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state'; @@ -16,7 +16,6 @@ import MenuItem from '@mui/material/MenuItem'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import { requestManager } from '@/lib/requests/RequestManager.ts'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx'; import { GridLayouts } from '@/modules/core/components/GridLayouts.tsx'; import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx'; @@ -35,6 +34,7 @@ import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx'; import { LibraryDuplicatesWorkerInput, TMangaDuplicate, TMangaDuplicates } from '@/modules/library/Library.types.ts'; import { GridLayout } from '@/modules/core/Core.types.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; export const LibraryDuplicates = () => { const { t } = useTranslation(); @@ -45,7 +45,7 @@ export const LibraryDuplicates = () => { false, ); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); useLayoutEffect(() => { setTitle(t('library.settings.advanced.duplicates.label.title')); setAction( diff --git a/src/modules/library/screens/LibrarySettings.tsx b/src/modules/library/screens/LibrarySettings.tsx index de8700fde4..14cd55829f 100644 --- a/src/modules/library/screens/LibrarySettings.tsx +++ b/src/modules/library/screens/LibrarySettings.tsx @@ -7,7 +7,7 @@ */ import { useTranslation } from 'react-i18next'; -import { useContext, useLayoutEffect } from 'react'; +import { useLayoutEffect } from 'react'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemButton from '@mui/material/ListItemButton'; @@ -15,7 +15,6 @@ import ListItemText from '@mui/material/ListItemText'; import Switch from '@mui/material/Switch'; import ListSubheader from '@mui/material/ListSubheader'; import { t as translate } from 'i18next'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { GlobalUpdateSettings } from '@/modules/settings/components/globalUpdate/GlobalUpdateSettings.tsx'; import { makeToast } from '@/modules/core/utils/Toast.ts'; import { @@ -39,6 +38,7 @@ import { GET_MANGAS_BASE } from '@/lib/graphql/queries/MangaQuery.ts'; import { MetadataLibrarySettings } from '@/modules/library/Library.types.ts'; import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; const removeNonLibraryMangasFromCategories = async (): Promise => { try { @@ -64,7 +64,7 @@ const removeNonLibraryMangasFromCategories = async (): Promise => { export function LibrarySettings() { const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); useLayoutEffect(() => { setTitle(t('library.settings.title')); diff --git a/src/modules/manga/screens/Manga.tsx b/src/modules/manga/screens/Manga.tsx index f80e11dede..510cdcab63 100644 --- a/src/modules/manga/screens/Manga.tsx +++ b/src/modules/manga/screens/Manga.tsx @@ -12,12 +12,11 @@ import IconButton from '@mui/material/IconButton'; import Stack from '@mui/material/Stack'; import Tooltip from '@mui/material/Tooltip'; import Box from '@mui/material/Box'; -import React, { useContext, useEffect, useLayoutEffect, useRef } from 'react'; +import React, { useEffect, useLayoutEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { isNetworkRequestInFlight } from '@apollo/client/core/networkStatus'; import { requestManager } from '@/lib/requests/RequestManager.ts'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { ChapterList } from '@/modules/chapter/components/ChapterList.tsx'; import { useRefreshManga } from '@/modules/manga/hooks/useRefreshManga.ts'; import { MangaDetails } from '@/modules/manga/components/MangaDetails.tsx'; @@ -27,11 +26,12 @@ import { LoadingPlaceholder } from '@/modules/core/components/placeholder/Loadin import { GetMangaScreenQuery } from '@/lib/graphql/generated/graphql.ts'; import { GET_MANGA_SCREEN } from '@/lib/graphql/queries/MangaQuery.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; export const Manga: React.FC = () => { const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); const { id } = useParams<{ id: string }>(); const autofetchedRef = useRef(false); diff --git a/src/modules/migration/screens/Migrate.tsx b/src/modules/migration/screens/Migrate.tsx index 1178f732da..2b356d5201 100644 --- a/src/modules/migration/screens/Migrate.tsx +++ b/src/modules/migration/screens/Migrate.tsx @@ -6,10 +6,9 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useContext, useEffect, useLayoutEffect, useState } from 'react'; +import { useEffect, useLayoutEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { TMigratableSource } from '@/modules/migration/components/MigrationCard.tsx'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; @@ -23,10 +22,11 @@ import { SOURCE_BASE_FIELDS } from '@/lib/graphql/fragments/SourceFragments.ts'; import { BaseMangaGrid } from '@/modules/manga/components/BaseMangaGrid.tsx'; import { GridLayout } from '@/modules/core/Core.types.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; export const Migrate = () => { const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); const { sourceId: paramSourceId } = useParams<{ sourceId: string }>(); diff --git a/src/modules/navigation-bar/components/DefaultNavBar.tsx b/src/modules/navigation-bar/components/DefaultNavBar.tsx index 2078d46364..67af48404f 100644 --- a/src/modules/navigation-bar/components/DefaultNavBar.tsx +++ b/src/modules/navigation-bar/components/DefaultNavBar.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useCallback, useContext, useLayoutEffect, useMemo, useRef } from 'react'; +import { useCallback, useLayoutEffect, useMemo, useRef } from 'react'; import AppBar from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; @@ -26,7 +26,6 @@ import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import MenuIcon from '@mui/icons-material/Menu'; import Stack from '@mui/material/Stack'; import { useTheme } from '@mui/material/styles'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { useBackButton } from '@/modules/core/hooks/useBackButton.ts'; import { useGetOptionForDirection } from '@/modules/theme/services/ThemeCreator.ts'; import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; @@ -35,6 +34,7 @@ import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; import { MobileBottomBar } from '@/modules/navigation-bar/components/MobileBottomBar.tsx'; import { NavbarItem } from '@/modules/navigation-bar/NavigationBar.types.ts'; import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; const navbarItems: Array = [ { @@ -76,7 +76,7 @@ const navbarItems: Array = [ export function DefaultNavBar() { const { title, action, override, isCollapsed, setIsCollapsed, setAppBarHeight, navBarWidth, setNavBarWidth } = - useContext(NavBarContext); + useNavBarContext(); const theme = useTheme(); const getOptionForDirection = useGetOptionForDirection(); diff --git a/src/modules/settings/screens/About.tsx b/src/modules/settings/screens/About.tsx index 57fd41a0ee..6bdc5d3039 100644 --- a/src/modules/settings/screens/About.tsx +++ b/src/modules/settings/screens/About.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useContext, useLayoutEffect } from 'react'; +import { useLayoutEffect } from 'react'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; @@ -15,7 +15,6 @@ import ListSubheader from '@mui/material/ListSubheader'; import Divider from '@mui/material/Divider'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { ListItemLink } from '@/modules/core/components/ListItemLink.tsx'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; import { UpdateState } from '@/lib/graphql/generated/graphql.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; @@ -23,10 +22,11 @@ import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder import { getBuildTime, getVersion } from '@/modules/app-updates/services/AppUpdateChecker.tsx'; import { VersionInfo } from '@/modules/app-updates/components/VersionInfo.tsx'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; export function About() { const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); useLayoutEffect(() => { setTitle(t('settings.about.title')); diff --git a/src/modules/settings/screens/Appearance.tsx b/src/modules/settings/screens/Appearance.tsx index 38800e2b92..fc2568dd76 100644 --- a/src/modules/settings/screens/Appearance.tsx +++ b/src/modules/settings/screens/Appearance.tsx @@ -16,7 +16,6 @@ import ListSubheader from '@mui/material/ListSubheader'; import Switch from '@mui/material/Switch'; import Link from '@mui/material/Link'; import { useColorScheme } from '@mui/material/styles'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { ThemeMode, ThemeModeContext } from '@/modules/theme/contexts/ThemeModeContext.tsx'; import { Select } from '@/modules/core/components/inputs/Select.tsx'; import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; @@ -36,6 +35,7 @@ import { makeToast } from '@/modules/core/utils/Toast.ts'; import { MetadataThemeSettings } from '@/modules/theme/AppTheme.types.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { AppStorage } from '@/lib/storage/AppStorage.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; export const Appearance = () => { const { t, i18n } = useTranslation(); @@ -43,7 +43,7 @@ export const Appearance = () => { const { mode, setMode } = useColorScheme(); const actualThemeMode = (mode ?? themeMode) as ThemeMode; - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); useLayoutEffect(() => { setTitle(t('settings.appearance.title')); setAction(null); diff --git a/src/modules/settings/screens/ServerSettings.tsx b/src/modules/settings/screens/ServerSettings.tsx index 2bb2f4025a..99381af22f 100644 --- a/src/modules/settings/screens/ServerSettings.tsx +++ b/src/modules/settings/screens/ServerSettings.tsx @@ -7,7 +7,7 @@ */ import { useTranslation, Trans } from 'react-i18next'; -import { useContext, useLayoutEffect, useMemo } from 'react'; +import { useLayoutEffect, useMemo } from 'react'; import Link from '@mui/material/Link'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; @@ -15,7 +15,6 @@ import ListItemText from '@mui/material/ListItemText'; import Switch from '@mui/material/Switch'; import ListSubheader from '@mui/material/ListSubheader'; import { t as translate } from 'i18next'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx'; import { TextSetting } from '@/modules/core/components/settings/text/TextSetting.tsx'; @@ -32,6 +31,7 @@ import { makeToast } from '@/modules/core/utils/Toast.ts'; import { MetadataUpdateSettings } from '@/modules/app-updates/AppUpdateChecker.types.ts'; import { ServerSettings as GqlServerSettings } from '@/modules/settings/Settings.types.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; type ServerSettingsType = Pick< GqlServerSettings, @@ -94,7 +94,7 @@ const getLogFilesCleanupDisplayValue = (ttl: number): string => { export const ServerSettings = () => { const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); useLayoutEffect(() => { setTitle(t('settings.server.title.settings')); diff --git a/src/modules/settings/screens/Settings.tsx b/src/modules/settings/screens/Settings.tsx index aab775f460..e5ba704988 100644 --- a/src/modules/settings/screens/Settings.tsx +++ b/src/modules/settings/screens/Settings.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useContext, useLayoutEffect } from 'react'; +import { useLayoutEffect } from 'react'; import AutoStoriesIcon from '@mui/icons-material/AutoStories'; import List from '@mui/material/List'; import ListAltIcon from '@mui/icons-material/ListAlt'; @@ -26,16 +26,16 @@ import DevicesIcon from '@mui/icons-material/Devices'; import SyncIcon from '@mui/icons-material/Sync'; import PaletteIcon from '@mui/icons-material/Palette'; import { ListItemLink } from '@/modules/core/components/ListItemLink.tsx'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { makeToast } from '@/modules/core/utils/Toast.ts'; import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; export function Settings() { const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); useLayoutEffect(() => { setTitle(t('settings.title')); setAction(null); diff --git a/src/modules/settings/screens/WebUISettings.tsx b/src/modules/settings/screens/WebUISettings.tsx index ae9d91d4bf..8df6946d23 100644 --- a/src/modules/settings/screens/WebUISettings.tsx +++ b/src/modules/settings/screens/WebUISettings.tsx @@ -7,12 +7,12 @@ */ import { useTranslation } from 'react-i18next'; -import { useContext, useLayoutEffect } from 'react'; +import { useLayoutEffect } from 'react'; 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 { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { WebUIUpdateIntervalSetting } from '@/modules/settings/components/webUI/WebUIUpdateIntervalSetting.tsx'; import { TextSetting } from '@/modules/core/components/settings/text/TextSetting.tsx'; @@ -120,7 +120,7 @@ const extractWebUISettings = (settings: ServerSettings): WebUISettingsType => ({ export const WebUISettings = () => { const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); useLayoutEffect(() => { setTitle(t('settings.webui.title.settings')); diff --git a/src/modules/source/screens/SourceConfigure.tsx b/src/modules/source/screens/SourceConfigure.tsx index 9aa5e0707e..1ec2beee91 100644 --- a/src/modules/source/screens/SourceConfigure.tsx +++ b/src/modules/source/screens/SourceConfigure.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { createElement, useContext, useLayoutEffect } from 'react'; +import { createElement, useLayoutEffect } from 'react'; import { useParams } from 'react-router-dom'; import List from '@mui/material/List'; import { useTranslation } from 'react-i18next'; @@ -19,7 +19,6 @@ import { import { ListPreference } from '@/modules/source/components/sourceConfiguration/ListPreference.tsx'; import { EditTextPreference } from '@/modules/source/components/sourceConfiguration/EditTextPreference.tsx'; import { MultiSelectListPreference } from '@/modules/source/components/sourceConfiguration/MultiSelectListPreference.tsx'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; @@ -28,6 +27,7 @@ import { GET_SOURCE_SETTINGS } from '@/lib/graphql/queries/SourceQuery.ts'; import { makeToast } from '@/modules/core/utils/Toast.ts'; import { PreferenceProps } from '@/modules/source/Source.types.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; function getPrefComponent(type: string) { switch (type) { @@ -48,7 +48,7 @@ function getPrefComponent(type: string) { export function SourceConfigure() { const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); useLayoutEffect(() => { setTitle(t('source.configuration.title')); diff --git a/src/modules/source/screens/SourceMangas.tsx b/src/modules/source/screens/SourceMangas.tsx index 63c1a353cb..74bd74e353 100644 --- a/src/modules/source/screens/SourceMangas.tsx +++ b/src/modules/source/screens/SourceMangas.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useParams, useNavigate, useLocation, useSearchParams } from 'react-router-dom'; import IconButton from '@mui/material/IconButton'; import SettingsIcon from '@mui/icons-material/Settings'; @@ -36,7 +36,6 @@ import { GetSourceMangasFetchMutationVariables, SourceType, } from '@/lib/graphql/generated/graphql.ts'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; import { useLocalStorage, useSessionStorage } from '@/modules/core/hooks/useStorage.tsx'; import { AppStorage } from '@/lib/storage/AppStorage.ts'; @@ -53,6 +52,7 @@ import { MangaIdInfo } from '@/modules/manga/Manga.types.ts'; import { GridLayout } from '@/modules/core/Core.types.ts'; import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; const DEFAULT_SOURCE: Pick = { id: '-1' }; @@ -204,7 +204,7 @@ const useSourceManga = ( export function SourceMangas() { const { t } = useTranslation(); - const { setTitle, setAction, appBarHeight } = useContext(NavBarContext); + const { setTitle, setAction, appBarHeight } = useNavBarContext(); const { sourceId } = useParams<{ sourceId: string }>(); diff --git a/src/modules/source/screens/Sources.tsx b/src/modules/source/screens/Sources.tsx index 2b47e1c0f1..79dcf90e4a 100644 --- a/src/modules/source/screens/Sources.tsx +++ b/src/modules/source/screens/Sources.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Fragment, useContext, useEffect, useLayoutEffect, useMemo } from 'react'; +import { Fragment, useEffect, useLayoutEffect, useMemo } from 'react'; import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; @@ -24,13 +24,13 @@ import { import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; import { SourceCard } from '@/modules/source/components/SourceCard.tsx'; import { LangSelect } from '@/modules/core/components/inputs/LangSelect.tsx'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { SourceType } from '@/lib/graphql/generated/graphql.ts'; import { translateExtensionLanguage } from '@/modules/extension/Extensions.utils.ts'; import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; function sourceToLangList(sources: Pick[]) { const result = new Set(); @@ -65,7 +65,7 @@ function groupByLang>( export function Sources() { const { t } = useTranslation(); - const { setAction } = useContext(NavBarContext); + const { setAction } = useNavBarContext(); const [shownLangs, setShownLangs] = useLocalStorage('shownSourceLangs', sourceDefualtLangs()); const [showNsfw] = useLocalStorage('showNsfw', true); diff --git a/src/modules/tracker/screens/TrackingSettings.tsx b/src/modules/tracker/screens/TrackingSettings.tsx index 44059263af..10c05d490f 100644 --- a/src/modules/tracker/screens/TrackingSettings.tsx +++ b/src/modules/tracker/screens/TrackingSettings.tsx @@ -7,14 +7,13 @@ */ import { useTranslation } from 'react-i18next'; -import { useContext, useLayoutEffect } from 'react'; +import { useLayoutEffect } from 'react'; import List from '@mui/material/List'; - import ListSubheader from '@mui/material/ListSubheader'; import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; import Switch from '@mui/material/Switch'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; @@ -32,7 +31,7 @@ import { getErrorMessage } from '@/lib/HelperFunctions.ts'; export const TrackingSettings = () => { const { t } = useTranslation(); - const { setTitle } = useContext(NavBarContext); + const { setTitle } = useNavBarContext(); useLayoutEffect(() => { setTitle(t('tracking.settings.title.settings')); diff --git a/src/modules/updates/screens/Updates.tsx b/src/modules/updates/screens/Updates.tsx index af5e4a411b..e0fa3fd405 100644 --- a/src/modules/updates/screens/Updates.tsx +++ b/src/modules/updates/screens/Updates.tsx @@ -7,13 +7,12 @@ */ import Typography from '@mui/material/Typography'; -import React, { useCallback, useContext, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx'; import { ChapterType } from '@/lib/graphql/generated/graphql.ts'; -import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { UpdateChecker } from '@/modules/core/components/UpdateChecker.tsx'; import { StyledGroupedVirtuoso } from '@/modules/core/components/virtuoso/StyledGroupedVirtuoso.tsx'; import { StyledGroupHeader } from '@/modules/core/components/virtuoso/StyledGroupHeader.tsx'; @@ -23,6 +22,7 @@ import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts' import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { ChapterUpdateCard } from '@/modules/updates/components/ChapterUpdateCard.tsx'; +import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; const groupByDate = (updates: Pick[]): [date: string, items: number][] => { if (!updates.length) { @@ -41,7 +41,7 @@ const groupByDate = (updates: Pick[]): [date: string, export const Updates: React.FC = () => { const { t } = useTranslation(); - const { setTitle, setAction } = useContext(NavBarContext); + const { setTitle, setAction } = useNavBarContext(); const { data: chapterUpdateData, loading: isLoading, From ed52199c606e5445c2ba45b69e33c12c1503f829 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:27:53 +0100 Subject: [PATCH 3/3] tmp - Add infinite scroll to reader --- src/lib/HelperFunctions.ts | 2 + src/modules/core/hoc/withPropsFrom.tsx | 13 +- .../progress-bar/ReaderProgressBarSlot.tsx | 1 - .../desktop/ReaderProgressBarSlotDesktop.tsx | 3 - .../variants/StandardReaderProgressBar.tsx | 1 - .../components/viewer/ReaderChapterViewer.tsx | 347 ++++++++++++++++++ .../ReaderInfiniteScrollUpdateChapter.tsx | 38 ++ .../viewer/ReaderTransitionPage.tsx | 119 ++++-- .../reader/components/viewer/ReaderViewer.tsx | 189 +++++----- .../components/viewer/pager/BasePager.tsx | 4 + .../state/ReaderStateChaptersContext.tsx | 12 +- .../ReaderStateChaptersContextProvider.tsx | 11 +- .../useReaderInfiniteScrollUpdateChapter.ts | 85 +++++ .../reader/hooks/useReaderResetStates.ts | 6 +- .../reader/hooks/useReaderSetChaptersState.ts | 61 +-- .../reader/hooks/useReaderSetPagesState.ts | 29 +- src/modules/reader/screens/Reader.tsx | 65 +--- src/modules/reader/services/ReaderControls.ts | 62 ++-- src/modules/reader/services/ReaderService.ts | 23 +- src/modules/reader/types/Reader.types.ts | 11 +- src/modules/reader/utils/Reader.utils.ts | 33 +- 21 files changed, 824 insertions(+), 291 deletions(-) create mode 100644 src/modules/reader/components/viewer/ReaderChapterViewer.tsx create mode 100644 src/modules/reader/components/viewer/ReaderInfiniteScrollUpdateChapter.tsx create mode 100644 src/modules/reader/hooks/useReaderInfiniteScrollUpdateChapter.ts diff --git a/src/lib/HelperFunctions.ts b/src/lib/HelperFunctions.ts index b9685c0435..9561089b01 100644 --- a/src/lib/HelperFunctions.ts +++ b/src/lib/HelperFunctions.ts @@ -39,3 +39,5 @@ export const getValueFromObject = (obj: Record, key: string): T }; export const coerceIn = (value: number, min: number, max: number): number => Math.min(Math.max(value, min), max); + +export const noOp = () => {}; diff --git a/src/modules/core/hoc/withPropsFrom.tsx b/src/modules/core/hoc/withPropsFrom.tsx index e14ed295af..76e9a78a8d 100644 --- a/src/modules/core/hoc/withPropsFrom.tsx +++ b/src/modules/core/hoc/withPropsFrom.tsx @@ -6,9 +6,9 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { ComponentType, forwardRef, memo } from 'react'; +import { ComponentType, memo, forwardRef } from 'react'; -type PropsSourceCreator = () => T; +type PropsSourceCreator> = (props: Props) => T; export const withPropsFrom = < ComponentProps extends Record, @@ -16,12 +16,17 @@ export const withPropsFrom = < SourcePropKeys extends keyof (ComponentProps | MergeObjectsArray), >( Component: ComponentType, - propsSources: { [K in keyof SourceProps]: PropsSourceCreator }, + propsSources: { + [K in keyof SourceProps]: PropsSourceCreator>; + }, sourcePropKeys: SourcePropKeys[], ) => memo( forwardRef>((props, ref) => { - const sourceProps = propsSources.reduce((acc, propsSource) => ({ ...acc, ...propsSource() }), {}); + const sourceProps = propsSources.reduce( + (acc, propsSource) => ({ ...acc, ...propsSource(props as Omit) }), + {}, + ); const selectedProps = Object.fromEntries( Object.entries(sourceProps).filter(([key]) => sourcePropKeys.includes(key as SourcePropKeys)), diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx index 86d1bcdb7a..b11e27fd87 100644 --- a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx @@ -17,7 +17,6 @@ export const ReaderProgressBarSlot = memo( ({ pageName, progressBarPosition, slotProps, children }: ReaderProgressBarSlotProps & { children?: ReactNode }) => ( diff --git a/src/modules/reader/components/overlay/progress-bar/desktop/ReaderProgressBarSlotDesktop.tsx b/src/modules/reader/components/overlay/progress-bar/desktop/ReaderProgressBarSlotDesktop.tsx index a1650de022..8cb164c8bd 100644 --- a/src/modules/reader/components/overlay/progress-bar/desktop/ReaderProgressBarSlotDesktop.tsx +++ b/src/modules/reader/components/overlay/progress-bar/desktop/ReaderProgressBarSlotDesktop.tsx @@ -16,7 +16,6 @@ import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts'; export const ReaderProgressBarSlotDesktop = memo( ({ pageName, - pageUrl, primaryPageLoadState, secondaryPageLoadState, progressBarPosition, @@ -25,7 +24,6 @@ export const ReaderProgressBarSlotDesktop = memo( showDraggingStyle, }: Pick & { pageName: string; - pageUrl: string; primaryPageLoadState: boolean; secondaryPageLoadState?: boolean; isCurrentPage: boolean; @@ -36,7 +34,6 @@ export const ReaderProgressBarSlotDesktop = memo( return ( ( & + Pick< + IReaderSettings, + 'readingMode' | 'shouldOffsetDoubleSpreads' | 'readingDirection' | 'readerWidth' | 'pageScaleMode' + > & { + updateCurrentPageIndex: ReturnType; + chapterId: ChapterIdInfo['id']; + lastPageRead: number; + isInitialChapter: boolean; + isCurrentChapter: boolean; + isLeadingChapter: boolean; + isTrailingChapter: boolean; + imageRefs: MutableRefObject<(HTMLElement | null)[]>; + }) => { + const { t } = useTranslation(); + const { direction: themeDirection } = useTheme(); + const { resumeMode } = useLocation<{ + resumeMode: ReaderResumeMode; + }>().state ?? { resumeMode: ReaderResumeMode.START }; + + const [fetchPages, pagesResponse] = requestManager.useGetChapterPagesFetch(chapterId ?? -1); + + const [arePagesFetched, setArePagesFetched] = useState(false); + const [totalPages, setTotalPages] = useState( + READER_STATE_PAGES_DEFAULTS.totalPages, + ); + const [pageUrls, setPageUrls] = useState(READER_STATE_PAGES_DEFAULTS.pageUrls); + const [pages, setPages] = useState(READER_STATE_PAGES_DEFAULTS.pages); + const [pageLoadStates, setPageLoadStates] = useState( + READER_STATE_PAGES_DEFAULTS.pageLoadStates, + ); + const [pagesToSpreadState, setPagesToSpreadState] = useState( + pageLoadStates.map(({ url }) => ({ url, isSpread: false })), + ); + + const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null)); + + const actualPages = useMemo(() => { + const arePagesLoaded = !!totalPages; + if (!arePagesLoaded) { + return pages; + } + + const isSpreadStateUpdated = totalPages === pagesToSpreadState.length; + if (!isSpreadStateUpdated) { + return pages; + } + + if (readingMode === ReadingMode.DOUBLE_PAGE) { + return getDoublePageModePages(pageUrls, pagesToSpreadState, shouldOffsetDoubleSpreads, readingDirection); + } + + return pages; + }, [pagesToSpreadState, readingMode, shouldOffsetDoubleSpreads, readingDirection, totalPages]); + + const Pager = useMemo(() => getPagerForReadingMode(readingMode), [readingMode]); + const isLtrReadingDirection = readingDirection === ReadingDirection.LTR; + + if (isCurrentChapter) { + // eslint-disable-next-line no-param-reassign + globalImageRefs.current = imageRefs.current; + } + + const previousTotalPages = useRef(totalPages); + const resetPagesSpreadState = previousTotalPages.current !== totalPages; + if (resetPagesSpreadState) { + previousTotalPages.current = totalPages; + setPagesToSpreadState(pageLoadStates.map(({ url }) => ({ url, isSpread: false }))); + } + + const doFetchPages = useCallback(() => { + if (!chapterId) { + return; + } + + setArePagesFetched(false); + + fetchPages({ variables: { input: { chapterId } } }).catch( + defaultPromiseErrorHandler(`ReaderChapterViewer(${chapterId})::fetchPages`), + ); + }, [fetchPages, chapterId]); + + const onLoad = useMemo( + () => + createUpdateReaderPageLoadState( + actualPages, + pagesToSpreadState, + setPagesToSpreadState, + pageLoadStates, + (value) => { + if (isCurrentChapter) { + setContextPageLoadStates(value); + } + + setPageLoadStates(value); + }, + readingMode, + ), + // do not add "pagesToSpreadState" and "pageLoadStates" as a dependency, otherwise, every page gets re-rendered + // when they change which impacts the performance massively (depending on the total page count) + [actualPages, readingMode], + ); + + const onError = useCallback((pageIndex: number, url: string) => { + setPageLoadStates((statePageLoadStates) => { + const pageLoadState = statePageLoadStates[pageIndex]; + + if (isPageOfOutdatedPageLoadStates(url, pageLoadState)) { + return statePageLoadStates; + } + + return statePageLoadStates.toSpliced(pageIndex, 1, { + ...pageLoadState, + loaded: false, + error: true, + }); + }); + }, []); + + useEffect(() => { + doFetchPages(); + }, [chapterId]); + + const updatePageState = ( + value: T, + setLocalState: (value: T) => void, + setGlobalState: (value: T) => void, + forceLocal: boolean = false, + ) => { + if (forceLocal || !arePagesFetched) { + setLocalState(value); + } + + if (isCurrentChapter) { + setGlobalState(value); + } + }; + useReaderSetPagesState( + isCurrentChapter, + pagesResponse, + (() => { + if (isInitialChapter) { + return resumeMode; + } + + if (isLeadingChapter) { + return ReaderResumeMode.END; + } + + if (isTrailingChapter) { + return ReaderResumeMode.START; + } + + return ReaderResumeMode.START; + })(), + lastPageRead, + setArePagesFetched, + (value) => updatePageState(value, setTotalPages, setContextTotalPages), + (value) => updatePageState(value, setPages, setContextPages), + setPageUrls, + (value) => updatePageState(value, setPageLoadStates, setContextPageLoadStates), + (value) => { + if (isCurrentChapter) { + setContextCurrentPageIndex(value); + } + }, + (value) => { + if (isInitialChapter && !arePagesFetched) { + setPageToScrollToIndex(value); + } + }, + () => {}, + ); + + useReaderConvertPagesForReadingMode( + currentPageIndex, + actualPages, + pageUrls, + (value) => updatePageState(value, setPages, setContextPages, true), + setPagesToSpreadState, + (value) => { + if (isCurrentChapter) { + updateCurrentPageIndex(value); + } + }, + readingMode, + ); + + if (pagesResponse.error) { + return ( + { + doFetchPages(); + }} + /> + ); + } + + if (pagesResponse.loading || !arePagesFetched) { + return ( + + + + ); + } + + if (chapterId != null && !totalPages) { + return ; + } + + return ( + + + {(isInitialChapter || isLeadingChapter) && ( + + )} + + {(isInitialChapter || isTrailingChapter) && ( + + )} + + ); +}; + +export const ReaderChapterViewer = withPropsFrom(memo(BaseReaderChapterViewer), [], []); diff --git a/src/modules/reader/components/viewer/ReaderInfiniteScrollUpdateChapter.tsx b/src/modules/reader/components/viewer/ReaderInfiniteScrollUpdateChapter.tsx new file mode 100644 index 0000000000..84af228039 --- /dev/null +++ b/src/modules/reader/components/viewer/ReaderInfiniteScrollUpdateChapter.tsx @@ -0,0 +1,38 @@ +/* + * 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 { memo } from 'react'; +import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx'; +import { useReaderInfiniteScrollUpdateChapter } from '@/modules/reader/hooks/useReaderInfiniteScrollUpdateChapter.ts'; +import { ReadingMode } from '@/modules/reader/types/Reader.types.ts'; +import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts'; + +const BaseReaderInfiniteScrollUpdateChapter = ({ + readingMode, + isCurrentChapter, + firstImage, + lastImage, + openChapter, +}: { + readingMode: ReadingMode; + isCurrentChapter: boolean; + firstImage: HTMLElement | null; + lastImage: HTMLElement | null; + openChapter: ReturnType; +}) => { + useReaderInfiniteScrollUpdateChapter('previous', readingMode, firstImage, isCurrentChapter, openChapter); + useReaderInfiniteScrollUpdateChapter('next', readingMode, lastImage, isCurrentChapter, openChapter); + + return null; +}; + +export const ReaderInfiniteScrollUpdateChapter = withPropsFrom( + memo(BaseReaderInfiniteScrollUpdateChapter), + [() => ({ openChapter: ReaderControls.useOpenChapter() })], + ['openChapter'], +); diff --git a/src/modules/reader/components/viewer/ReaderTransitionPage.tsx b/src/modules/reader/components/viewer/ReaderTransitionPage.tsx index 2db8329c55..7132769c43 100644 --- a/src/modules/reader/components/viewer/ReaderTransitionPage.tsx +++ b/src/modules/reader/components/viewer/ReaderTransitionPage.tsx @@ -11,16 +11,14 @@ import Stack from '@mui/material/Stack'; import { useTranslation } from 'react-i18next'; import Button from '@mui/material/Button'; import { Link } from 'react-router-dom'; -import { memo } from 'react'; +import { ComponentProps, memo, useMemo } from 'react'; import { alpha, useTheme } from '@mui/material/styles'; import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx'; import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; -import { ChapterScanlatorInfo } from '@/modules/chapter/services/Chapters.ts'; -import { TChapterReader } from '@/modules/chapter/Chapter.types.ts'; +import { ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts'; import { IReaderSettings, ReaderPageScaleMode, - ReaderStateChapters, ReaderTransitionPageMode, ReadingMode, TReaderScrollbarContext, @@ -38,19 +36,22 @@ import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; import { NavbarContextType } from '@/modules/navigation-bar/NavigationBar.types.ts'; import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx'; import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx'; -import { getValueFromObject } from '@/lib/HelperFunctions.ts'; +import { getValueFromObject, noOp } from '@/lib/HelperFunctions.ts'; import { READER_BACKGROUND_TO_COLOR } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; -import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx'; import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts'; +import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx'; +import { ChapterType } from '@/lib/graphql/generated/graphql.ts'; const ChapterInfo = ({ title, - chapter, + name, + scanlator, backgroundColor, }: { title: string; - chapter?: Pick & ChapterScanlatorInfo; + name?: ChapterType['name']; + scanlator?: ChapterType['scanlator']; backgroundColor: IReaderSettings['backgroundColor']; }) => { const theme = useTheme(); @@ -60,7 +61,7 @@ const ChapterInfo = ({ ); const disabledText = alpha(contrastText, 0.5); - if (!chapter) { + if (!name) { return null; } @@ -68,11 +69,11 @@ const ChapterInfo = ({ {title} - {chapter.name} + {name} - {chapter.scanlator && ( + {scanlator && ( - {chapter.scanlator} + {scanlator} )} @@ -86,30 +87,39 @@ const BaseReaderTransitionPage = ({ pageScaleMode, backgroundColor, manga, - currentChapter, - previousChapter, - nextChapter, + currentChapterName, + currentChapterScanlator, + previousChapterName, + previousChapterScanlator, + nextChapterName, + nextChapterScanlator, scrollbarXSize, scrollbarYSize, readerNavBarWidth, + handleBack, }: Pick & Pick & - Pick & Pick & Pick & Pick & { + // eslint-disable-next-line react/no-unused-prop-types + chapterId: ChapterIdInfo['id']; + currentChapterName?: ChapterType['name']; + currentChapterScanlator?: ChapterType['scanlator']; + previousChapterName?: ChapterType['name']; + previousChapterScanlator?: ChapterType['scanlator']; + nextChapterName?: ChapterType['name']; + nextChapterScanlator?: ChapterType['scanlator']; type: Exclude; + handleBack: () => void; }) => { const { t } = useTranslation(); - const handleBack = useBackButton(); - const isPreviousType = type === ReaderTransitionPageMode.PREVIOUS; const isNextType = type === ReaderTransitionPageMode.NEXT; - const isFirstChapter = !!currentChapter && !previousChapter; - const isLastChapter = !!currentChapter && !nextChapter; - + const isFirstChapter = !!currentChapterName && !previousChapterName; + const isLastChapter = !!currentChapterName && !nextChapterName; const isFitWidthPageScaleMode = [ReaderPageScaleMode.SCREEN, ReaderPageScaleMode.WIDTH].includes(pageScaleMode); if (!isTransitionPageVisible(type, transitionPageMode, readingMode)) { @@ -176,23 +186,26 @@ const BaseReaderTransitionPage = ({ {isPreviousType && !isFirstChapter && ( )} - {!!currentChapter && ( + {!!currentChapterName && ( )} {isNextType && !isLastChapter && ( )} @@ -231,20 +244,65 @@ const BaseReaderTransitionPage = ({ }; export const ReaderTransitionPage = withPropsFrom( - memo(BaseReaderTransitionPage), + memo(BaseReaderTransitionPage) as typeof BaseReaderTransitionPage, [ useReaderStateMangaContext, - useReaderStateChaptersContext, + ({ chapterId }: Pick, 'chapterId'>) => { + const { chapters } = useReaderStateChaptersContext(); + + const currentChapterIndex = useMemo( + () => chapters.findIndex((chapter) => chapter.id === chapterId), + [chapterId, chapters], + ); + const currentChapter = chapters[currentChapterIndex]; + // chapters are sorted from latest to oldest + const previousChapter = useMemo(() => chapters[currentChapterIndex + 1], [currentChapterIndex, chapters]); + const nextChapter = useMemo(() => chapters[currentChapterIndex - 1], [currentChapterIndex, chapters]); + + return { + currentChapterName: currentChapter?.name, + currentChapterScanlator: currentChapter?.scanlator, + previousChapterName: previousChapter?.name, + previousChapterScanlator: previousChapter?.name, + nextChapterName: nextChapter?.name, + nextChapterScanlator: nextChapter?.scanlator, + }; + }, useReaderScrollbarContext, useNavBarContext, userReaderStatePagesContext, ReaderService.useSettingsWithoutDefaultFlag, + ({ chapterId, type }: Pick, 'chapterId' | 'type'>) => { + const handleBack = useBackButton(); + const { chapters } = useReaderStateChaptersContext(); + + const currentChapterIndex = useMemo( + () => chapters.findIndex((chapter) => chapter.id === chapterId), + [chapterId, chapters], + ); + + // chapters are sorted from latest to oldest + const isLastChapter = currentChapterIndex === 0; + const isFirstChapter = currentChapterIndex === chapters.length - 1; + + const handleBackFirstChapter = type === ReaderTransitionPageMode.PREVIOUS && isFirstChapter; + const handleBackLastChapter = type === ReaderTransitionPageMode.NEXT && isLastChapter; + + const needsToHandleBack = handleBackFirstChapter || handleBackLastChapter; + + return { + handleBack: needsToHandleBack ? handleBack : noOp, + }; + }, ], [ 'manga', - 'currentChapter', - 'previousChapter', - 'nextChapter', + 'currentChapterName', + 'currentChapterScanlator', + 'previousChapterName', + 'previousChapterScanlator', + 'nextChapterName', + 'nextChapterScanlator', 'scrollbarXSize', 'scrollbarYSize', 'readerNavBarWidth', @@ -252,5 +310,6 @@ export const ReaderTransitionPage = withPropsFrom( 'transitionPageMode', 'readingMode', 'pageScaleMode', + 'handleBack', ], ); diff --git a/src/modules/reader/components/viewer/ReaderViewer.tsx b/src/modules/reader/components/viewer/ReaderViewer.tsx index ba7f5745bd..a227468c8e 100644 --- a/src/modules/reader/components/viewer/ReaderViewer.tsx +++ b/src/modules/reader/components/viewer/ReaderViewer.tsx @@ -10,13 +10,11 @@ import { ForwardedRef, forwardRef, memo, - useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, - useState, } from 'react'; import Stack from '@mui/material/Stack'; import { useTheme } from '@mui/material/styles'; @@ -24,19 +22,16 @@ import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; import { IReaderSettings, PageInViewportType, - ReaderPageSpreadState, - ReaderTransitionPageMode, + ReaderStateChapters, ReadingDirection, ReadingMode, TReaderScrollbarContext, } from '@/modules/reader/types/Reader.types.ts'; import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx'; -import { getDoublePageModePages, isPageOfOutdatedPageLoadStates } from '@/modules/reader/utils/ReaderPager.utils.tsx'; import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx'; import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts'; import { - getPagerForReadingMode, isContinuousReadingMode, isContinuousVerticalReadingMode, shouldApplyReaderWidth, @@ -48,7 +43,6 @@ import { TReaderOverlayContext } from '@/modules/reader/types/ReaderOverlay.type import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts'; import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx'; import { useReaderAutoScrollContext } from '@/modules/reader/contexts/ReaderAutoScrollContext.tsx'; -import { createUpdateReaderPageLoadState } from '@/modules/reader/utils/Reader.utils.ts'; import { TReaderTapZoneContext } from '@/modules/reader/types/TapZoneLayout.types.ts'; import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx'; import { useReaderAutoScroll } from '@/modules/reader/hooks/useReaderAutoScroll.ts'; @@ -57,8 +51,8 @@ import { useReaderHorizontalModeInvertXYScrolling } from '@/modules/reader/hooks import { useReaderHideCursorOnInactivity } from '@/modules/reader/hooks/useReaderHideCursorOnInactivity.ts'; import { useReaderScrollToStartOnPageChange } from '@/modules/reader/hooks/useReaderScrollToStartOnPageChange.ts'; import { useReaderHandlePageSelection } from '@/modules/reader/hooks/useReaderHandlePageSelection.ts'; -import { useReaderConvertPagesForReadingMode } from '@/modules/reader/hooks/useReaderConvertPagesForReadingMode.ts'; -import { ReaderTransitionPage } from '@/modules/reader/components/viewer/ReaderTransitionPage.tsx'; +import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; +import { ReaderChapterViewer } from '@/modules/reader/components/viewer/ReaderChapterViewer.tsx'; const READING_MODE_TO_IN_VIEWPORT_TYPE: Record = { [ReadingMode.SINGLE_PAGE]: PageInViewportType.X, @@ -75,18 +69,18 @@ const BaseReaderViewer = forwardRef( pageToScrollToIndex, setPageToScrollToIndex, pages, - setPages, totalPages, - pageUrls, - pageLoadStates, + setPages, setPageLoadStates, + setTotalPages, + setCurrentPageIndex, transitionPageMode, retryFailedPagesKeyPrefix, readingMode, - shouldOffsetDoubleSpreads, readingDirection, readerWidth, pageScaleMode, + shouldOffsetDoubleSpreads, setScrollbarXSize, setScrollbarYSize, isVisible: isOverlayVisible, @@ -94,26 +88,31 @@ const BaseReaderViewer = forwardRef( updateCurrentPageIndex, showPreview, setShowPreview, + initialChapter, + currentChapter, + chapters, + visibleChapters, }: Pick< ReaderStatePages, | 'currentPageIndex' | 'pageToScrollToIndex' | 'setPageToScrollToIndex' | 'pages' - | 'setPages' | 'totalPages' - | 'pageUrls' - | 'pageLoadStates' + | 'setPages' | 'setPageLoadStates' + | 'setTotalPages' + | 'setCurrentPageIndex' | 'transitionPageMode' | 'retryFailedPagesKeyPrefix' > & Pick< IReaderSettings, - 'readingMode' | 'shouldOffsetDoubleSpreads' | 'readingDirection' | 'readerWidth' | 'pageScaleMode' + 'readingMode' | 'readingDirection' | 'readerWidth' | 'pageScaleMode' | 'shouldOffsetDoubleSpreads' > & Pick & Pick & + Pick & TReaderTapZoneContext & { updateCurrentPageIndex: ReturnType; }, @@ -140,81 +139,15 @@ const BaseReaderViewer = forwardRef( const handleClick = ReaderControls.useHandleClick(scrollElementRef.current); - const previousTotalPages = useRef(totalPages); - const [pagesToSpreadState, setPagesToSpreadState] = useState( - pageLoadStates.map(({ url }) => ({ url, isSpread: false })), - ); - - const resetPagesSpreadState = previousTotalPages.current !== totalPages; - if (resetPagesSpreadState) { - previousTotalPages.current = totalPages; - setPagesToSpreadState(pageLoadStates.map(({ url }) => ({ url, isSpread: false }))); - } - const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null)); - const actualPages = useMemo(() => { - const arePagesLoaded = !!totalPages; - if (!arePagesLoaded) { - return pages; - } - - if (readingMode === ReadingMode.DOUBLE_PAGE) { - return getDoublePageModePages( - pageUrls, - pagesToSpreadState, - shouldOffsetDoubleSpreads, - readingDirection, - ); - } - - return pages; - }, [pagesToSpreadState, readingMode, shouldOffsetDoubleSpreads, readingDirection, totalPages]); - - const Pager = useMemo(() => getPagerForReadingMode(readingMode), [readingMode]); const inViewportType = READING_MODE_TO_IN_VIEWPORT_TYPE[readingMode]; const isLtrReadingDirection = readingDirection === ReadingDirection.LTR; - - const onLoad = useMemo( - () => - createUpdateReaderPageLoadState( - actualPages, - pagesToSpreadState, - setPagesToSpreadState, - pageLoadStates, - setPageLoadStates, - readingMode, - ), - // do not add "pagesToSpreadState" and "pageLoadStates" as a dependency, otherwise, every page gets re-rendered - // when they change which impacts the performance massively (depending on the total page count) - [actualPages, readingMode], + const initialChapterIndex = useMemo( + () => chapters.findIndex((chapter) => chapter.id === initialChapter?.id), + [initialChapter?.id], ); - const onError = useCallback((pageIndex: number, url: string) => { - setPageLoadStates((statePageLoadStates) => { - const pageLoadState = statePageLoadStates[pageIndex]; - - if (isPageOfOutdatedPageLoadStates(url, pageLoadState)) { - return statePageLoadStates; - } - - return statePageLoadStates.toSpliced(pageIndex, 1, { - ...pageLoadState, - loaded: false, - error: true, - }); - }); - }, []); - - useReaderConvertPagesForReadingMode( - currentPageIndex, - actualPages, - pageUrls, - setPages, - setPagesToSpreadState, - updateCurrentPageIndex, - readingMode, - ); useReaderHandlePageSelection( pageToScrollToIndex, currentPageIndex, @@ -245,6 +178,11 @@ const BaseReaderViewer = forwardRef( ); useReaderAutoScroll(isOverlayVisible, automaticScrolling); + // should not be possible, only here to get rid of possible undefined tsc errors + if (!initialChapter || !currentChapter) { + return null; + } + return ( - - - + {/* chapters are sorted by latest to oldest, thus, loop over it in reversed order */} + {chapters + .slice( + Math.max(0, initialChapterIndex - visibleChapters.trailing), + Math.min(chapters.length, initialChapterIndex + visibleChapters.leading + 1), + ) + .map((_, index, chaptersToRender) => { + const chapter = chaptersToRender[Math.max(0, chaptersToRender.length - index - 1)]; + + const isInitialChapter = chapter.id === initialChapter.id; + const isCurrentChapter = chapter.id === currentChapter.id; + const isLeadingChapter = initialChapter.sourceOrder > chapter.sourceOrder; + const isTrailingChapter = initialChapter.sourceOrder < chapter.sourceOrder; + + return ( + { + if (isCurrentChapter) { + return currentPageIndex; + } + + if (isLeadingChapter) { + return chapter.pageCount - 1; + } + + return 0; + })()} + isInitialChapter={isInitialChapter} + isCurrentChapter={isCurrentChapter} + isLeadingChapter={isLeadingChapter} + isTrailingChapter={isTrailingChapter} + imageRefs={imageRefs} + setPages={setPages} + setPageLoadStates={setPageLoadStates} + setTotalPages={setTotalPages} + setCurrentPageIndex={setCurrentPageIndex} + setPageToScrollToIndex={setPageToScrollToIndex} + transitionPageMode={transitionPageMode} + retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix} + readingMode={readingMode} + readerWidth={readerWidth} + pageScaleMode={pageScaleMode} + shouldOffsetDoubleSpreads={shouldOffsetDoubleSpreads} + readingDirection={readingDirection} + updateCurrentPageIndex={updateCurrentPageIndex} + /> + ); + })} ); }, @@ -305,24 +281,25 @@ export const ReaderViewer = withPropsFrom( useReaderOverlayContext, () => ({ updateCurrentPageIndex: ReaderControls.useUpdateCurrentPageIndex() }), useReaderTapZoneContext, + useReaderStateChaptersContext, ], [ 'currentPageIndex', 'pageToScrollToIndex', 'setPageToScrollToIndex', 'pages', - 'setPages', 'totalPages', - 'pageUrls', - 'pageLoadStates', + 'setPages', 'setPageLoadStates', - 'transitionPageMode', + 'setTotalPages', + 'setCurrentPageIndex', 'retryFailedPagesKeyPrefix', 'readingMode', - 'shouldOffsetDoubleSpreads', 'readingDirection', 'readerWidth', 'pageScaleMode', + 'shouldOffsetDoubleSpreads', + 'transitionPageMode', 'setScrollbarXSize', 'setScrollbarYSize', 'isVisible', @@ -330,5 +307,9 @@ export const ReaderViewer = withPropsFrom( 'updateCurrentPageIndex', 'showPreview', 'setShowPreview', + 'initialChapter', + 'currentChapter', + 'chapters', + 'visibleChapters', ], ); diff --git a/src/modules/reader/components/viewer/pager/BasePager.tsx b/src/modules/reader/components/viewer/pager/BasePager.tsx index 0fbe68d106..28b603434b 100644 --- a/src/modules/reader/components/viewer/pager/BasePager.tsx +++ b/src/modules/reader/components/viewer/pager/BasePager.tsx @@ -52,6 +52,10 @@ const BaseBasePager = ({ const setRef = useCallback( (pagesIndex: number, element: HTMLElement | null) => { + if (!imageRefs) { + return; + } + // eslint-disable-next-line no-param-reassign imageRefs.current[pagesIndex] = element; }, diff --git a/src/modules/reader/contexts/state/ReaderStateChaptersContext.tsx b/src/modules/reader/contexts/state/ReaderStateChaptersContext.tsx index dbf4f04dcf..42023072ca 100644 --- a/src/modules/reader/contexts/state/ReaderStateChaptersContext.tsx +++ b/src/modules/reader/contexts/state/ReaderStateChaptersContext.tsx @@ -9,9 +9,19 @@ import { createContext, useContext } from 'react'; import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts'; -export const ReaderStateChaptersContext = createContext({ +export const READER_STATE_CHAPTERS_DEFAULTS: Omit = { mangaChapters: [], chapters: [], + visibleChapters: { + leading: 0, + trailing: 0, + lastLeadingChapterSourceOrder: 99999, + lastTrailingChapterSourceOrder: -1, + }, +}; + +export const ReaderStateChaptersContext = createContext({ + ...READER_STATE_CHAPTERS_DEFAULTS, setReaderStateChapters: () => {}, }); diff --git a/src/modules/reader/contexts/state/ReaderStateChaptersContextProvider.tsx b/src/modules/reader/contexts/state/ReaderStateChaptersContextProvider.tsx index a485971e82..54386b5829 100644 --- a/src/modules/reader/contexts/state/ReaderStateChaptersContextProvider.tsx +++ b/src/modules/reader/contexts/state/ReaderStateChaptersContextProvider.tsx @@ -7,14 +7,15 @@ */ import { ReactNode, useMemo, useState } from 'react'; -import { ReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; +import { + READER_STATE_CHAPTERS_DEFAULTS, + ReaderStateChaptersContext, +} from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts'; export const ReaderStateChaptersContextProvider = ({ children }: { children: ReactNode }) => { - const [state, setState] = useState>({ - mangaChapters: [], - chapters: [], - }); + const [state, setState] = + useState>(READER_STATE_CHAPTERS_DEFAULTS); const value = useMemo( () => ({ diff --git a/src/modules/reader/hooks/useReaderInfiniteScrollUpdateChapter.ts b/src/modules/reader/hooks/useReaderInfiniteScrollUpdateChapter.ts new file mode 100644 index 0000000000..76413b9dff --- /dev/null +++ b/src/modules/reader/hooks/useReaderInfiniteScrollUpdateChapter.ts @@ -0,0 +1,85 @@ +/* + * 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 { useLayoutEffect } from 'react'; +import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts'; +import { ReadingMode } from '@/modules/reader/types/Reader.types.ts'; +import { isContinuousReadingMode } from '@/modules/reader/utils/ReaderSettings.utils.tsx'; + +export const useReaderInfiniteScrollUpdateChapter = ( + offset: 'previous' | 'next', + readingMode: ReadingMode, + image: HTMLElement | null, + isCurrentChapter: boolean, + openChapter: ReturnType, +) => { + useLayoutEffect(() => { + if (!image || !isContinuousReadingMode(readingMode)) { + return () => {}; + } + + /** + * first observe needs to be ignored because + * - the initial observe needs to be ignored, would otherwise trigger unwanted chapter loads/changes + * - when going to an already loaded previous/next chapter, the last/first page would trigger setting the next/previous chapter again + * example 1: + * chapter 1 -> chapter 2 (would otherwise set chapter 1 immediately again) + * + * example 2: + * chapter 1 -> chapter 2 -> chapter 1 (would otherwise set chapter 2 immediately again) + * + */ + let wasObservedAtLeastOnce = false; + let isInitialObserve = true; + let wasNextChapterOpened = false; + const intersectionObserver = new IntersectionObserver( + (entries) => { + if (isInitialObserve) { + isInitialObserve = false; + return; + } + + if (wasNextChapterOpened) { + return; + } + + if (!isCurrentChapter) { + return; + } + + const { top, bottom } = entries[entries.length - 1].target.getBoundingClientRect(); + + const shouldSetNextChapter = (() => { + if (!wasObservedAtLeastOnce) { + wasObservedAtLeastOnce = true; + return false; + } + + switch (offset) { + case 'previous': + return bottom >= window.innerHeight; + case 'next': + return top <= 0; + default: + throw new Error( + `useReaderInfiniteScrollUpdateChapter#shouldSetNextChapter: unexpected "offset" (${offset})`, + ); + } + })(); + if (shouldSetNextChapter) { + wasNextChapterOpened = true; + openChapter(offset); + } + }, + { threshold: [0.1] }, + ); + intersectionObserver.observe(image); + + return () => intersectionObserver.unobserve(image); + }, [image, readingMode, isCurrentChapter, offset]); +}; diff --git a/src/modules/reader/hooks/useReaderResetStates.ts b/src/modules/reader/hooks/useReaderResetStates.ts index 46fa6c32ba..a8be49bb85 100644 --- a/src/modules/reader/hooks/useReaderResetStates.ts +++ b/src/modules/reader/hooks/useReaderResetStates.ts @@ -18,6 +18,7 @@ import { import { DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG } from '@/modules/reader/services/ReaderSettingsMetadata.ts'; import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts'; import { TReaderOverlayContext } from '@/modules/reader/types/ReaderOverlay.types.ts'; +import { READER_STATE_CHAPTERS_DEFAULTS } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; export const useReaderResetStates = ( setManga: TReaderStateMangaContext['setManga'], @@ -36,10 +37,7 @@ export const useReaderResetStates = ( useEffect( () => () => { setManga(undefined); - setReaderStateChapters({ - mangaChapters: [], - chapters: [], - }); + setReaderStateChapters(READER_STATE_CHAPTERS_DEFAULTS); setCurrentPageIndex(0); setPageToScrollToIndex(null); diff --git a/src/modules/reader/hooks/useReaderSetChaptersState.ts b/src/modules/reader/hooks/useReaderSetChaptersState.ts index fe9862d358..ca91715472 100644 --- a/src/modules/reader/hooks/useReaderSetChaptersState.ts +++ b/src/modules/reader/hooks/useReaderSetChaptersState.ts @@ -12,6 +12,7 @@ import { DirectionOffset } from '@/Base.types.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { GetChaptersReaderQuery } from '@/lib/graphql/generated/graphql.ts'; import { IReaderSettings, ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts'; +import { READER_STATE_CHAPTERS_DEFAULTS } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; export const useReaderSetChaptersState = ( chaptersResponse: ReturnType>, @@ -27,30 +28,42 @@ export const useReaderSetChaptersState = ( : undefined; const newInitialChapter = initialChapter ?? newCurrentChapter; - setReaderStateChapters({ - mangaChapters: newMangaChapters ?? [], - chapters: - newInitialChapter && newMangaChapters - ? Chapters.removeDuplicates(newInitialChapter, newMangaChapters) - : [], - initialChapter: newInitialChapter, - currentChapter: newCurrentChapter, - nextChapter: - newMangaChapters && - newCurrentChapter && - Chapters.getNextChapter(newCurrentChapter, newMangaChapters, { - offset: DirectionOffset.NEXT, - skipDupe: shouldSkipDupChapters, - skipDupeChapter: newInitialChapter, - }), - previousChapter: - newMangaChapters && - newCurrentChapter && - Chapters.getNextChapter(newCurrentChapter, newMangaChapters, { - offset: DirectionOffset.PREVIOUS, - skipDupe: shouldSkipDupChapters, - skipDupeChapter: newInitialChapter, - }), + const nextChapter = + newMangaChapters && + newCurrentChapter && + Chapters.getNextChapter(newCurrentChapter, newMangaChapters, { + offset: DirectionOffset.NEXT, + skipDupe: shouldSkipDupChapters, + skipDupeChapter: newInitialChapter, + }); + const previousChapter = + newMangaChapters && + newCurrentChapter && + Chapters.getNextChapter(newCurrentChapter, newMangaChapters, { + offset: DirectionOffset.PREVIOUS, + skipDupe: shouldSkipDupChapters, + skipDupeChapter: newInitialChapter, + }); + + setReaderStateChapters((prevState) => { + const hasInitialChapterChanged = + newInitialChapter != null && newInitialChapter.id !== prevState.initialChapter?.id; + + return { + ...prevState, + mangaChapters: newMangaChapters ?? [], + chapters: + newInitialChapter && newMangaChapters + ? Chapters.removeDuplicates(newInitialChapter, newMangaChapters) + : [], + initialChapter: newInitialChapter, + currentChapter: newCurrentChapter, + nextChapter, + previousChapter, + visibleChapters: hasInitialChapterChanged + ? READER_STATE_CHAPTERS_DEFAULTS.visibleChapters + : prevState.visibleChapters, + }; }); }, [chaptersResponse.data?.chapters.nodes, chapterSourceOrder, shouldSkipDupChapters]); }; diff --git a/src/modules/reader/hooks/useReaderSetPagesState.ts b/src/modules/reader/hooks/useReaderSetPagesState.ts index 6e5dce7a40..7da21119bf 100644 --- a/src/modules/reader/hooks/useReaderSetPagesState.ts +++ b/src/modules/reader/hooks/useReaderSetPagesState.ts @@ -9,19 +9,16 @@ import { useEffect } from 'react'; import { getInitialReaderPageIndex } from '@/modules/reader/utils/Reader.utils.ts'; import { createPagesData } from '@/modules/reader/utils/ReaderPager.utils.tsx'; -import { - ReaderResumeMode, - ReaderStateChapters, - ReaderTransitionPageMode, -} from '@/modules/reader/types/Reader.types.ts'; +import { ReaderResumeMode, ReaderTransitionPageMode } from '@/modules/reader/types/Reader.types.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts'; -import { READER_STATE_PAGES_DEFAULTS } from '@/modules/reader/constants/ReaderContext.constants.ts'; +import { TChapterReader } from '@/modules/chapter/Chapter.types.ts'; export const useReaderSetPagesState = ( + isCurrentChapter: boolean, pagesResponse: ReturnType[1], resumeMode: ReaderResumeMode, - currentChapter: ReaderStateChapters['currentChapter'], + lastPageRead: TChapterReader['lastPageRead'] | undefined, setArePagesFetched: (fetched: boolean) => void, setTotalPages: ReaderStatePages['setTotalPages'], setPages: ReaderStatePages['setPages'], @@ -38,7 +35,7 @@ export const useReaderSetPagesState = ( const initialReaderPageIndex = getInitialReaderPageIndex( resumeMode, - currentChapter?.lastPageRead ?? 0, + lastPageRead ?? 0, newPages.length - 1, ); @@ -52,15 +49,15 @@ export const useReaderSetPagesState = ( setCurrentPageIndex(initialReaderPageIndex); setPageToScrollToIndex(initialReaderPageIndex); } else { - setArePagesFetched(false); - setCurrentPageIndex(READER_STATE_PAGES_DEFAULTS.currentPageIndex); - setPageToScrollToIndex(READER_STATE_PAGES_DEFAULTS.pageToScrollToIndex); - setTotalPages(READER_STATE_PAGES_DEFAULTS.totalPages); - setPages(READER_STATE_PAGES_DEFAULTS.pages); - setPageUrls(READER_STATE_PAGES_DEFAULTS.pageUrls); - setPageLoadStates(READER_STATE_PAGES_DEFAULTS.pageLoadStates); + // setArePagesFetched(false); + // setCurrentPageIndex(READER_STATE_PAGES_DEFAULTS.currentPageIndex); + // setPageToScrollToIndex(READER_STATE_PAGES_DEFAULTS.pageToScrollToIndex); + // setTotalPages(READER_STATE_PAGES_DEFAULTS.totalPages); + // setPages(READER_STATE_PAGES_DEFAULTS.pages); + // setPageUrls(READER_STATE_PAGES_DEFAULTS.pageUrls); + // setPageLoadStates(READER_STATE_PAGES_DEFAULTS.pageLoadStates); } setTransitionPageMode(ReaderTransitionPageMode.NONE); - }, [pagesResponse.data?.fetchChapterPages?.pages]); + }, [pagesResponse.data?.fetchChapterPages?.pages, isCurrentChapter]); }; diff --git a/src/modules/reader/screens/Reader.tsx b/src/modules/reader/screens/Reader.tsx index 68ab4f6fe8..c0bd85b6bb 100644 --- a/src/modules/reader/screens/Reader.tsx +++ b/src/modules/reader/screens/Reader.tsx @@ -7,8 +7,8 @@ */ import Box from '@mui/material/Box'; -import { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; -import { useLocation, useParams } from 'react-router-dom'; +import { memo, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useDefaultReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts'; import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; @@ -33,7 +33,6 @@ import { ReaderHotkeys } from '@/modules/reader/components/ReaderHotkeys.tsx'; import { IReaderSettings, IReaderSettingsWithDefaultFlag, - ReaderResumeMode, ReaderStateChapters, TReaderAutoScrollContext, TReaderStateMangaContext, @@ -49,7 +48,6 @@ import { useReaderAutoScrollContext } from '@/modules/reader/contexts/ReaderAuto import { TReaderTapZoneContext } from '@/modules/reader/types/TapZoneLayout.types.ts'; import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx'; import { useReaderResetStates } from '@/modules/reader/hooks/useReaderResetStates.ts'; -import { useReaderSetPagesState } from '@/modules/reader/hooks/useReaderSetPagesState.ts'; import { useReaderSetSettingsState } from '@/modules/reader/hooks/useReaderSetSettingsState.ts'; import { useReaderShowSettingPreviewOnChange } from '@/modules/reader/hooks/useReaderShowSettingPreviewOnChange.ts'; import { useReaderSetChaptersState } from '@/modules/reader/hooks/useReaderSetChaptersState.ts'; @@ -72,7 +70,6 @@ const BaseReader = ({ setSettings, initialChapter, currentChapter, - mangaChapters, setReaderStateChapters, firstPageUrl, totalPages, @@ -94,7 +91,7 @@ const BaseReader = ({ 'shouldSkipDupChapters' | 'backgroundColor' | 'shouldShowReadingModePreview' | 'shouldShowTapZoneLayoutPreview' > & Pick & - Pick & + Pick & Pick< ReaderStatePages, | 'totalPages' @@ -111,9 +108,6 @@ const BaseReader = ({ cancelAutoScroll: TReaderAutoScrollContext['cancel']; }) => { const { t } = useTranslation(); - const { resumeMode } = useLocation<{ - resumeMode: ReaderResumeMode; - }>().state ?? { resumeMode: ReaderResumeMode.START }; const scrollElementRef = useRef(null); @@ -128,20 +122,6 @@ const BaseReader = ({ const mangaResponse = requestManager.useGetManga(GET_MANGA_READER, mangaId); const chaptersResponse = requestManager.useGetMangaChapters(GET_CHAPTERS_READER, mangaId); - const [arePagesFetched, setArePagesFetched] = useState(false); - const [fetchPages, pagesResponse] = requestManager.useGetChapterPagesFetch(-1); - - const doFetchPages = useCallback(() => { - if (!currentChapter) { - return; - } - - setArePagesFetched(false); - - fetchPages({ variables: { input: { chapterId: currentChapter.id } } }).catch( - defaultPromiseErrorHandler('Reader::fetchPages'), - ); - }, [fetchPages, currentChapter?.id]); const { metadata: defaultSettingsMetadata, @@ -149,21 +129,13 @@ const BaseReader = ({ request: defaultSettingsResponse, } = useDefaultReaderSettings(); - const doesChapterExist = - !chaptersResponse.loading && - !chaptersResponse.error && - chapterSourceOrder >= 0 && - chapterSourceOrder <= mangaChapters.length; - const isLoading = currentChapter === undefined || !areSettingsSet || mangaResponse.loading || chaptersResponse.loading || - defaultSettingsResponse.loading || - pagesResponse.loading || - (doesChapterExist && !arePagesFetched && !chaptersResponse.error && !pagesResponse.error); - const error = mangaResponse.error ?? chaptersResponse.error ?? defaultSettingsResponse.error ?? pagesResponse.error; + defaultSettingsResponse.loading; + const error = mangaResponse.error ?? chaptersResponse.error ?? defaultSettingsResponse.error; useLayoutEffect(() => { if (!manga || !currentChapter) { @@ -174,11 +146,6 @@ const BaseReader = ({ setTitle(`${manga.title}: ${currentChapter.name}`); }, [t, mangaId, chapterSourceOrder, manga, currentChapter]); - useEffect(() => { - doFetchPages(); - return () => setArePagesFetched(false); - }, [currentChapter?.id]); - useEffect(() => { setManga(mangaResponse.data?.manga); }, [mangaResponse.data?.manga]); @@ -197,19 +164,6 @@ const BaseReader = ({ setSettings, cancelAutoScroll, ); - useReaderSetPagesState( - pagesResponse, - resumeMode, - currentChapter, - setArePagesFetched, - setTotalPages, - setPages, - setPageUrls, - setPageLoadStates, - setCurrentPageIndex, - setPageToScrollToIndex, - setTransitionPageMode, - ); useReaderSetSettingsState( mangaResponse, defaultSettingsResponse, @@ -283,10 +237,6 @@ const BaseReader = ({ if (chaptersResponse.error) { chaptersResponse.refetch().catch(defaultPromiseErrorHandler('Reader::refetchChapters')); } - - if (pagesResponse.error) { - doFetchPages(); - } }} /> ); @@ -311,10 +261,6 @@ const BaseReader = ({ return ; } - if (currentChapter && !currentChapter?.pageCount) { - return ; - } - if (!manga || !currentChapter) { return null; } @@ -397,7 +343,6 @@ export const Reader = withPropsFrom( 'setSettings', 'initialChapter', 'currentChapter', - 'mangaChapters', 'setReaderStateChapters', 'firstPageUrl', 'totalPages', diff --git a/src/modules/reader/services/ReaderControls.ts b/src/modules/reader/services/ReaderControls.ts index 76a1b6d5d5..72c1499ca7 100644 --- a/src/modules/reader/services/ReaderControls.ts +++ b/src/modules/reader/services/ReaderControls.ts @@ -44,7 +44,7 @@ import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZone import { TapZoneRegionType, TReaderTapZoneContext } from '@/modules/reader/types/TapZoneLayout.types.ts'; import { ReaderTapZoneService } from '@/modules/reader/services/ReaderTapZoneService.ts'; import { isContinuousReadingMode } from '@/modules/reader/utils/ReaderSettings.utils.tsx'; -import { getReaderChapterFromCache } from '@/modules/reader/utils/Reader.utils.ts'; +import { getReaderChapterFromCache, updateReaderStateVisibleChapters } from '@/modules/reader/utils/Reader.utils.ts'; import { Chapters } from '@/modules/chapter/services/Chapters.ts'; import { DirectionOffset, TranslationKey } from '@/Base.types.ts'; import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; @@ -170,41 +170,53 @@ export class ReaderControls { const { t } = useTranslation(); const { readingMode, shouldInformAboutMissingChapter, shouldInformAboutScanlatorChange } = ReaderService.useSettings(); - const { currentChapter, previousChapter, nextChapter } = useReaderStateChaptersContext(); + const { currentChapter, previousChapter, nextChapter, setReaderStateChapters } = + useReaderStateChaptersContext(); const openPreviousChapter = ReaderService.useNavigateToChapter(previousChapter, ReaderResumeMode.END); const openNextChapter = ReaderService.useNavigateToChapter(nextChapter, ReaderResumeMode.START); return useCallback( (offset) => { - switch (offset) { - case 'previous': - ReaderControls.checkNextChapterConsistency( - t, - offset, - currentChapter, - previousChapter, - shouldInformAboutMissingChapter, - shouldInformAboutScanlatorChange, - ) - .then(openPreviousChapter) - .catch(defaultPromiseErrorHandler('ReaderControls::checkNextChapterConsistency: previous')); - break; - case 'next': - ReaderControls.checkNextChapterConsistency( + const isPreviousChapter = offset === 'previous'; + + const doesPreviousChapterExist = isPreviousChapter && !!previousChapter; + const doesNextChapterExist = !isPreviousChapter && !!nextChapter; + + const canOpenNextChapter = doesPreviousChapterExist || doesNextChapterExist; + if (!canOpenNextChapter) { + return; + } + + const openChapter = async () => { + const chapterToOpen = isPreviousChapter ? previousChapter! : nextChapter!; + + try { + await ReaderControls.checkNextChapterConsistency( t, offset, currentChapter, - nextChapter, + chapterToOpen, shouldInformAboutMissingChapter, shouldInformAboutScanlatorChange, - ) - .then(openNextChapter) - .catch(defaultPromiseErrorHandler('ReaderControls::checkNextChapterConsistency: next')); - break; - default: - throw new Error(`Unexpected "offset" (${offset})`); - } + ); + + setReaderStateChapters((prevState) => + updateReaderStateVisibleChapters(isPreviousChapter, prevState, chapterToOpen.sourceOrder), + ); + + if (isPreviousChapter) { + openPreviousChapter(); + return; + } + + openNextChapter(); + } catch (error) { + defaultPromiseErrorHandler('ReaderControls#useOpenChapter#openChapter: '); + } + }; + + openChapter().catch(defaultPromiseErrorHandler('ReaderControls#useOpenChapter')); }, [ t, diff --git a/src/modules/reader/services/ReaderService.ts b/src/modules/reader/services/ReaderService.ts index ef0525e9a3..b37173724c 100644 --- a/src/modules/reader/services/ReaderService.ts +++ b/src/modules/reader/services/ReaderService.ts @@ -77,17 +77,18 @@ export class ReaderService { static useNavigateToChapter(chapter?: TChapterReader, resumeMode?: ReaderResumeMode): () => void { const navigate = useNavigate(); - return useCallback( - () => - chapter && - navigate(Chapters.getReaderUrl(chapter), { - replace: true, - state: { - resumeMode, - }, - }), - [chapter], - ); + return useCallback(() => { + if (!chapter) { + return; + } + + navigate(Chapters.getReaderUrl(chapter), { + replace: true, + state: { + resumeMode, + }, + }); + }, [chapter]); } static downloadAhead( diff --git a/src/modules/reader/types/Reader.types.ts b/src/modules/reader/types/Reader.types.ts index df5237bb94..9a26a6a745 100644 --- a/src/modules/reader/types/Reader.types.ts +++ b/src/modules/reader/types/Reader.types.ts @@ -192,6 +192,15 @@ export interface ReaderStateChapters { currentChapter?: TChapterReader | null; nextChapter?: TChapterReader; previousChapter?: TChapterReader; + /** + * Based from the initial chapter index + */ + visibleChapters: { + leading: number; + trailing: number; + lastLeadingChapterSourceOrder: number; + lastTrailingChapterSourceOrder: number; + }; setReaderStateChapters: React.Dispatch>>; } @@ -246,9 +255,9 @@ export interface ReaderPagerProps | 'pageLoadStates' | 'retryFailedPagesKeyPrefix' > { - imageRefs: MutableRefObject<(HTMLElement | null)[]>; onLoad?: (pagesIndex: number, url: string, isPrimary?: boolean) => void; onError?: (pageIndex: number, url: string) => void; + imageRefs: MutableRefObject<(HTMLElement | null)[]>; } export enum PageInViewportType { diff --git a/src/modules/reader/utils/Reader.utils.ts b/src/modules/reader/utils/Reader.utils.ts index 311a46d621..c97003a60c 100644 --- a/src/modules/reader/utils/Reader.utils.ts +++ b/src/modules/reader/utils/Reader.utils.ts @@ -6,7 +6,12 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { ReaderPageSpreadState, ReaderResumeMode, ReadingMode } from '@/modules/reader/types/Reader.types.ts'; +import { + ReaderPageSpreadState, + ReaderResumeMode, + ReaderStateChapters, + ReadingMode, +} from '@/modules/reader/types/Reader.types.ts'; import { UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts'; import { TChapterReader } from '@/modules/chapter/Chapter.types.ts'; import { ChapterIdInfo, Chapters } from '@/modules/chapter/services/Chapters.ts'; @@ -164,3 +169,29 @@ export const createUpdateReaderPageLoadState = return statePageLoadStates.toSpliced(index, 1, { url: pageLoadState.url, loaded: true }); }); }; + +export const updateReaderStateVisibleChapters = ( + isPreviousChapter: boolean, + state: Omit, + chapterToOpenSourceOrder: TChapterReader['sourceOrder'], +): Omit => { + const { leading, trailing, lastLeadingChapterSourceOrder, lastTrailingChapterSourceOrder } = state.visibleChapters; + + const isNewLeadingChapter = isPreviousChapter && chapterToOpenSourceOrder < lastLeadingChapterSourceOrder; + const isNewTrailingChapter = !isPreviousChapter && chapterToOpenSourceOrder > lastTrailingChapterSourceOrder; + + return { + ...state, + visibleChapters: { + ...state.visibleChapters, + leading: leading + Number(isNewLeadingChapter), + trailing: trailing + Number(isNewTrailingChapter), + lastLeadingChapterSourceOrder: isNewLeadingChapter + ? chapterToOpenSourceOrder + : lastLeadingChapterSourceOrder, + lastTrailingChapterSourceOrder: isNewTrailingChapter + ? chapterToOpenSourceOrder + : lastTrailingChapterSourceOrder, + }, + }; +};