Streamline route location state
This commit is contained in:
@@ -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<string, unknown>;
|
||||
};
|
||||
|
||||
type TAppRoutes = Record<string, AppRouteInfo & { childRoutes?: TAppRoutes }>;
|
||||
@@ -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',
|
||||
|
||||
@@ -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<IProps> = (props: IProps) => {
|
||||
<CardActionArea
|
||||
component={Link}
|
||||
to={AppRoutes.sources.childRoutes.browse.path(id)}
|
||||
state={{ contentType: SourceContentType.POPULAR, clearCache: true }}
|
||||
state={AppRoutes.sources.childRoutes.browse.state({
|
||||
contentType: SourceContentType.POPULAR,
|
||||
clearCache: true,
|
||||
})}
|
||||
>
|
||||
<ListCardContent>
|
||||
<ListCardAvatar
|
||||
@@ -103,7 +106,10 @@ export const SourceCard: React.FC<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`}
|
||||
</Button>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
12
src/features/global-search/SearchAll.types.ts
Normal file
12
src/features/global-search/SearchAll.types.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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<string, SourceLoadingState>;
|
||||
@@ -250,7 +251,7 @@ export const SearchAll = ({
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
const navigate = useNavigate();
|
||||
const { state } = useLocation<{ title?: string; shouldShowOnlyPinnedSources?: boolean }>();
|
||||
const { state } = useLocation<RouteStateSourcesSearchAll>();
|
||||
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,
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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' }}
|
||||
>
|
||||
<IconButton 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' }}
|
||||
>
|
||||
<ListItemIcon>
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -383,7 +383,7 @@ export enum ReaderResumeMode {
|
||||
LAST_READ,
|
||||
}
|
||||
|
||||
export interface ReaderOpenChapterLocationState {
|
||||
export interface RouteStateReader {
|
||||
resumeMode?: ReaderResumeMode;
|
||||
updateInitialChapter?: boolean;
|
||||
}
|
||||
|
||||
@@ -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<typeof requestManager.useGetMangaChapters<GetChaptersReaderQuery>>,
|
||||
@@ -36,7 +33,7 @@ export const useReaderSetChaptersState = (
|
||||
chapterListOptions: ChapterListFilterOptions,
|
||||
) => {
|
||||
const navigate = useNavigate();
|
||||
const locationState = useLocation<ReaderOpenChapterLocationState>().state;
|
||||
const locationState = useLocation<RouteStateReader>().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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, PageInViewportType> = {
|
||||
[ReadingMode.SINGLE_PAGE]: PageInViewportType.X,
|
||||
@@ -109,9 +110,7 @@ const BaseReaderViewer = ({
|
||||
isStaticNav: state.isStaticNav,
|
||||
}));
|
||||
const safeAreaInset = useReaderSettingsStore('safeAreaInset');
|
||||
const { resumeMode = ReaderResumeMode.START } = useLocation<ReaderOpenChapterLocationState>().state ?? {
|
||||
resumeMode: ReaderResumeMode.START,
|
||||
};
|
||||
const { resumeMode = ReaderResumeMode.START } = useLocation<RouteStateReader>().state ?? STABLE_EMPTY_OBJECT;
|
||||
|
||||
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
||||
const mergedRef = useMergedRef(ref, scrollElementRef);
|
||||
|
||||
@@ -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<ExtensionType, 'repo'> };
|
||||
export type SourceMetaInfo = { meta: SourceMetaFieldsFragment[] };
|
||||
export type SourceConfigurableInfo = Pick<SourceType, 'isConfigurable'>;
|
||||
export type SourceIconInfo = Pick<SourceType, 'iconUrl'>;
|
||||
|
||||
export enum SourceContentType {
|
||||
POPULAR,
|
||||
LATEST,
|
||||
SEARCH,
|
||||
}
|
||||
|
||||
export interface RouteStateSourceBrowse {
|
||||
contentType?: SourceContentType;
|
||||
clearCache?: boolean;
|
||||
mode?: MangaCardMode;
|
||||
}
|
||||
|
||||
@@ -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<RouteStateSourceBrowse>().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 }),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user