Streamline route location state

This commit is contained in:
schroda
2026-05-27 23:59:58 +02:00
parent 29bb60c28b
commit e973872539
13 changed files with 103 additions and 44 deletions

View File

@@ -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>

View File

@@ -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,
};
});
}
/**

View 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;
}

View File

@@ -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,
}),
},
},
)
}

View File

@@ -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>

View File

@@ -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',
}),
});
}

View File

@@ -383,7 +383,7 @@ export enum ReaderResumeMode {
LAST_READ,
}
export interface ReaderOpenChapterLocationState {
export interface RouteStateReader {
resumeMode?: ReaderResumeMode;
updateInitialChapter?: boolean;
}

View File

@@ -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) => {

View File

@@ -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,

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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 }),
},
},
);
}