Add setting to apply chapter list filters in reader
This commit is contained in:
@@ -995,7 +995,9 @@
|
||||
"reading_mode": "Reading mode",
|
||||
"show_page_number": "Show page number",
|
||||
"skip_dup_chapters": "Skip duplicate chapters",
|
||||
"static_navigation": "Static navigation"
|
||||
"skip_filtered_chapters": "Skip filtered chapters",
|
||||
"static_navigation": "Static navigation",
|
||||
"unchangeable_in_reader": "Setting can not be changed while the reader is opened"
|
||||
},
|
||||
"overlay_mode": "Overlay mode",
|
||||
"page_scale": {
|
||||
|
||||
@@ -92,18 +92,25 @@ const sortChapters = <T extends TChapterSort>(
|
||||
return sortedChapters;
|
||||
};
|
||||
|
||||
type TChapterFilter = TChapterSort & ChapterReadInfo & ChapterDownloadInfo & ChapterBookmarkInfo & ChapterScanlatorInfo;
|
||||
export function filterAndSortChapters<Chapters extends TChapterFilter>(
|
||||
type TChapterFilter = ChapterReadInfo & ChapterDownloadInfo & ChapterBookmarkInfo & ChapterScanlatorInfo;
|
||||
export function filterChapters<Chapters extends TChapterFilter>(
|
||||
chapters: Chapters[],
|
||||
options: ChapterListOptions,
|
||||
): Chapters[] {
|
||||
const filtered = chapters.filter(
|
||||
return chapters.filter(
|
||||
(chp) =>
|
||||
unreadFilter(options.unread, chp) &&
|
||||
downloadFilter(options.downloaded, chp) &&
|
||||
bookmarkedFilter(options.bookmarked, chp) &&
|
||||
scanlatorFilter(options.excludedScanlators, chp),
|
||||
);
|
||||
}
|
||||
|
||||
export function filterAndSortChapters<Chapters extends TChapterSort & TChapterFilter>(
|
||||
chapters: Chapters[],
|
||||
options: ChapterListOptions,
|
||||
): Chapters[] {
|
||||
const filtered = filterChapters(chapters, options);
|
||||
|
||||
return sortChapters(filtered, options);
|
||||
}
|
||||
|
||||
@@ -13,8 +13,9 @@ import {
|
||||
CHAPTER_ACTION_TO_CONFIRMATION_REQUIRED,
|
||||
CHAPTER_ACTION_TO_TRANSLATION,
|
||||
} from '@/modules/chapter/Chapter.constants.ts';
|
||||
import { GqlMetaHolder } from '@/modules/metadata/Metadata.types.ts';
|
||||
|
||||
export const FALLBACK_MANGA: MangaIdInfo = { id: -1 };
|
||||
export const FALLBACK_MANGA: MangaIdInfo & GqlMetaHolder = { id: -1 };
|
||||
|
||||
export const GLOBAL_READER_SETTINGS_MANGA: MangaIdInfo = { id: -2 };
|
||||
|
||||
|
||||
@@ -259,6 +259,9 @@ export const APP_METADATA: Record<
|
||||
shouldSkipDupChapters: {
|
||||
convert: convertToBoolean,
|
||||
},
|
||||
shouldSkipFilteredChapters: {
|
||||
convert: convertToBoolean,
|
||||
},
|
||||
isStaticNav: {
|
||||
convert: convertToBoolean,
|
||||
},
|
||||
@@ -452,6 +455,7 @@ export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
|
||||
'exitMode',
|
||||
'customFilter',
|
||||
'shouldSkipDupChapters',
|
||||
'shouldSkipFilteredChapters',
|
||||
'hotkeys',
|
||||
'shouldShowTransitionPage',
|
||||
|
||||
|
||||
@@ -155,7 +155,8 @@ const BaseReaderSettingsTabs = ({
|
||||
<ReaderBehaviourSettings
|
||||
settings={settings}
|
||||
updateSetting={(...args) => updateSetting(...args)}
|
||||
isDefaultable
|
||||
// @ts-expect-error - TS2322: Type boolean is not assignable to type true
|
||||
isDefaultable={!areDefaultSettings}
|
||||
onDefault={(...args) => deleteSetting?.(...args)}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
@@ -27,6 +27,7 @@ export const ReaderBehaviourSettings = ({
|
||||
settings,
|
||||
updateSetting,
|
||||
onDefault,
|
||||
isDefaultable,
|
||||
}: {
|
||||
settings: IReaderSettingsWithDefaultFlag;
|
||||
updateSetting: (
|
||||
@@ -50,6 +51,21 @@ export const ReaderBehaviourSettings = ({
|
||||
checked={settings.shouldSkipDupChapters}
|
||||
onChange={(_, checked) => updateSetting('shouldSkipDupChapters', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={
|
||||
<Box>
|
||||
<Typography>{t('reader.settings.label.skip_filtered_chapters')}</Typography>
|
||||
{isDefaultable && (
|
||||
<Typography variant="body2" color="textDisabled">
|
||||
{t('reader.settings.label.unchangeable_in_reader')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
checked={settings.shouldSkipFilteredChapters}
|
||||
onChange={(_, checked) => updateSetting('shouldSkipFilteredChapters', checked)}
|
||||
disabled={isDefaultable}
|
||||
/>
|
||||
{isOffsetDoubleSpreadPagesEditable(settings.readingMode.value) && (
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.label.offset_double_spread')}
|
||||
|
||||
@@ -135,6 +135,7 @@ const GLOBAL_READER_SETTING_OBJECT: Record<keyof IReaderSettingsGlobal, undefine
|
||||
exitMode: undefined,
|
||||
customFilter: undefined,
|
||||
shouldSkipDupChapters: undefined,
|
||||
shouldSkipFilteredChapters: undefined,
|
||||
progressBarType: undefined,
|
||||
progressBarSize: undefined,
|
||||
progressBarPosition: undefined,
|
||||
@@ -170,6 +171,7 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = {
|
||||
shouldStretchPage: false,
|
||||
shouldOffsetDoubleSpreads: false,
|
||||
shouldSkipDupChapters: true,
|
||||
shouldSkipFilteredChapters: false,
|
||||
shouldShowPageNumber: true,
|
||||
isStaticNav: false,
|
||||
readingDirection: ReadingDirection.LTR,
|
||||
|
||||
@@ -10,7 +10,6 @@ import { createContext, useContext } from 'react';
|
||||
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
|
||||
|
||||
export const READER_STATE_CHAPTERS_DEFAULTS: Omit<ReaderStateChapters, 'setReaderStateChapters'> = {
|
||||
mangaChapters: [],
|
||||
chapters: [],
|
||||
isCurrentChapterReady: false,
|
||||
visibleChapters: {
|
||||
|
||||
@@ -18,14 +18,20 @@ import {
|
||||
ReaderStateChapters,
|
||||
} from '@/modules/reader/types/Reader.types.ts';
|
||||
import { READER_STATE_CHAPTERS_DEFAULTS } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { filterChapters } from '@/modules/chapter/utils/ChapterList.util.tsx';
|
||||
import { ChapterListOptions } from '@/modules/chapter/Chapter.types.ts';
|
||||
import { getReaderChapterFromCache } from '@/modules/reader/utils/Reader.utils.ts';
|
||||
|
||||
export const useReaderSetChaptersState = (
|
||||
chaptersResponse: ReturnType<typeof requestManager.useGetMangaChapters<GetChaptersReaderQuery>>,
|
||||
chapterSourceOrder: number,
|
||||
mangaChapters: ReaderStateChapters['mangaChapters'],
|
||||
initialChapter: ReaderStateChapters['initialChapter'],
|
||||
chapterForDuplicatesHandling: ReaderStateChapters['chapterForDuplicatesHandling'],
|
||||
setReaderStateChapters: ReaderStateChapters['setReaderStateChapters'],
|
||||
shouldSkipDupChapters: IReaderSettings['shouldSkipDupChapters'],
|
||||
shouldSkipFilteredChapters: IReaderSettings['shouldSkipFilteredChapters'],
|
||||
chapterListOptions: ChapterListOptions,
|
||||
) => {
|
||||
const navigate = useNavigate();
|
||||
const locationState = useLocation<ReaderOpenChapterLocationState>().state;
|
||||
@@ -41,33 +47,30 @@ export const useReaderSetChaptersState = (
|
||||
const newInitialChapter = finalInitialChapter ?? newCurrentChapter;
|
||||
const newChapterForDuplicatesHandling = chapterForDuplicatesHandling ?? newCurrentChapter;
|
||||
|
||||
const nextChapter =
|
||||
newMangaChapters &&
|
||||
newCurrentChapter &&
|
||||
Chapters.getNextChapter(newCurrentChapter, newMangaChapters, {
|
||||
offset: DirectionOffset.NEXT,
|
||||
skipDupe: shouldSkipDupChapters,
|
||||
skipDupeChapter: newChapterForDuplicatesHandling,
|
||||
});
|
||||
const previousChapter =
|
||||
newMangaChapters &&
|
||||
newCurrentChapter &&
|
||||
Chapters.getNextChapter(newCurrentChapter, newMangaChapters, {
|
||||
offset: DirectionOffset.PREVIOUS,
|
||||
skipDupe: shouldSkipDupChapters,
|
||||
skipDupeChapter: newChapterForDuplicatesHandling,
|
||||
});
|
||||
const newChapters = (() => {
|
||||
const visibleChapters = (() => {
|
||||
if (!newMangaChapters || !newChapterForDuplicatesHandling) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (shouldSkipDupChapters) {
|
||||
return Chapters.removeDuplicates(newChapterForDuplicatesHandling, newMangaChapters);
|
||||
}
|
||||
const filteredChapters = shouldSkipFilteredChapters
|
||||
? filterChapters(mangaChapters ?? newMangaChapters, chapterListOptions)
|
||||
: newMangaChapters;
|
||||
const uniqueChapters = shouldSkipDupChapters
|
||||
? Chapters.removeDuplicates(newChapterForDuplicatesHandling, filteredChapters)
|
||||
: filteredChapters;
|
||||
|
||||
return newMangaChapters;
|
||||
return uniqueChapters.map((chapter) => getReaderChapterFromCache(chapter.id)!);
|
||||
})();
|
||||
const nextChapter =
|
||||
newCurrentChapter &&
|
||||
Chapters.getNextChapter(newCurrentChapter, visibleChapters, {
|
||||
offset: DirectionOffset.NEXT,
|
||||
});
|
||||
const previousChapter =
|
||||
newCurrentChapter &&
|
||||
Chapters.getNextChapter(newCurrentChapter, visibleChapters, {
|
||||
offset: DirectionOffset.PREVIOUS,
|
||||
});
|
||||
|
||||
const hasInitialChapterChanged = newInitialChapter != null && newInitialChapter.id !== finalInitialChapter?.id;
|
||||
|
||||
@@ -77,10 +80,11 @@ export const useReaderSetChaptersState = (
|
||||
|
||||
setReaderStateChapters((prevState) => {
|
||||
const hasCurrentChapterChanged = newCurrentChapter?.id !== prevState.currentChapter?.id;
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
mangaChapters: newMangaChapters ?? [],
|
||||
chapters: newChapters,
|
||||
mangaChapters: prevState.mangaChapters ?? newMangaChapters,
|
||||
chapters: visibleChapters,
|
||||
initialChapter: newInitialChapter,
|
||||
chapterForDuplicatesHandling: newChapterForDuplicatesHandling,
|
||||
currentChapter: newCurrentChapter,
|
||||
@@ -104,5 +108,12 @@ export const useReaderSetChaptersState = (
|
||||
: prevState.visibleChapters,
|
||||
};
|
||||
});
|
||||
}, [chaptersResponse.data?.chapters.nodes, chapterSourceOrder, shouldSkipDupChapters, finalInitialChapter]);
|
||||
}, [
|
||||
chaptersResponse.data?.chapters.nodes,
|
||||
chapterSourceOrder,
|
||||
shouldSkipDupChapters,
|
||||
shouldSkipFilteredChapters,
|
||||
finalInitialChapter,
|
||||
chapterListOptions,
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -52,6 +52,8 @@ import { useReaderSetSettingsState } from '@/modules/reader/hooks/useReaderSetSe
|
||||
import { useReaderShowSettingPreviewOnChange } from '@/modules/reader/hooks/useReaderShowSettingPreviewOnChange.ts';
|
||||
import { useReaderSetChaptersState } from '@/modules/reader/hooks/useReaderSetChaptersState.ts';
|
||||
import { useAppTitle } from '@/modules/navigation-bar/hooks/useAppTitle.ts';
|
||||
import { useChapterListOptions } from '@/modules/chapter/utils/ChapterList.util.tsx';
|
||||
import { FALLBACK_MANGA } from '@/modules/manga/Manga.constants.ts';
|
||||
|
||||
const BaseReader = ({
|
||||
setOverride,
|
||||
@@ -61,6 +63,7 @@ const BaseReader = ({
|
||||
manga,
|
||||
setManga,
|
||||
shouldSkipDupChapters,
|
||||
shouldSkipFilteredChapters,
|
||||
backgroundColor,
|
||||
readingMode,
|
||||
tapZoneLayout,
|
||||
@@ -68,6 +71,7 @@ const BaseReader = ({
|
||||
shouldShowReadingModePreview,
|
||||
shouldShowTapZoneLayoutPreview,
|
||||
setSettings,
|
||||
mangaChapters,
|
||||
initialChapter,
|
||||
chapterForDuplicatesHandling,
|
||||
currentChapter,
|
||||
@@ -87,12 +91,20 @@ const BaseReader = ({
|
||||
Pick<TReaderStateSettingsContext, 'setSettings'> &
|
||||
Pick<
|
||||
IReaderSettings,
|
||||
'shouldSkipDupChapters' | 'backgroundColor' | 'shouldShowReadingModePreview' | 'shouldShowTapZoneLayoutPreview'
|
||||
| 'shouldSkipDupChapters'
|
||||
| 'shouldSkipFilteredChapters'
|
||||
| 'backgroundColor'
|
||||
| 'shouldShowReadingModePreview'
|
||||
| 'shouldShowTapZoneLayoutPreview'
|
||||
> &
|
||||
Pick<IReaderSettingsWithDefaultFlag, 'readingMode' | 'tapZoneLayout' | 'tapZoneInvertMode'> &
|
||||
Pick<
|
||||
ReaderStateChapters,
|
||||
'initialChapter' | 'chapterForDuplicatesHandling' | 'currentChapter' | 'setReaderStateChapters'
|
||||
| 'mangaChapters'
|
||||
| 'initialChapter'
|
||||
| 'chapterForDuplicatesHandling'
|
||||
| 'currentChapter'
|
||||
| 'setReaderStateChapters'
|
||||
> &
|
||||
Pick<
|
||||
ReaderStatePages,
|
||||
@@ -134,6 +146,7 @@ const BaseReader = ({
|
||||
settings: defaultSettings,
|
||||
request: defaultSettingsResponse,
|
||||
} = useDefaultReaderSettings();
|
||||
const chapterListOptions = useChapterListOptions(manga ?? FALLBACK_MANGA);
|
||||
|
||||
const isLoading =
|
||||
currentChapter === undefined ||
|
||||
@@ -183,10 +196,13 @@ const BaseReader = ({
|
||||
useReaderSetChaptersState(
|
||||
chaptersResponse,
|
||||
chapterSourceOrder,
|
||||
mangaChapters,
|
||||
initialChapter,
|
||||
chapterForDuplicatesHandling,
|
||||
setReaderStateChapters,
|
||||
shouldSkipDupChapters,
|
||||
shouldSkipFilteredChapters,
|
||||
chapterListOptions,
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -296,12 +312,14 @@ export const Reader = withPropsFrom(
|
||||
() => {
|
||||
const {
|
||||
shouldSkipDupChapters,
|
||||
shouldSkipFilteredChapters,
|
||||
backgroundColor,
|
||||
shouldShowReadingModePreview,
|
||||
shouldShowTapZoneLayoutPreview,
|
||||
} = ReaderService.useSettingsWithoutDefaultFlag();
|
||||
return {
|
||||
shouldSkipDupChapters,
|
||||
shouldSkipFilteredChapters,
|
||||
backgroundColor,
|
||||
shouldShowReadingModePreview,
|
||||
shouldShowTapZoneLayoutPreview,
|
||||
@@ -323,6 +341,7 @@ export const Reader = withPropsFrom(
|
||||
'manga',
|
||||
'setManga',
|
||||
'shouldSkipDupChapters',
|
||||
'shouldSkipFilteredChapters',
|
||||
'backgroundColor',
|
||||
'readingMode',
|
||||
'tapZoneLayout',
|
||||
@@ -330,6 +349,7 @@ export const Reader = withPropsFrom(
|
||||
'shouldShowReadingModePreview',
|
||||
'shouldShowTapZoneLayoutPreview',
|
||||
'setSettings',
|
||||
'mangaChapters',
|
||||
'initialChapter',
|
||||
'chapterForDuplicatesHandling',
|
||||
'currentChapter',
|
||||
|
||||
@@ -475,32 +475,22 @@ export class ReaderControls {
|
||||
endReached?: boolean,
|
||||
) => void {
|
||||
const { currentPageIndex, setCurrentPageIndex } = userReaderStatePagesContext();
|
||||
const {
|
||||
chapterForDuplicatesHandling,
|
||||
currentChapter,
|
||||
previousChapter,
|
||||
nextChapter,
|
||||
mangaChapters,
|
||||
visibleChapters,
|
||||
setReaderStateChapters,
|
||||
} = useReaderStateChaptersContext();
|
||||
const { currentChapter, chapters, previousChapter, nextChapter, visibleChapters, setReaderStateChapters } =
|
||||
useReaderStateChaptersContext();
|
||||
const updateChapter = ReaderService.useUpdateChapter();
|
||||
const { shouldSkipDupChapters } = ReaderService.useSettings();
|
||||
const {
|
||||
settings: { downloadAheadLimit },
|
||||
} = useMetadataServerSettings();
|
||||
|
||||
const nextChapters = useMemo(() => {
|
||||
if (!chapterForDuplicatesHandling || !currentChapter) {
|
||||
if (!currentChapter) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Chapters.getNextChapters(currentChapter, mangaChapters, {
|
||||
return Chapters.getNextChapters(currentChapter, chapters, {
|
||||
offset: DirectionOffset.NEXT,
|
||||
skipDupe: shouldSkipDupChapters,
|
||||
skipDupeChapter: chapterForDuplicatesHandling,
|
||||
});
|
||||
}, [chapterForDuplicatesHandling?.id, currentChapter?.id, mangaChapters, shouldSkipDupChapters]);
|
||||
}, [currentChapter?.id, chapters]);
|
||||
|
||||
return useCallback(
|
||||
(pageIndex, debounceChapterUpdate = true, endReached = false) => {
|
||||
|
||||
@@ -157,27 +157,25 @@ export class ReaderService {
|
||||
|
||||
static useUpdateChapter(): (patch: UpdateChapterPatchInput) => void {
|
||||
const { manga } = useReaderStateMangaContext();
|
||||
const { chapterForDuplicatesHandling, currentChapter, mangaChapters } = useReaderStateChaptersContext();
|
||||
const { currentChapter, mangaChapters, chapters } = useReaderStateChaptersContext();
|
||||
const { shouldSkipDupChapters } = ReaderService.useSettings();
|
||||
const {
|
||||
settings: { deleteChaptersWhileReading, deleteChaptersWithBookmark, updateProgressAfterReading },
|
||||
} = useMetadataServerSettings();
|
||||
|
||||
const previousChapters = useMemo(() => {
|
||||
if (!chapterForDuplicatesHandling || !currentChapter) {
|
||||
if (!currentChapter) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Chapters.getNextChapters(currentChapter, mangaChapters, {
|
||||
return Chapters.getNextChapters(currentChapter, chapters, {
|
||||
offset: DirectionOffset.PREVIOUS,
|
||||
skipDupe: shouldSkipDupChapters,
|
||||
skipDupeChapter: chapterForDuplicatesHandling,
|
||||
});
|
||||
}, [chapterForDuplicatesHandling?.id, currentChapter?.id, mangaChapters, shouldSkipDupChapters]);
|
||||
}, [currentChapter?.id, chapters]);
|
||||
|
||||
return useCallback(
|
||||
(patch) => {
|
||||
if (!manga || !currentChapter) {
|
||||
if (!manga || !currentChapter || !mangaChapters) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -144,6 +144,7 @@ export interface IReaderSettingsGlobal {
|
||||
exitMode: ReaderExitMode;
|
||||
customFilter: ReaderCustomFilter;
|
||||
shouldSkipDupChapters: boolean;
|
||||
shouldSkipFilteredChapters: boolean;
|
||||
progressBarType: ProgressBarType;
|
||||
/**
|
||||
* pixel
|
||||
@@ -202,14 +203,19 @@ export interface IReaderSettingsWithDefaultFlag
|
||||
|
||||
export interface ReaderStateChapters {
|
||||
/**
|
||||
* all chapters of the manga
|
||||
*/
|
||||
mangaChapters: TChapterReader[];
|
||||
/**
|
||||
* actual chapters that have been filtered
|
||||
* All chapters of the manga with their state preserved from the initial reader load.
|
||||
*
|
||||
* optional filters:
|
||||
* - removed duplicate chapters
|
||||
* The state needs to be preserved so that chapter state changes do not lead to chapters getting filtered out.
|
||||
* E.g., in case read chapters are filtered out. This would then lead to the removal of the current chapter
|
||||
* after the end of the chapter is reached.
|
||||
*/
|
||||
mangaChapters?: TChapterReader[];
|
||||
/**
|
||||
* Actual chapters that have been filtered
|
||||
*
|
||||
* Optional filters:
|
||||
* - Removed duplicate chapters
|
||||
* - Manga chapter list filters
|
||||
*/
|
||||
chapters: TChapterReader[];
|
||||
chapterForDuplicatesHandling?: TChapterReader | null;
|
||||
|
||||
Reference in New Issue
Block a user