diff --git a/src/base/AppRoute.constants.ts b/src/base/AppRoute.constants.ts index d1c05a15..2fdee0a7 100644 --- a/src/base/AppRoute.constants.ts +++ b/src/base/AppRoute.constants.ts @@ -12,11 +12,14 @@ import type { ChapterSourceOrderInfo } from '@/features/chapter/Chapter.types.ts import type { BrowseTab } from '@/features/browse/Browse.types.ts'; import { SearchParam } from '@/base/Base.types.ts'; import { UrlUtil } from '@/lib/UrlUtil.ts'; -import type { SourceIdInfo } from '@/features/source/Source.types.ts'; +import type { RouteStateSourceBrowse, SourceIdInfo } from '@/features/source/Source.types.ts'; +import type { RouteStateReader } from '@/features/reader/Reader.types.ts'; +import type { RouteStateSourcesSearchAll } from '@/features/global-search/SearchAll.types.ts'; type AppRouteInfo = { match: string; path?: string | ((...args: any[]) => string); + state?: (...args: any[]) => Record; }; type TAppRoutes = Record; @@ -133,6 +136,9 @@ export const AppRoutes = { match: ':sourceId', path: (sourceId: SourceIdInfo['id'], query?: string | null | undefined) => UrlUtil.addQueryParam(`/sources/${sourceId}`, query), + state: (state: RouteStateSourceBrowse) => ({ + ...state, + }), }, configure: { match: ':sourceId/configure', @@ -141,6 +147,7 @@ export const AppRoutes = { searchAll: { match: 'all/search', path: (query?: string | null | undefined) => UrlUtil.addQueryParam('/sources/all/search', query), + state: (state: RouteStateSourcesSearchAll) => ({ ...state }), }, }, }, @@ -206,11 +213,15 @@ export const AppRoutes = { match: 'source/:sourceId/manga/:mangaId/search', path: (sourceId: SourceIdInfo['id'], mangaId: MangaIdInfo['id'], query?: string | null | undefined) => UrlUtil.addQueryParam(`/migrate/source/${sourceId}/manga/${mangaId}/search`, query), + state: (state: RouteStateSourcesSearchAll) => ({ + ...state, + }), }, manualSearch: { match: 'manual-search/:mangaId', path: (mangaId: MangaIdInfo['id'], query?: string | null | undefined) => UrlUtil.addQueryParam(`/migrate/manual-search/${mangaId}`, query), + state: (state: RouteStateSourcesSearchAll) => ({ ...state }), }, }, }, @@ -222,6 +233,7 @@ export const AppRoutes = { match: '/manga/:mangaId/chapter/:chapterSourceOrder/*', path: (mangaId: MangaIdInfo['id'], chapterSourceOrder: ChapterSourceOrderInfo['sourceOrder']) => `/manga/${mangaId}/chapter/${chapterSourceOrder}`, + state: (state: RouteStateReader) => ({ ...state }), }, more: { match: '/more', diff --git a/src/features/browse/sources/components/SourceCard.tsx b/src/features/browse/sources/components/SourceCard.tsx index b64b6447..3a0bec22 100644 --- a/src/features/browse/sources/components/SourceCard.tsx +++ b/src/features/browse/sources/components/SourceCard.tsx @@ -18,7 +18,6 @@ import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined'; import IconButton from '@mui/material/IconButton'; import { useLingui } from '@lingui/react/macro'; import { requestManager } from '@/lib/requests/RequestManager.ts'; -import { SourceContentType } from '@/features/source/browse/screens/SourceMangas.tsx'; import type { GetSourcesListQuery } from '@/lib/graphql/generated/graphql.ts'; import { AppRoutes } from '@/base/AppRoute.constants.ts'; import { MUIUtil } from '@/lib/mui/MUI.util.ts'; @@ -30,6 +29,7 @@ import { createUpdateSourceMetadata, useGetSourceMetadata } from '@/features/sou import { makeToast } from '@/base/utils/Toast.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { languageCodeToName } from '@/base/utils/Languages.ts'; +import { SourceContentType } from '@/features/source/Source.types.ts'; interface IProps { source: GetSourcesListQuery['sources']['nodes'][number]; @@ -64,7 +64,10 @@ export const SourceCard: React.FC = (props: IProps) => { = (props: IProps) => { variant="outlined" component={Link} to={AppRoutes.sources.childRoutes.browse.path(id)} - state={{ contentType: SourceContentType.LATEST, clearCache: true }} + state={AppRoutes.sources.childRoutes.browse.state({ + contentType: SourceContentType.LATEST, + clearCache: true, + })} > {t`Latest`} diff --git a/src/features/chapter/services/Chapters.ts b/src/features/chapter/services/Chapters.ts index 3de194e7..43379646 100644 --- a/src/features/chapter/services/Chapters.ts +++ b/src/features/chapter/services/Chapters.ts @@ -18,7 +18,7 @@ import { DownloadState } from '@/lib/graphql/generated/graphql-base.types.ts'; import { CHAPTER_LIST_FIELDS } from '@/lib/graphql/chapter/ChapterFragments.ts'; import type { MangaIdInfo } from '@/features/manga/Manga.types.ts'; -import type { ReaderOpenChapterLocationState } from '@/features/reader/Reader.types.ts'; +import type { RouteStateReader } from '@/features/reader/Reader.types.ts'; import { ReaderResumeMode } from '@/features/reader/Reader.types.ts'; import { AppRoutes } from '@/base/AppRoute.constants.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; @@ -431,11 +431,11 @@ export class Chapters { static getReaderOpenChapterLocationState( chapter: ChapterReadInfo, updateInitialChapter?: boolean, - ): ReaderOpenChapterLocationState { - return { + ): RouteStateReader { + return AppRoutes.reader.state({ resumeMode: Chapters.getReaderResumeMode(chapter), updateInitialChapter, - }; + }); } /** diff --git a/src/features/global-search/SearchAll.types.ts b/src/features/global-search/SearchAll.types.ts new file mode 100644 index 00000000..da726b44 --- /dev/null +++ b/src/features/global-search/SearchAll.types.ts @@ -0,0 +1,12 @@ +/* + * 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/. + */ + +export interface RouteStateSourcesSearchAll { + title?: string; + shouldShowOnlyPinnedSources?: boolean; +} diff --git a/src/features/global-search/screens/SearchAll.tsx b/src/features/global-search/screens/SearchAll.tsx index aa46294b..3942e977 100644 --- a/src/features/global-search/screens/SearchAll.tsx +++ b/src/features/global-search/screens/SearchAll.tsx @@ -60,6 +60,7 @@ import { MigrationManager } from '@/features/migration/MigrationManager.ts'; import { assertIsDefined } from '@/base/Asserts.ts'; import { ReactRouter } from '@/lib/react-router/ReactRouter.ts'; import { SubpathUtil } from '@/lib/utils/SubpathUtil.ts'; +import type { RouteStateSourcesSearchAll } from '@/features/global-search/SearchAll.types.ts'; type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean; error: any }; type SourceToLoadingStateMap = Map; @@ -250,7 +251,7 @@ export const SearchAll = ({ }) => { const { t } = useLingui(); const navigate = useNavigate(); - const { state } = useLocation<{ title?: string; shouldShowOnlyPinnedSources?: boolean }>(); + const { state } = useLocation(); const { ref: filterHeaderRef, height: filterHeaderHeight } = useElementSize(); const isMigrateMode = SubpathUtil.getPathname().startsWith(AppRoutes.migrate.path); @@ -373,7 +374,12 @@ export const SearchAll = ({ }, { replace: true, - state: { ...state, shouldShowOnlyPinnedSources: true }, + state: { + ...state, + ...AppRoutes.sources.childRoutes.searchAll.state({ + shouldShowOnlyPinnedSources: true, + }), + }, }, ) } @@ -391,7 +397,12 @@ export const SearchAll = ({ }, { replace: true, - state: { ...state, shouldShowOnlyPinnedSources: false }, + state: { + ...state, + ...AppRoutes.sources.childRoutes.searchAll.state({ + shouldShowOnlyPinnedSources: false, + }), + }, }, ) } diff --git a/src/features/manga/components/MangaToolbarMenu.tsx b/src/features/manga/components/MangaToolbarMenu.tsx index 9113a17d..b5632a5a 100644 --- a/src/features/manga/components/MangaToolbarMenu.tsx +++ b/src/features/manga/components/MangaToolbarMenu.tsx @@ -94,7 +94,9 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => { manga.id, manga.title, )} - state={{ title: t`Migrate "${manga.title}"` }} + state={AppRoutes.migrate.childRoutes.singleMangaSearch.state({ + title: t`Migrate "${manga.title}"`, + })} style={{ textDecoration: 'none', color: 'inherit' }} > @@ -168,7 +170,9 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => { manga.id, manga.title, )} - state={{ title: t`Migrate "${manga.title}"` }} + state={AppRoutes.migrate.childRoutes.singleMangaSearch.state({ + title: t`Migrate "${manga.title}"`, + })} style={{ textDecoration: 'none', color: 'inherit' }} > diff --git a/src/features/migration/MigrationManager.ts b/src/features/migration/MigrationManager.ts index de51c4cc..2279ccdc 100644 --- a/src/features/migration/MigrationManager.ts +++ b/src/features/migration/MigrationManager.ts @@ -321,7 +321,9 @@ export class MigrationManager { ReactRouter.navigate( AppRoutes.migrate.childRoutes.singleMangaSearch.path(manga.sourceId, manga.id, manga.title), { - state: { title: t`Migrate "${manga.title}"` }, + state: AppRoutes.migrate.childRoutes.singleMangaSearch.state({ + title: t`Migrate "${manga.title}"`, + }), }, ); @@ -670,7 +672,10 @@ export class MigrationManager { static openManualSearch(mangaId: MangaIdInfo['id'], title: string): void { ReactRouter.navigate(AppRoutes.migrate.childRoutes.manualSearch.path(mangaId, title), { - state: { title: t`Manual migration search for "${title}"` }, + state: AppRoutes.migrate.children.manualSearch.state({ + title: t`Manual migration search for "${title}"`, + mode: 'migrate.select.bulk', + }), }); } diff --git a/src/features/reader/Reader.types.ts b/src/features/reader/Reader.types.ts index 36443097..db1f4b5e 100644 --- a/src/features/reader/Reader.types.ts +++ b/src/features/reader/Reader.types.ts @@ -383,7 +383,7 @@ export enum ReaderResumeMode { LAST_READ, } -export interface ReaderOpenChapterLocationState { +export interface RouteStateReader { resumeMode?: ReaderResumeMode; updateInitialChapter?: boolean; } diff --git a/src/features/reader/hooks/useReaderSetChaptersState.ts b/src/features/reader/hooks/useReaderSetChaptersState.ts index a1e585c6..0eb9de84 100644 --- a/src/features/reader/hooks/useReaderSetChaptersState.ts +++ b/src/features/reader/hooks/useReaderSetChaptersState.ts @@ -11,11 +11,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { Chapters } from '@/features/chapter/services/Chapters.ts'; import type { requestManager } from '@/lib/requests/RequestManager.ts'; import type { GetChaptersReaderQuery } from '@/lib/graphql/generated/graphql.ts'; -import type { - IReaderSettings, - ReaderOpenChapterLocationState, - ReaderStateChapters, -} from '@/features/reader/Reader.types.ts'; +import type { IReaderSettings, RouteStateReader, ReaderStateChapters } from '@/features/reader/Reader.types.ts'; import { filterChapters } from '@/features/chapter/utils/ChapterList.util.tsx'; import type { ChapterListFilterOptions } from '@/features/chapter/Chapter.types.ts'; import { getReaderChapterFromCache } from '@/features/reader/Reader.utils.ts'; @@ -24,6 +20,7 @@ import { getReaderChaptersStore } from '@/features/reader/stores/ReaderStore.ts' import { STABLE_EMPTY_OBJECT } from '@/base/Base.constants.ts'; import { READER_DEFAULT_CHAPTERS_STATE } from '@/features/reader/stores/ReaderChaptersStore.ts'; +import { AppRoutes } from '@/base/AppRoute.constants.ts'; export const useReaderSetChaptersState = ( chaptersResponse: ReturnType>, @@ -36,7 +33,7 @@ export const useReaderSetChaptersState = ( chapterListOptions: ChapterListFilterOptions, ) => { const navigate = useNavigate(); - const locationState = useLocation().state; + const locationState = useLocation().state; const { updateInitialChapter } = locationState ?? STABLE_EMPTY_OBJECT; const finalInitialChapter = updateInitialChapter ? undefined : initialChapter; @@ -77,7 +74,10 @@ export const useReaderSetChaptersState = ( const hasInitialChapterChanged = newInitialChapter != null && newInitialChapter.id !== finalInitialChapter?.id; if (hasInitialChapterChanged) { - navigate('', { replace: true, state: { ...locationState, updateInitialChapter: undefined } }); + navigate('', { + replace: true, + state: { ...locationState, ...AppRoutes.reader.state({ updateInitialChapter: undefined }) }, + }); } getReaderChaptersStore().setReaderStateChapters((prevState) => { diff --git a/src/features/reader/services/ReaderService.ts b/src/features/reader/services/ReaderService.ts index b84dbd3a..e5efb7b6 100644 --- a/src/features/reader/services/ReaderService.ts +++ b/src/features/reader/services/ReaderService.ts @@ -13,7 +13,7 @@ import { useTheme } from '@mui/material/styles'; import { t } from '@lingui/core/macro'; import type { ChapterIdInfo, TChapterReader } from '@/features/chapter/Chapter.types.ts'; import { Chapters } from '@/features/chapter/services/Chapters.ts'; -import type { IReaderSettings, ReaderOpenChapterLocationState } from '@/features/reader/Reader.types.ts'; +import type { IReaderSettings, RouteStateReader } from '@/features/reader/Reader.types.ts'; import { ReaderExitMode, ReaderOverlayMode, ReadingDirection, ReadingMode } from '@/features/reader/Reader.types.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { MANGA_META_FIELDS } from '@/lib/graphql/manga/MangaFragments.ts'; @@ -82,7 +82,7 @@ export class ReaderService { return ReaderService.chapterUpdateQueues.get(id)!; } - static navigateToChapter(chapter: TChapterReader, state?: ReaderOpenChapterLocationState): void { + static navigateToChapter(chapter: TChapterReader, state?: RouteStateReader): void { ReactRouter.navigate(Chapters.getReaderUrl(chapter), { replace: true, state, diff --git a/src/features/reader/viewer/ReaderViewer.tsx b/src/features/reader/viewer/ReaderViewer.tsx index a7fc6bd2..e4fb0a63 100644 --- a/src/features/reader/viewer/ReaderViewer.tsx +++ b/src/features/reader/viewer/ReaderViewer.tsx @@ -12,7 +12,7 @@ import Stack from '@mui/material/Stack'; import { useTheme } from '@mui/material/styles'; import { useLocation } from 'react-router-dom'; import { useMergedRef } from '@mantine/hooks'; -import type { ReaderOpenChapterLocationState } from '@/features/reader/Reader.types.ts'; +import type { RouteStateReader } from '@/features/reader/Reader.types.ts'; import { PageInViewportType, ReaderResumeMode, ReadingDirection, ReadingMode } from '@/features/reader/Reader.types.ts'; import { MediaQuery } from '@/base/utils/MediaQuery.tsx'; import { ReaderControls } from '@/features/reader/services/ReaderControls.ts'; @@ -50,6 +50,7 @@ import { useReaderPagesStore, useReaderSettingsStore, } from '@/features/reader/stores/ReaderStore.ts'; +import { STABLE_EMPTY_OBJECT } from '@/base/Base.constants.ts'; const READING_MODE_TO_IN_VIEWPORT_TYPE: Record = { [ReadingMode.SINGLE_PAGE]: PageInViewportType.X, @@ -109,9 +110,7 @@ const BaseReaderViewer = ({ isStaticNav: state.isStaticNav, })); const safeAreaInset = useReaderSettingsStore('safeAreaInset'); - const { resumeMode = ReaderResumeMode.START } = useLocation().state ?? { - resumeMode: ReaderResumeMode.START, - }; + const { resumeMode = ReaderResumeMode.START } = useLocation().state ?? STABLE_EMPTY_OBJECT; const scrollElementRef = useRef(null); const mergedRef = useMergedRef(ref, scrollElementRef); diff --git a/src/features/source/Source.types.ts b/src/features/source/Source.types.ts index b81a5315..bac558cd 100644 --- a/src/features/source/Source.types.ts +++ b/src/features/source/Source.types.ts @@ -16,6 +16,7 @@ import type { SourcePreferenceChangeInput, SourceType, } from '@/lib/graphql/generated/graphql-base.types.ts'; +import type { MangaCardMode } from '@/features/manga/Manga.types.ts'; export interface IPos { type: 'selectState' | 'textState' | 'checkBoxState' | 'triState' | 'sortState'; @@ -74,3 +75,15 @@ export type SourceRepoInfo = { extension: Pick }; export type SourceMetaInfo = { meta: SourceMetaFieldsFragment[] }; export type SourceConfigurableInfo = Pick; export type SourceIconInfo = Pick; + +export enum SourceContentType { + POPULAR, + LATEST, + SEARCH, +} + +export interface RouteStateSourceBrowse { + contentType?: SourceContentType; + clearCache?: boolean; + mode?: MangaCardMode; +} diff --git a/src/features/source/browse/screens/SourceMangas.tsx b/src/features/source/browse/screens/SourceMangas.tsx index efd572b0..b288e01d 100644 --- a/src/features/source/browse/screens/SourceMangas.tsx +++ b/src/features/source/browse/screens/SourceMangas.tsx @@ -8,10 +8,10 @@ import type { MessageDescriptor } from '@lingui/core'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useParams, useNavigate, useLocation, useSearchParams } from 'react-router-dom'; +import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom'; import IconButton from '@mui/material/IconButton'; import SettingsIcon from '@mui/icons-material/Settings'; -import { useQueryParam, StringParam } from 'use-query-params'; +import { StringParam, useQueryParam } from 'use-query-params'; import Link from '@mui/material/Link'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -43,7 +43,8 @@ import { MANGA_GRID_SNAPSHOT_KEY } from '@/features/manga/components/MangaGrid.t import { createUpdateSourceMetadata, useGetSourceMetadata } from '@/features/source/services/SourceMetadata.ts'; import { makeToast } from '@/base/utils/Toast.ts'; import { GET_SOURCE_BROWSE } from '@/lib/graphql/source/SourceQuery.ts'; -import type { IPos, SourceIdInfo } from '@/features/source/Source.types.ts'; +import type { IPos, RouteStateSourceBrowse, SourceIdInfo } from '@/features/source/Source.types.ts'; +import { SourceContentType } from '@/features/source/Source.types.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { EmptyView } from '@/base/components/feedback/EmptyView.tsx'; import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx'; @@ -77,12 +78,6 @@ const StyledGridWrapper = styled(Box)(() => ({ position: 'relative', })); -export enum SourceContentType { - POPULAR, - LATEST, - SEARCH, -} - const SOURCE_CONTENT_TYPE_TO_ERROR_MSG_KEY: { [contentType in SourceContentType]: MessageDescriptor } = { [SourceContentType.POPULAR]: msg`No manga found`, [SourceContentType.LATEST]: msg`No manga found`, @@ -218,11 +213,10 @@ export function SourceMangas() { const navigate = useNavigate(); const location = useLocation(); const { key: locationKey, state: locationState } = location; - const { contentType: initialContentType = SourceContentType.POPULAR, clearCache = false } = - useLocation<{ - contentType: SourceContentType; - clearCache: boolean; - }>().state ?? STABLE_EMPTY_OBJECT; + const { + contentType: initialContentType = SourceContentType.POPULAR, + clearCache = false, + } = useLocation().state ?? STABLE_EMPTY_OBJECT; const { settings: { hideLibraryEntries }, @@ -363,7 +357,10 @@ export function SourceMangas() { pathname: '', }, { - state: { ...locationState, contentType: newContentType }, + state: { + ...locationState, + ...AppRoutes.sources.childRoutes.browse.state({ contentType: newContentType }), + }, }, ); }