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` export const MANGA_SCREEN_FIELDS = gql`
${MANGA_LIBRARY_FIELDS} ${MANGA_LIBRARY_FIELDS}
${MANGA_META_FIELDS}
fragment MANGA_SCREEN_FIELDS on MangaType { fragment MANGA_SCREEN_FIELDS on MangaType {
...MANGA_LIBRARY_FIELDS ...MANGA_LIBRARY_FIELDS
@@ -148,6 +149,10 @@ export const MANGA_SCREEN_FIELDS = gql`
status status
realUrl realUrl
meta {
...MANGA_META_FIELDS
}
sourceId sourceId
source { source {
id 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 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 } }; 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<{ export type GetMangaToMigrateToFetchMutationVariables = Exact<{
id: Scalars['Int']['input']; 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<{ export type GetMangaReaderQueryVariables = Exact<{
id: Scalars['Int']['input']; 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 FALLBACK_CHAPTER = { id: -1, name: '', realUrl: '', isBookmarked: false };
export const DEFAULT_CHAPTER_OPTIONS: ChapterListOptions = { export const DEFAULT_CHAPTER_OPTIONS: ChapterListOptions = {
active: false,
unread: undefined, unread: undefined,
downloaded: undefined, downloaded: undefined,
bookmarked: 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 type ChapterSortMode = 'fetchedAt' | 'source' | 'chapterNumber' | 'uploadedAt';
export interface ChapterListOptions { export interface ChapterListOptions {
active: boolean;
unread: NullAndUndefined<boolean>; unread: NullAndUndefined<boolean>;
downloaded: NullAndUndefined<boolean>; downloaded: NullAndUndefined<boolean>;
bookmarked: NullAndUndefined<boolean>; bookmarked: NullAndUndefined<boolean>;
@@ -21,10 +20,4 @@ export interface ChapterListOptions {
showChapterNumber: boolean; showChapterNumber: boolean;
} }
export type ChapterOptionsReducerAction =
| { type: 'filter'; filterType: string; filterValue: NullAndUndefined<boolean> }
| { type: 'sortBy'; sortBy: ChapterSortMode }
| { type: 'sortReverse' }
| { type: 'showChapterNumber' };
export type TChapterReader = ChapterReaderFieldsFragment; 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 { requestManager } from '@/lib/requests/RequestManager.ts';
import { ChapterCard } from '@/modules/chapter/components/cards/ChapterCard.tsx'; import { ChapterCard } from '@/modules/chapter/components/cards/ChapterCard.tsx';
import { ResumeFab } from '@/modules/manga/components/ResumeFAB.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 { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
import { ChaptersToolbarMenu } from '@/modules/chapter/components/ChaptersToolbarMenu.tsx'; import { ChaptersToolbarMenu } from '@/modules/chapter/components/ChaptersToolbarMenu.tsx';
import { SelectionFAB } from '@/modules/collection/components/SelectionFAB.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 { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx'; import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
import { shouldForwardProp } from '@/modules/core/utils/ShouldForwardProp.ts'; import { shouldForwardProp } from '@/modules/core/utils/ShouldForwardProp.ts';
import { useChapterOptions } from '@/modules/chapter/hooks/useChapterOptions.tsx';
import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { makeToast } from '@/modules/core/utils/Toast.ts';
type ChapterListHeaderProps = { type ChapterListHeaderProps = {
scrollbarWidth: number; scrollbarWidth: number;
@@ -118,7 +122,10 @@ export const ChapterList = ({
const scrollbarWidth = MediaQuery.useGetScrollbarSize('width'); 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 { const {
data: chaptersData, data: chaptersData,
loading: isLoading, loading: isLoading,
@@ -137,7 +144,6 @@ export const ChapterList = ({
useSelectableCollection(chapters.length, { itemIds: chapterIds, currentKey: 'default' }); useSelectableCollection(chapters.length, { itemIds: chapterIds, currentKey: 'default' });
const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), [chapters, options]); const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), [chapters, options]);
const areAllChaptersRead = Mangas.isFullyRead(manga); const areAllChaptersRead = Mangas.isFullyRead(manga);
const areAllChaptersDownloaded = Mangas.isFullyDownloaded(manga); const areAllChaptersDownloaded = Mangas.isFullyDownloaded(manga);
@@ -225,7 +231,7 @@ export const ChapterList = ({
)} )}
</PopupState> </PopupState>
<ChaptersToolbarMenu options={options} optionsDispatch={dispatch} /> <ChaptersToolbarMenu options={options} updateOption={updateOption} />
<SelectableCollectionSelectAll <SelectableCollectionSelectAll
areAllItemsSelected={areAllItemsSelected} areAllItemsSelected={areAllItemsSelected}
areNoItemsSelected={areNoItemsSelected} 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 { OptionsTabs } from '@/modules/core/components/OptionsTabs.tsx';
import { CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY } from '@/modules/chapter/Chapter.constants.ts'; import { CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY } from '@/modules/chapter/Chapter.constants.ts';
import { TranslationKey } from '@/Base.types.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 { interface IProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
options: ChapterListOptions; options: ChapterListOptions;
optionsDispatch: React.Dispatch<ChapterOptionsReducerAction>; updateOption: ReturnType<typeof updateChapterListOptions>;
} }
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = { const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
@@ -30,7 +31,7 @@ const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
display: 'global.label.display', 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(); const { t } = useTranslation();
return ( return (
@@ -47,35 +48,17 @@ export const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optio
<ThreeStateCheckboxInput <ThreeStateCheckboxInput
label={t('global.filter.label.unread')} label={t('global.filter.label.unread')}
checked={options.unread} checked={options.unread}
onChange={(c) => onChange={(c) => updateOption('unread', c)}
optionsDispatch({
type: 'filter',
filterType: 'unread',
filterValue: c,
})
}
/> />
<ThreeStateCheckboxInput <ThreeStateCheckboxInput
label={t('global.filter.label.downloaded')} label={t('global.filter.label.downloaded')}
checked={options.downloaded} checked={options.downloaded}
onChange={(c) => onChange={(c) => updateOption('downloaded', c)}
optionsDispatch({
type: 'filter',
filterType: 'downloaded',
filterValue: c,
})
}
/> />
<ThreeStateCheckboxInput <ThreeStateCheckboxInput
label={t('global.filter.label.bookmarked')} label={t('global.filter.label.bookmarked')}
checked={options.bookmarked} checked={options.bookmarked}
onChange={(c) => onChange={(c) => updateOption('bookmarked', c)}
optionsDispatch({
type: 'filter',
filterType: 'bookmarked',
filterValue: c,
})
}
/> />
</> </>
); );
@@ -89,11 +72,11 @@ export const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optio
sortDescending={options.reverse} sortDescending={options.reverse}
onClick={() => onClick={() =>
mode !== options.sortBy mode !== options.sortBy
? optionsDispatch({ ? updateOption(
type: 'sortBy', 'sortBy',
sortBy: mode as keyof typeof CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY, mode as keyof typeof CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY,
}) )
: optionsDispatch({ type: 'sortReverse' }) : updateOption('reverse', !options.reverse)
} }
/> />
)); ));
@@ -101,7 +84,7 @@ export const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, optio
if (key === 'display') { if (key === 'display') {
return ( return (
<RadioGroup <RadioGroup
onChange={() => optionsDispatch({ type: 'showChapterNumber' })} onChange={() => updateOption('showChapterNumber', !options.showChapterNumber)}
value={options.showChapterNumber} value={options.showChapterNumber}
> >
<RadioInput label={t('chapter.option.display.label.source_title')} value={false} /> <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 { useTranslation } from 'react-i18next';
import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx'; import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx';
import { ChapterOptions } from '@/modules/chapter/components/ChapterOptions.tsx'; import { ChapterOptions } from '@/modules/chapter/components/ChapterOptions.tsx';
import { isFilterActive } from '@/modules/chapter/utils/ChapterList.util.tsx'; import { isFilterActive, updateChapterListOptions } from '@/modules/chapter/utils/ChapterList.util.tsx';
import { ChapterListOptions, ChapterOptionsReducerAction } from '@/modules/chapter/Chapter.types.ts'; import { ChapterListOptions } from '@/modules/chapter/Chapter.types.ts';
interface IProps { interface IProps {
options: ChapterListOptions; 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 { t } = useTranslation();
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
@@ -33,12 +33,7 @@ export const ChaptersToolbarMenu = ({ options, optionsDispatch }: IProps) => {
<FilterList color={isFiltered ? 'warning' : undefined} /> <FilterList color={isFiltered ? 'warning' : undefined} />
</IconButton> </IconButton>
</CustomTooltip> </CustomTooltip>
<ChapterOptions <ChapterOptions open={open} onClose={() => setOpen(false)} options={options} updateOption={updateOption} />
open={open}
onClose={() => setOpen(false)}
options={options}
optionsDispatch={optionsDispatch}
/>
</> </>
); );
}; };

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/. * 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 { ChapterBookmarkInfo, ChapterDownloadInfo, ChapterReadInfo } from '@/modules/chapter/services/Chapters.ts';
import { ChapterType } from '@/lib/graphql/generated/graphql.ts'; import { ChapterType } from '@/lib/graphql/generated/graphql.ts';
import { NullAndUndefined } from '@/Base.types.ts'; import { NullAndUndefined } from '@/Base.types.ts';
import { ChapterListOptions } from '@/modules/chapter/Chapter.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) { export function unreadFilter(unread: NullAndUndefined<boolean>, { isRead: isChapterRead }: ChapterReadInfo) {
switch (unread) { switch (unread) {
@@ -83,14 +88,12 @@ export function filterAndSortChapters<Chapters extends TChapterFilter>(
chapters: Chapters[], chapters: Chapters[],
options: ChapterListOptions, options: ChapterListOptions,
): Chapters[] { ): Chapters[] {
const filtered = options.active const filtered = chapters.filter(
? chapters.filter( (chp) =>
(chp) => unreadFilter(options.unread, chp) &&
unreadFilter(options.unread, chp) && downloadFilter(options.downloaded, chp) &&
downloadFilter(options.downloaded, chp) && bookmarkedFilter(options.bookmarked, chp),
bookmarkedFilter(options.bookmarked, chp), );
)
: [...chapters];
return sortChapters(filtered, options); return sortChapters(filtered, options);
} }
@@ -99,3 +102,17 @@ export const isFilterActive = (options: ChapterListOptions) => {
const { unread, downloaded, bookmarked } = options; const { unread, downloaded, bookmarked } = options;
return unread != null || downloaded != null || bookmarked != null; 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, TrackRecordType,
} from '@/lib/graphql/generated/graphql.ts'; } from '@/lib/graphql/generated/graphql.ts';
import { SingleModeProps } from '@/modules/manga/components/MangaActionMenuItems.tsx'; 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 { 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'; export type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select' | 'duplicate';
@@ -67,7 +67,9 @@ export type SpecificMangaCardProps = Omit<MangaCardProps, 'manga'> &
mangaBadges: JSX.Element; mangaBadges: JSX.Element;
}; };
export type MangaMetadataKeys = keyof IReaderSettings; export type MangaMetadata = ChapterListOptions;
export type MangaMetadataKeys = keyof MangaMetadata;
export type MangaAction = export type MangaAction =
| 'download' | '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, shouldInformAboutScanlatorChange: undefined,
hideHistory: undefined, hideHistory: undefined,
scrollAmount: undefined, scrollAmount: undefined,
reverse: undefined,
bookmarked: undefined,
downloaded: undefined,
unread: undefined,
showChapterNumber: undefined,
}; };
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT); export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT);
@@ -154,6 +159,14 @@ export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
'customFilter', 'customFilter',
'shouldSkipDupChapters', 'shouldSkipDupChapters',
'hotkeys', '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 { SourceMetadataKeys } from '@/modules/source/Source.types.ts';
import { CategoryMetadataKeys } from '@/modules/category/Category.types.ts'; import { CategoryMetadataKeys } from '@/modules/category/Category.types.ts';
import { MetaType } from '@/lib/graphql/generated/graphql.ts'; import { MetaType } from '@/lib/graphql/generated/graphql.ts';
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
export interface IMetadataMigration { export interface IMetadataMigration {
appKeyPrefix?: { oldPrefix: string; newPrefix: string }; appKeyPrefix?: { oldPrefix: string; newPrefix: string };
@@ -47,6 +48,7 @@ export type AppMetadataKeys =
| keyof MetadataAppliedMigration | keyof MetadataAppliedMigration
| MetadataServerSettingKeys | MetadataServerSettingKeys
| MangaMetadataKeys | MangaMetadataKeys
| keyof IReaderSettings
| SearchMetadataKeys | SearchMetadataKeys
| SourceMetadataKeys | SourceMetadataKeys
| CategoryMetadataKeys; | CategoryMetadataKeys;