Save chapter list options on server

This commit is contained in:
schroda
2025-04-20 21:40:28 +02:00
parent 0f3becf505
commit 0733c930d4
13 changed files with 172 additions and 104 deletions

View File

@@ -137,6 +137,7 @@ export const MANGA_LIBRARY_FIELDS = gql`
export const MANGA_SCREEN_FIELDS = gql`
${MANGA_LIBRARY_FIELDS}
${MANGA_META_FIELDS}
fragment MANGA_SCREEN_FIELDS on MangaType {
...MANGA_LIBRARY_FIELDS
@@ -148,6 +149,10 @@ export const MANGA_SCREEN_FIELDS = gql`
status
realUrl
meta {
...MANGA_META_FIELDS
}
sourceId
source {
id

View File

@@ -2875,7 +2875,7 @@ export type MangaReaderFieldsFragment = { __typename?: 'MangaType', genre: Array
export type MangaLibraryFieldsFragment = { __typename?: 'MangaType', genre: Array<string>, lastFetchedAt?: string | null, inLibraryAt: string, status: MangaStatus, artist?: string | null, author?: string | null, description?: string | null, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null };
export type MangaScreenFieldsFragment = { __typename?: 'MangaType', artist?: string | null, author?: string | null, description?: string | null, status: MangaStatus, realUrl?: string | null, sourceId: string, genre: Array<string>, lastFetchedAt?: string | null, inLibraryAt: string, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null };
export type MangaScreenFieldsFragment = { __typename?: 'MangaType', artist?: string | null, author?: string | null, description?: string | null, status: MangaStatus, realUrl?: string | null, sourceId: string, genre: Array<string>, lastFetchedAt?: string | null, inLibraryAt: string, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null };
export type MangaLibraryDuplicateScreenFieldsFragment = { __typename?: 'MangaType', description?: string | null, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number } };
@@ -3173,7 +3173,7 @@ export type GetMangaFetchMutationVariables = Exact<{
}>;
export type GetMangaFetchMutation = { __typename?: 'Mutation', fetchManga?: { __typename?: 'FetchMangaPayload', manga: { __typename?: 'MangaType', artist?: string | null, author?: string | null, description?: string | null, status: MangaStatus, realUrl?: string | null, sourceId: string, genre: Array<string>, lastFetchedAt?: string | null, inLibraryAt: string, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null } } | null };
export type GetMangaFetchMutation = { __typename?: 'Mutation', fetchManga?: { __typename?: 'FetchMangaPayload', manga: { __typename?: 'MangaType', artist?: string | null, author?: string | null, description?: string | null, status: MangaStatus, realUrl?: string | null, sourceId: string, genre: Array<string>, lastFetchedAt?: string | null, inLibraryAt: string, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null } } | null };
export type GetMangaToMigrateToFetchMutationVariables = Exact<{
id: Scalars['Int']['input'];
@@ -3517,7 +3517,7 @@ export type GetMangaScreenQueryVariables = Exact<{
}>;
export type GetMangaScreenQuery = { __typename?: 'Query', manga: { __typename?: 'MangaType', artist?: string | null, author?: string | null, description?: string | null, status: MangaStatus, realUrl?: string | null, sourceId: string, genre: Array<string>, lastFetchedAt?: string | null, inLibraryAt: string, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null } };
export type GetMangaScreenQuery = { __typename?: 'Query', manga: { __typename?: 'MangaType', artist?: string | null, author?: string | null, description?: string | null, status: MangaStatus, realUrl?: string | null, sourceId: string, genre: Array<string>, lastFetchedAt?: string | null, inLibraryAt: string, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }>, source?: { __typename?: 'SourceType', id: string, displayName: string } | null, trackRecords: { __typename?: 'TrackRecordNodeList', totalCount: number, nodes: Array<{ __typename?: 'TrackRecordType', id: number, trackerId: number }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, firstUnreadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, isRead: boolean, mangaId: number } | null, lastReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number, sourceOrder: number, lastReadAt: string } | null, latestFetchedChapter?: { __typename?: 'ChapterType', id: number, fetchedAt: string } | null, latestUploadedChapter?: { __typename?: 'ChapterType', id: number, uploadDate: string } | null } };
export type GetMangaReaderQueryVariables = Exact<{
id: Scalars['Int']['input'];

View File

@@ -12,7 +12,6 @@ import { ChapterListOptions, ChapterSortMode } from '@/modules/chapter/Chapter.t
export const FALLBACK_CHAPTER = { id: -1, name: '', realUrl: '', isBookmarked: false };
export const DEFAULT_CHAPTER_OPTIONS: ChapterListOptions = {
active: false,
unread: undefined,
downloaded: undefined,
bookmarked: undefined,

View File

@@ -12,7 +12,6 @@ import { ChapterReaderFieldsFragment } from '@/lib/graphql/generated/graphql.ts'
export type ChapterSortMode = 'fetchedAt' | 'source' | 'chapterNumber' | 'uploadedAt';
export interface ChapterListOptions {
active: boolean;
unread: NullAndUndefined<boolean>;
downloaded: NullAndUndefined<boolean>;
bookmarked: NullAndUndefined<boolean>;
@@ -21,10 +20,4 @@ export interface ChapterListOptions {
showChapterNumber: boolean;
}
export type ChapterOptionsReducerAction =
| { type: 'filter'; filterType: string; filterValue: NullAndUndefined<boolean> }
| { type: 'sortBy'; sortBy: ChapterSortMode }
| { type: 'sortReverse' }
| { type: 'showChapterNumber' };
export type TChapterReader = ChapterReaderFieldsFragment;

View File

@@ -22,7 +22,11 @@ import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { ChapterCard } from '@/modules/chapter/components/cards/ChapterCard.tsx';
import { ResumeFab } from '@/modules/manga/components/ResumeFAB.tsx';
import { filterAndSortChapters } from '@/modules/chapter/utils/ChapterList.util.tsx';
import {
filterAndSortChapters,
updateChapterListOptions,
useChapterListOptions,
} from '@/modules/chapter/utils/ChapterList.util.tsx';
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
import { ChaptersToolbarMenu } from '@/modules/chapter/components/ChaptersToolbarMenu.tsx';
import { SelectionFAB } from '@/modules/collection/components/SelectionFAB.tsx';
@@ -46,8 +50,8 @@ import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContex
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
import { shouldForwardProp } from '@/modules/core/utils/ShouldForwardProp.ts';
import { useChapterOptions } from '@/modules/chapter/hooks/useChapterOptions.tsx';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { makeToast } from '@/modules/core/utils/Toast.ts';
type ChapterListHeaderProps = {
scrollbarWidth: number;
@@ -118,7 +122,10 @@ export const ChapterList = ({
const scrollbarWidth = MediaQuery.useGetScrollbarSize('width');
const [options, dispatch] = useChapterOptions(manga.id);
const options = useChapterListOptions(manga);
const updateOption = updateChapterListOptions(manga, (e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
);
const {
data: chaptersData,
loading: isLoading,
@@ -137,7 +144,6 @@ export const ChapterList = ({
useSelectableCollection(chapters.length, { itemIds: chapterIds, currentKey: 'default' });
const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), [chapters, options]);
const areAllChaptersRead = Mangas.isFullyRead(manga);
const areAllChaptersDownloaded = Mangas.isFullyDownloaded(manga);
@@ -225,7 +231,7 @@ export const ChapterList = ({
)}
</PopupState>
<ChaptersToolbarMenu options={options} optionsDispatch={dispatch} />
<ChaptersToolbarMenu options={options} updateOption={updateOption} />
<SelectableCollectionSelectAll
areAllItemsSelected={areAllItemsSelected}
areNoItemsSelected={areNoItemsSelected}

View File

@@ -15,13 +15,14 @@ import { ThreeStateCheckboxInput } from '@/modules/core/components/inputs/ThreeS
import { OptionsTabs } from '@/modules/core/components/OptionsTabs.tsx';
import { CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY } from '@/modules/chapter/Chapter.constants.ts';
import { TranslationKey } from '@/Base.types.ts';
import { ChapterListOptions, ChapterOptionsReducerAction } from '@/modules/chapter/Chapter.types.ts';
import { ChapterListOptions } from '@/modules/chapter/Chapter.types.ts';
import { updateChapterListOptions } from '@/modules/chapter/utils/ChapterList.util.tsx';
interface IProps {
open: boolean;
onClose: () => void;
options: ChapterListOptions;
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>;
updateOption: ReturnType<typeof updateChapterListOptions>;
}
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
@@ -30,7 +31,7 @@ const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
display: 'global.label.display',
};
export const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optionsDispatch }) => {
export const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, updateOption }) => {
const { t } = useTranslation();
return (
@@ -47,35 +48,17 @@ export const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optio
<ThreeStateCheckboxInput
label={t('global.filter.label.unread')}
checked={options.unread}
onChange={(c) =>
optionsDispatch({
type: 'filter',
filterType: 'unread',
filterValue: c,
})
}
onChange={(c) => updateOption('unread', c)}
/>
<ThreeStateCheckboxInput
label={t('global.filter.label.downloaded')}
checked={options.downloaded}
onChange={(c) =>
optionsDispatch({
type: 'filter',
filterType: 'downloaded',
filterValue: c,
})
}
onChange={(c) => updateOption('downloaded', c)}
/>
<ThreeStateCheckboxInput
label={t('global.filter.label.bookmarked')}
checked={options.bookmarked}
onChange={(c) =>
optionsDispatch({
type: 'filter',
filterType: 'bookmarked',
filterValue: c,
})
}
onChange={(c) => updateOption('bookmarked', c)}
/>
</>
);
@@ -89,11 +72,11 @@ export const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optio
sortDescending={options.reverse}
onClick={() =>
mode !== options.sortBy
? optionsDispatch({
type: 'sortBy',
sortBy: mode as keyof typeof CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY,
})
: optionsDispatch({ type: 'sortReverse' })
? updateOption(
'sortBy',
mode as keyof typeof CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY,
)
: updateOption('reverse', !options.reverse)
}
/>
));
@@ -101,7 +84,7 @@ export const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optio
if (key === 'display') {
return (
<RadioGroup
onChange={() => optionsDispatch({ type: 'showChapterNumber' })}
onChange={() => updateOption('showChapterNumber', !options.showChapterNumber)}
value={options.showChapterNumber}
>
<RadioInput label={t('chapter.option.display.label.source_title')} value={false} />

View File

@@ -12,15 +12,15 @@ import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx';
import { ChapterOptions } from '@/modules/chapter/components/ChapterOptions.tsx';
import { isFilterActive } from '@/modules/chapter/utils/ChapterList.util.tsx';
import { ChapterListOptions, ChapterOptionsReducerAction } from '@/modules/chapter/Chapter.types.ts';
import { isFilterActive, updateChapterListOptions } from '@/modules/chapter/utils/ChapterList.util.tsx';
import { ChapterListOptions } from '@/modules/chapter/Chapter.types.ts';
interface IProps {
options: ChapterListOptions;
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>;
updateOption: ReturnType<typeof updateChapterListOptions>;
}
export const ChaptersToolbarMenu = ({ options, optionsDispatch }: IProps) => {
export const ChaptersToolbarMenu = ({ options, updateOption }: IProps) => {
const { t } = useTranslation();
const [open, setOpen] = React.useState(false);
@@ -33,12 +33,7 @@ export const ChaptersToolbarMenu = ({ options, optionsDispatch }: IProps) => {
<FilterList color={isFiltered ? 'warning' : undefined} />
</IconButton>
</CustomTooltip>
<ChapterOptions
open={open}
onClose={() => setOpen(false)}
options={options}
optionsDispatch={optionsDispatch}
/>
<ChapterOptions open={open} onClose={() => setOpen(false)} options={options} updateOption={updateOption} />
</>
);
};

View File

@@ -1,38 +0,0 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { t } from 'i18next';
import { useReducerLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
import { DEFAULT_CHAPTER_OPTIONS } from '@/modules/chapter/Chapter.constants.ts';
import { ChapterListOptions, ChapterOptionsReducerAction } from '@/modules/chapter/Chapter.types.ts';
function chapterOptionsReducer(state: ChapterListOptions, actions: ChapterOptionsReducerAction): ChapterListOptions {
switch (actions.type) {
case 'filter':
return {
...state,
active: state.unread !== false && state.downloaded !== false && state.bookmarked !== false,
[actions.filterType!]: actions.filterValue,
};
case 'sortBy':
return { ...state, sortBy: actions.sortBy };
case 'sortReverse':
return { ...state, reverse: !state.reverse };
case 'showChapterNumber':
return { ...state, showChapterNumber: !state.showChapterNumber };
default:
throw Error(t('global.error.label.invalid_action'));
}
}
export const useChapterOptions = (mangaId: number) =>
useReducerLocalStorage<ChapterListOptions, ChapterOptionsReducerAction>(
chapterOptionsReducer,
`${mangaId}filterOptions`,
DEFAULT_CHAPTER_OPTIONS,
);

View File

@@ -6,10 +6,15 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useMemo } from 'react';
import { ChapterBookmarkInfo, ChapterDownloadInfo, ChapterReadInfo } from '@/modules/chapter/services/Chapters.ts';
import { ChapterType } from '@/lib/graphql/generated/graphql.ts';
import { NullAndUndefined } from '@/Base.types.ts';
import { ChapterListOptions } from '@/modules/chapter/Chapter.types.ts';
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
import { GqlMetaHolder } from '@/modules/metadata/Metadata.types.ts';
import { createUpdateMangaMetadata, useGetMangaMetadata } from '@/modules/manga/services/MangaMetadata.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
export function unreadFilter(unread: NullAndUndefined<boolean>, { isRead: isChapterRead }: ChapterReadInfo) {
switch (unread) {
@@ -83,14 +88,12 @@ export function filterAndSortChapters<Chapters extends TChapterFilter>(
chapters: Chapters[],
options: ChapterListOptions,
): Chapters[] {
const filtered = options.active
? chapters.filter(
(chp) =>
unreadFilter(options.unread, chp) &&
downloadFilter(options.downloaded, chp) &&
bookmarkedFilter(options.bookmarked, chp),
)
: [...chapters];
const filtered = chapters.filter(
(chp) =>
unreadFilter(options.unread, chp) &&
downloadFilter(options.downloaded, chp) &&
bookmarkedFilter(options.bookmarked, chp),
);
return sortChapters(filtered, options);
}
@@ -99,3 +102,17 @@ export const isFilterActive = (options: ChapterListOptions) => {
const { unread, downloaded, bookmarked } = options;
return unread != null || downloaded != null || bookmarked != null;
};
export const useChapterListOptions = (manga: MangaIdInfo & GqlMetaHolder): ChapterListOptions => {
const { unread, downloaded, bookmarked, reverse, sortBy, showChapterNumber } = useGetMangaMetadata(manga);
return useMemo(
() => ({ unread, downloaded, bookmarked, reverse, sortBy, showChapterNumber }),
[unread, downloaded, bookmarked, reverse, sortBy, showChapterNumber],
);
};
export const updateChapterListOptions = (
manga: MangaIdInfo & GqlMetaHolder,
handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateMangaMetadata'),
) => createUpdateMangaMetadata(manga, handleError);

View File

@@ -20,8 +20,8 @@ import {
TrackRecordType,
} from '@/lib/graphql/generated/graphql.ts';
import { SingleModeProps } from '@/modules/manga/components/MangaActionMenuItems.tsx';
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
import { GridLayout } from '@/modules/core/Core.types.ts';
import { ChapterListOptions } from '@/modules/chapter/Chapter.types.ts';
export type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select' | 'duplicate';
@@ -67,7 +67,9 @@ export type SpecificMangaCardProps = Omit<MangaCardProps, 'manga'> &
mangaBadges: JSX.Element;
};
export type MangaMetadataKeys = keyof IReaderSettings;
export type MangaMetadata = ChapterListOptions;
export type MangaMetadataKeys = keyof MangaMetadata;
export type MangaAction =
| 'download'

View File

@@ -0,0 +1,91 @@
/*
* 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 { useEffect, useMemo } from 'react';
import { DEFAULT_CHAPTER_OPTIONS } from '@/modules/chapter/Chapter.constants.ts';
import { getMetadataFrom } from '@/modules/metadata/services/MetadataReader.ts';
import { MangaIdInfo, MangaMetadata, MangaMetadataKeys } from '@/modules/manga/Manga.types.ts';
import {
AllowedMetadataValueTypes,
AppMetadataKeys,
GqlMetaHolder,
Metadata,
MetadataHolder,
} from '@/modules/metadata/Metadata.types.ts';
import { convertFromGqlMeta } from '@/modules/metadata/services/MetadataConverter.ts';
import { requestUpdateMangaMetadata } from '@/modules/metadata/services/MetadataUpdater.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
const DEFAULT_MANGA_METADATA: MangaMetadata = {
...DEFAULT_CHAPTER_OPTIONS,
};
const convertAppMetadataToGqlMetadata = (
metadata: Partial<MangaMetadata>,
): Metadata<string, AllowedMetadataValueTypes> => ({
...metadata,
});
const convertGqlMetadataToAppMetadata = (
metadata: Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>,
): MangaMetadata => ({
...(metadata as unknown as MangaMetadata),
});
const getMangaMetadataWithDefaultValueFallback = (
meta: MangaIdInfo & MetadataHolder,
defaultMetadata: MangaMetadata = DEFAULT_MANGA_METADATA,
useEffectFn?: typeof useEffect,
): MangaMetadata =>
convertGqlMetadataToAppMetadata(
getMetadataFrom('manga', meta, convertAppMetadataToGqlMetadata(defaultMetadata), undefined, useEffectFn),
);
const getMetadata = (
metaHolder: MangaIdInfo & GqlMetaHolder,
defaultMetadata?: MangaMetadata,
useEffectFn?: typeof useEffect,
) =>
getMangaMetadataWithDefaultValueFallback(
{ ...metaHolder, meta: convertFromGqlMeta(metaHolder.meta) },
defaultMetadata,
useEffectFn,
);
export const getMangaMetadata = (
metaHolder: MangaIdInfo & GqlMetaHolder,
defaultMetadata?: MangaMetadata,
): MangaMetadata => getMetadata(metaHolder, defaultMetadata);
export const useGetMangaMetadata = (
metaHolder: MangaIdInfo & GqlMetaHolder,
defaultMetadata?: MangaMetadata,
): MangaMetadata => {
const metadata = getMetadata(metaHolder, defaultMetadata, useEffect);
return useMemo(() => metadata, [metaHolder, defaultMetadata]);
};
export const updateMangaMetadata = async <
MetadataKeys extends MangaMetadataKeys = MangaMetadataKeys,
MetadataKey extends MetadataKeys = MetadataKeys,
>(
manga: MangaIdInfo & GqlMetaHolder,
metadataKey: MetadataKey,
value: MangaMetadata[MetadataKey],
): Promise<void[]> =>
requestUpdateMangaMetadata(manga, [
[metadataKey, convertAppMetadataToGqlMetadata({ [metadataKey]: value })[metadataKey]],
]);
export const createUpdateMangaMetadata =
<Settings extends MangaMetadataKeys>(
manga: MangaIdInfo & GqlMetaHolder,
handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateMangaMetadata'),
): ((...args: OmitFirst<Parameters<typeof updateMangaMetadata<Settings>>>) => Promise<void | void[]>) =>
(metadataKey, value) =>
updateMangaMetadata(manga, metadataKey, value).catch(handleError);

View File

@@ -80,6 +80,11 @@ const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
shouldInformAboutScanlatorChange: undefined,
hideHistory: undefined,
scrollAmount: undefined,
reverse: undefined,
bookmarked: undefined,
downloaded: undefined,
unread: undefined,
showChapterNumber: undefined,
};
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT);
@@ -154,6 +159,14 @@ export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
'customFilter',
'shouldSkipDupChapters',
'hotkeys',
// manga
// chapter list options
'reverse',
'bookmarked',
'downloaded',
'unread',
'showChapterNumber',
];
/**

View File

@@ -11,6 +11,7 @@ import { MangaMetadataKeys } from '@/modules/manga/Manga.types.ts';
import { SourceMetadataKeys } from '@/modules/source/Source.types.ts';
import { CategoryMetadataKeys } from '@/modules/category/Category.types.ts';
import { MetaType } from '@/lib/graphql/generated/graphql.ts';
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
export interface IMetadataMigration {
appKeyPrefix?: { oldPrefix: string; newPrefix: string };
@@ -47,6 +48,7 @@ export type AppMetadataKeys =
| keyof MetadataAppliedMigration
| MetadataServerSettingKeys
| MangaMetadataKeys
| keyof IReaderSettings
| SearchMetadataKeys
| SourceMetadataKeys
| CategoryMetadataKeys;