Save chapter list options on server
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user