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

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