From 0733c930d433572a10da9e3b4f99ea8d90e6a8ca Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 20 Apr 2025 21:40:28 +0200 Subject: [PATCH] Save chapter list options on server --- src/lib/graphql/fragments/MangaFragments.ts | 5 + src/lib/graphql/generated/graphql.ts | 6 +- src/modules/chapter/Chapter.constants.ts | 1 - src/modules/chapter/Chapter.types.ts | 7 -- .../chapter/components/ChapterList.tsx | 16 +++- .../chapter/components/ChapterOptions.tsx | 43 +++------ .../components/ChaptersToolbarMenu.tsx | 15 +-- .../chapter/hooks/useChapterOptions.tsx | 38 -------- .../chapter/utils/ChapterList.util.tsx | 33 +++++-- src/modules/manga/Manga.types.ts | 6 +- src/modules/manga/services/MangaMetadata.ts | 91 +++++++++++++++++++ src/modules/metadata/Metadata.constants.ts | 13 +++ src/modules/metadata/Metadata.types.ts | 2 + 13 files changed, 172 insertions(+), 104 deletions(-) delete mode 100644 src/modules/chapter/hooks/useChapterOptions.tsx create mode 100644 src/modules/manga/services/MangaMetadata.ts diff --git a/src/lib/graphql/fragments/MangaFragments.ts b/src/lib/graphql/fragments/MangaFragments.ts index e326f99a..002de1cf 100644 --- a/src/lib/graphql/fragments/MangaFragments.ts +++ b/src/lib/graphql/fragments/MangaFragments.ts @@ -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 diff --git a/src/lib/graphql/generated/graphql.ts b/src/lib/graphql/generated/graphql.ts index 0f1e659e..750a72b6 100644 --- a/src/lib/graphql/generated/graphql.ts +++ b/src/lib/graphql/generated/graphql.ts @@ -2875,7 +2875,7 @@ export type MangaReaderFieldsFragment = { __typename?: 'MangaType', genre: Array export type MangaLibraryFieldsFragment = { __typename?: 'MangaType', genre: Array, 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, 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, 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, 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, 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, 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, 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']; diff --git a/src/modules/chapter/Chapter.constants.ts b/src/modules/chapter/Chapter.constants.ts index 6b045b22..8d78547f 100644 --- a/src/modules/chapter/Chapter.constants.ts +++ b/src/modules/chapter/Chapter.constants.ts @@ -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, diff --git a/src/modules/chapter/Chapter.types.ts b/src/modules/chapter/Chapter.types.ts index 111bdb7b..8088b279 100644 --- a/src/modules/chapter/Chapter.types.ts +++ b/src/modules/chapter/Chapter.types.ts @@ -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; downloaded: NullAndUndefined; bookmarked: NullAndUndefined; @@ -21,10 +20,4 @@ export interface ChapterListOptions { showChapterNumber: boolean; } -export type ChapterOptionsReducerAction = - | { type: 'filter'; filterType: string; filterValue: NullAndUndefined } - | { type: 'sortBy'; sortBy: ChapterSortMode } - | { type: 'sortReverse' } - | { type: 'showChapterNumber' }; - export type TChapterReader = ChapterReaderFieldsFragment; diff --git a/src/modules/chapter/components/ChapterList.tsx b/src/modules/chapter/components/ChapterList.tsx index 98cb9414..7e160094 100644 --- a/src/modules/chapter/components/ChapterList.tsx +++ b/src/modules/chapter/components/ChapterList.tsx @@ -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 = ({ )} - + void; options: ChapterListOptions; - optionsDispatch: React.Dispatch; + updateOption: ReturnType; } 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 = ({ open, onClose, options, optionsDispatch }) => { +export const ChapterOptions: React.FC = ({ open, onClose, options, updateOption }) => { const { t } = useTranslation(); return ( @@ -47,35 +48,17 @@ export const ChapterOptions: React.FC = ({ open, onClose, options, optio - optionsDispatch({ - type: 'filter', - filterType: 'unread', - filterValue: c, - }) - } + onChange={(c) => updateOption('unread', c)} /> - optionsDispatch({ - type: 'filter', - filterType: 'downloaded', - filterValue: c, - }) - } + onChange={(c) => updateOption('downloaded', c)} /> - optionsDispatch({ - type: 'filter', - filterType: 'bookmarked', - filterValue: c, - }) - } + onChange={(c) => updateOption('bookmarked', c)} /> ); @@ -89,11 +72,11 @@ export const ChapterOptions: React.FC = ({ 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 = ({ open, onClose, options, optio if (key === 'display') { return ( optionsDispatch({ type: 'showChapterNumber' })} + onChange={() => updateOption('showChapterNumber', !options.showChapterNumber)} value={options.showChapterNumber} > diff --git a/src/modules/chapter/components/ChaptersToolbarMenu.tsx b/src/modules/chapter/components/ChaptersToolbarMenu.tsx index a077681b..a31f4708 100644 --- a/src/modules/chapter/components/ChaptersToolbarMenu.tsx +++ b/src/modules/chapter/components/ChaptersToolbarMenu.tsx @@ -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; + updateOption: ReturnType; } -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) => { - setOpen(false)} - options={options} - optionsDispatch={optionsDispatch} - /> + setOpen(false)} options={options} updateOption={updateOption} /> ); }; diff --git a/src/modules/chapter/hooks/useChapterOptions.tsx b/src/modules/chapter/hooks/useChapterOptions.tsx deleted file mode 100644 index 66ffcf6a..00000000 --- a/src/modules/chapter/hooks/useChapterOptions.tsx +++ /dev/null @@ -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( - chapterOptionsReducer, - `${mangaId}filterOptions`, - DEFAULT_CHAPTER_OPTIONS, - ); diff --git a/src/modules/chapter/utils/ChapterList.util.tsx b/src/modules/chapter/utils/ChapterList.util.tsx index 3bfde45c..74cd1403 100644 --- a/src/modules/chapter/utils/ChapterList.util.tsx +++ b/src/modules/chapter/utils/ChapterList.util.tsx @@ -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, { isRead: isChapterRead }: ChapterReadInfo) { switch (unread) { @@ -83,14 +88,12 @@ export function filterAndSortChapters( 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); diff --git a/src/modules/manga/Manga.types.ts b/src/modules/manga/Manga.types.ts index 391ea34c..f8def3fa 100644 --- a/src/modules/manga/Manga.types.ts +++ b/src/modules/manga/Manga.types.ts @@ -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 & mangaBadges: JSX.Element; }; -export type MangaMetadataKeys = keyof IReaderSettings; +export type MangaMetadata = ChapterListOptions; + +export type MangaMetadataKeys = keyof MangaMetadata; export type MangaAction = | 'download' diff --git a/src/modules/manga/services/MangaMetadata.ts b/src/modules/manga/services/MangaMetadata.ts new file mode 100644 index 00000000..6a441359 --- /dev/null +++ b/src/modules/manga/services/MangaMetadata.ts @@ -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, +): Metadata => ({ + ...metadata, +}); + +const convertGqlMetadataToAppMetadata = ( + metadata: Partial>, +): 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 => + requestUpdateMangaMetadata(manga, [ + [metadataKey, convertAppMetadataToGqlMetadata({ [metadataKey]: value })[metadataKey]], + ]); + +export const createUpdateMangaMetadata = + ( + manga: MangaIdInfo & GqlMetaHolder, + handleError: (error: any) => void = defaultPromiseErrorHandler('createUpdateMangaMetadata'), + ): ((...args: OmitFirst>>) => Promise) => + (metadataKey, value) => + updateMangaMetadata(manga, metadataKey, value).catch(handleError); diff --git a/src/modules/metadata/Metadata.constants.ts b/src/modules/metadata/Metadata.constants.ts index eef0bf98..81e67ec9 100644 --- a/src/modules/metadata/Metadata.constants.ts +++ b/src/modules/metadata/Metadata.constants.ts @@ -80,6 +80,11 @@ const APP_METADATA_OBJECT: Record = { 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', ]; /** diff --git a/src/modules/metadata/Metadata.types.ts b/src/modules/metadata/Metadata.types.ts index ee079d34..3b39eb8f 100644 --- a/src/modules/metadata/Metadata.types.ts +++ b/src/modules/metadata/Metadata.types.ts @@ -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;