@@ -18,6 +18,7 @@ export const DEFAULT_CHAPTER_OPTIONS: ChapterListOptions = {
|
||||
reverse: true,
|
||||
sortBy: 'source',
|
||||
showChapterNumber: false,
|
||||
excludedScanlators: [],
|
||||
};
|
||||
|
||||
export const CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY: Record<ChapterSortMode, TranslationKey> = {
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface ChapterListOptions {
|
||||
reverse: boolean;
|
||||
sortBy: ChapterSortMode;
|
||||
showChapterNumber: boolean;
|
||||
excludedScanlators: string[];
|
||||
}
|
||||
|
||||
export type TChapterReader = ChapterReaderFieldsFragment;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 { bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PeopleAltOutlinedIcon from '@mui/icons-material/PeopleAltOutlined';
|
||||
import { CheckboxListSetting } from '@/modules/core/components/settings/CheckboxListSetting.tsx';
|
||||
import { updateChapterListOptions } from '@/modules/chapter/utils/ChapterList.util.tsx';
|
||||
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
|
||||
|
||||
export const ChapterExcludeSanlatorsFilter = ({
|
||||
updateOption,
|
||||
scanlators,
|
||||
excludedScanlators,
|
||||
}: {
|
||||
updateOption: ReturnType<typeof updateChapterListOptions>;
|
||||
scanlators: string[];
|
||||
excludedScanlators: string[];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const popupState = usePopupState({ variant: 'dialog', popupId: 'chapter-list-options-scanlator-filter-dialog' });
|
||||
|
||||
return (
|
||||
<>
|
||||
<CheckboxInput
|
||||
{...bindTrigger(popupState)}
|
||||
label={t('global.label.scanlator')}
|
||||
icon={<PeopleAltOutlinedIcon />}
|
||||
checkedIcon={<PeopleAltOutlinedIcon color="warning" />}
|
||||
checked={!!excludedScanlators.length}
|
||||
/>
|
||||
<CheckboxListSetting
|
||||
open={popupState.isOpen}
|
||||
onClose={(selectedScanlators) => {
|
||||
if (selectedScanlators) {
|
||||
updateOption('excludedScanlators', selectedScanlators);
|
||||
}
|
||||
popupState.close();
|
||||
}}
|
||||
items={scanlators}
|
||||
getId={(scanlator) => scanlator}
|
||||
getLabel={(scanlator) => scanlator}
|
||||
isChecked={(scanlator) => excludedScanlators.includes(scanlator)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -216,6 +216,8 @@ export const ChapterList = ({
|
||||
options={options}
|
||||
updateOption={updateOption}
|
||||
unreadChapters={Chapters.getNonRead(chapters)}
|
||||
scanlators={Chapters.getScanlators(chapters)}
|
||||
excludeScanlators={options.excludedScanlators}
|
||||
/>
|
||||
)}
|
||||
{!!visibleChapterIds.length && (
|
||||
|
||||
@@ -17,12 +17,15 @@ import { CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY } from '@/modules/chapter/Chapt
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
import { ChapterListOptions } from '@/modules/chapter/Chapter.types.ts';
|
||||
import { updateChapterListOptions } from '@/modules/chapter/utils/ChapterList.util.tsx';
|
||||
import { ChapterExcludeSanlatorsFilter } from '@/modules/chapter/components/ChapterExcludeSanlatorsFilter.tsx';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
options: ChapterListOptions;
|
||||
updateOption: ReturnType<typeof updateChapterListOptions>;
|
||||
scanlators: string[];
|
||||
excludedScanlators: string[];
|
||||
}
|
||||
|
||||
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
|
||||
@@ -31,7 +34,14 @@ const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
|
||||
display: 'global.label.display',
|
||||
};
|
||||
|
||||
export const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, updateOption }) => {
|
||||
export const ChapterOptions: React.FC<IProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
options,
|
||||
updateOption,
|
||||
scanlators,
|
||||
excludedScanlators,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -60,6 +70,11 @@ export const ChapterOptions: React.FC<IProps> = ({ open, onClose, options, updat
|
||||
checked={options.bookmarked}
|
||||
onChange={(c) => updateOption('bookmarked', c)}
|
||||
/>
|
||||
<ChapterExcludeSanlatorsFilter
|
||||
scanlators={scanlators}
|
||||
excludedScanlators={excludedScanlators}
|
||||
updateOption={updateOption}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ interface IProps {
|
||||
options: ChapterListOptions;
|
||||
updateOption: ReturnType<typeof updateChapterListOptions>;
|
||||
unreadChapters: (ChapterIdInfo & ChapterDownloadInfo & ChapterBookmarkInfo)[];
|
||||
scanlators: string[];
|
||||
excludeScanlators: string[];
|
||||
}
|
||||
|
||||
export const ChaptersToolbarMenu = ({
|
||||
@@ -42,6 +44,8 @@ export const ChaptersToolbarMenu = ({
|
||||
options,
|
||||
updateOption,
|
||||
unreadChapters,
|
||||
scanlators,
|
||||
excludeScanlators,
|
||||
}: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -90,7 +94,14 @@ export const ChaptersToolbarMenu = ({
|
||||
<FilterList color={isFiltered ? 'warning' : undefined} />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
<ChapterOptions open={open} onClose={() => setOpen(false)} options={options} updateOption={updateOption} />
|
||||
<ChapterOptions
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
options={options}
|
||||
updateOption={updateOption}
|
||||
scanlators={scanlators}
|
||||
excludedScanlators={excludeScanlators}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -439,4 +439,12 @@ export class Chapters {
|
||||
|
||||
return Math.max(0, Math.floor(higherChapterNumber) - Math.floor(lowerChapterNumber) - 1);
|
||||
}
|
||||
|
||||
static getScanlators<Chapter extends ChapterScanlatorInfo>(chapters: Chapter[]): string[] {
|
||||
return [
|
||||
...new Set(
|
||||
chapters.map((chapter) => chapter.scanlator).filter((scanlator) => typeof scanlator === 'string'),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
ChapterDownloadInfo,
|
||||
ChapterListOptions,
|
||||
ChapterReadInfo,
|
||||
ChapterScanlatorInfo,
|
||||
} from '@/modules/chapter/Chapter.types.ts';
|
||||
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
|
||||
import { GqlMetaHolder } from '@/modules/metadata/Metadata.types.ts';
|
||||
@@ -56,6 +57,10 @@ function bookmarkedFilter(
|
||||
}
|
||||
}
|
||||
|
||||
function scanlatorFilter(excludedScanlators: string[], { scanlator }: ChapterScanlatorInfo): boolean {
|
||||
return !!scanlator && !excludedScanlators.includes(scanlator);
|
||||
}
|
||||
|
||||
type TChapterSort = Pick<ChapterType, 'sourceOrder' | 'fetchedAt' | 'chapterNumber' | 'uploadDate'>;
|
||||
const sortChapters = <T extends TChapterSort>(
|
||||
chapters: T[],
|
||||
@@ -87,7 +92,7 @@ const sortChapters = <T extends TChapterSort>(
|
||||
return sortedChapters;
|
||||
};
|
||||
|
||||
type TChapterFilter = TChapterSort & ChapterReadInfo & ChapterDownloadInfo & ChapterBookmarkInfo;
|
||||
type TChapterFilter = TChapterSort & ChapterReadInfo & ChapterDownloadInfo & ChapterBookmarkInfo & ChapterScanlatorInfo;
|
||||
export function filterAndSortChapters<Chapters extends TChapterFilter>(
|
||||
chapters: Chapters[],
|
||||
options: ChapterListOptions,
|
||||
@@ -96,23 +101,25 @@ export function filterAndSortChapters<Chapters extends TChapterFilter>(
|
||||
(chp) =>
|
||||
unreadFilter(options.unread, chp) &&
|
||||
downloadFilter(options.downloaded, chp) &&
|
||||
bookmarkedFilter(options.bookmarked, chp),
|
||||
bookmarkedFilter(options.bookmarked, chp) &&
|
||||
scanlatorFilter(options.excludedScanlators, chp),
|
||||
);
|
||||
|
||||
return sortChapters(filtered, options);
|
||||
}
|
||||
|
||||
export const isFilterActive = (options: ChapterListOptions) => {
|
||||
const { unread, downloaded, bookmarked } = options;
|
||||
return unread != null || downloaded != null || bookmarked != null;
|
||||
const { unread, downloaded, bookmarked, excludedScanlators } = options;
|
||||
return unread != null || downloaded != null || bookmarked != null || !!excludedScanlators.length;
|
||||
};
|
||||
|
||||
export const useChapterListOptions = (manga: MangaIdInfo & GqlMetaHolder): ChapterListOptions => {
|
||||
const { unread, downloaded, bookmarked, reverse, sortBy, showChapterNumber } = useGetMangaMetadata(manga);
|
||||
const { unread, downloaded, bookmarked, reverse, sortBy, showChapterNumber, excludedScanlators } =
|
||||
useGetMangaMetadata(manga);
|
||||
|
||||
return useMemo(
|
||||
() => ({ unread, downloaded, bookmarked, reverse, sortBy, showChapterNumber }),
|
||||
[unread, downloaded, bookmarked, reverse, sortBy, showChapterNumber],
|
||||
() => ({ unread, downloaded, bookmarked, reverse, sortBy, showChapterNumber, excludedScanlators }),
|
||||
[unread, downloaded, bookmarked, reverse, sortBy, showChapterNumber, excludedScanlators],
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user