From 51975ea3cde3ab8e8d437cd6d78026056a340c85 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Tue, 1 Jul 2025 00:37:29 +0200 Subject: [PATCH] Add setting to apply chapter list filters in reader --- public/locales/en.json | 4 +- .../chapter/utils/ChapterList.util.tsx | 13 +++- src/modules/manga/Manga.constants.ts | 3 +- src/modules/metadata/Metadata.constants.ts | 4 ++ .../settings/ReaderSettingsTabs.tsx | 3 +- .../behaviour/ReaderBehaviourSettings.tsx | 16 +++++ .../constants/ReaderSettings.constants.tsx | 2 + .../state/ReaderStateChaptersContext.tsx | 1 - .../reader/hooks/useReaderSetChaptersState.ts | 59 +++++++++++-------- src/modules/reader/screens/Reader.tsx | 24 +++++++- src/modules/reader/services/ReaderControls.ts | 20 ++----- src/modules/reader/services/ReaderService.ts | 12 ++-- src/modules/reader/types/Reader.types.ts | 20 ++++--- 13 files changed, 119 insertions(+), 62 deletions(-) diff --git a/public/locales/en.json b/public/locales/en.json index 02bc4b10..93ece585 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -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": { diff --git a/src/modules/chapter/utils/ChapterList.util.tsx b/src/modules/chapter/utils/ChapterList.util.tsx index 7a21f4b3..df7d9b84 100644 --- a/src/modules/chapter/utils/ChapterList.util.tsx +++ b/src/modules/chapter/utils/ChapterList.util.tsx @@ -92,18 +92,25 @@ const sortChapters = ( return sortedChapters; }; -type TChapterFilter = TChapterSort & ChapterReadInfo & ChapterDownloadInfo & ChapterBookmarkInfo & ChapterScanlatorInfo; -export function filterAndSortChapters( +type TChapterFilter = ChapterReadInfo & ChapterDownloadInfo & ChapterBookmarkInfo & ChapterScanlatorInfo; +export function filterChapters( 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: Chapters[], + options: ChapterListOptions, +): Chapters[] { + const filtered = filterChapters(chapters, options); return sortChapters(filtered, options); } diff --git a/src/modules/manga/Manga.constants.ts b/src/modules/manga/Manga.constants.ts index dc3a00cc..05dcadfc 100644 --- a/src/modules/manga/Manga.constants.ts +++ b/src/modules/manga/Manga.constants.ts @@ -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 }; diff --git a/src/modules/metadata/Metadata.constants.ts b/src/modules/metadata/Metadata.constants.ts index da32e494..9e3847cb 100644 --- a/src/modules/metadata/Metadata.constants.ts +++ b/src/modules/metadata/Metadata.constants.ts @@ -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', diff --git a/src/modules/reader/components/settings/ReaderSettingsTabs.tsx b/src/modules/reader/components/settings/ReaderSettingsTabs.tsx index 4f390b0c..2b9b2acb 100644 --- a/src/modules/reader/components/settings/ReaderSettingsTabs.tsx +++ b/src/modules/reader/components/settings/ReaderSettingsTabs.tsx @@ -155,7 +155,8 @@ const BaseReaderSettingsTabs = ({ updateSetting(...args)} - isDefaultable + // @ts-expect-error - TS2322: Type boolean is not assignable to type true + isDefaultable={!areDefaultSettings} onDefault={(...args) => deleteSetting?.(...args)} /> diff --git a/src/modules/reader/components/settings/behaviour/ReaderBehaviourSettings.tsx b/src/modules/reader/components/settings/behaviour/ReaderBehaviourSettings.tsx index 6a29f051..74fec5cb 100644 --- a/src/modules/reader/components/settings/behaviour/ReaderBehaviourSettings.tsx +++ b/src/modules/reader/components/settings/behaviour/ReaderBehaviourSettings.tsx @@ -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)} /> + + {t('reader.settings.label.skip_filtered_chapters')} + {isDefaultable && ( + + {t('reader.settings.label.unchangeable_in_reader')} + + )} + + } + checked={settings.shouldSkipFilteredChapters} + onChange={(_, checked) => updateSetting('shouldSkipFilteredChapters', checked)} + disabled={isDefaultable} + /> {isOffsetDoubleSpreadPagesEditable(settings.readingMode.value) && ( = { - mangaChapters: [], chapters: [], isCurrentChapterReady: false, visibleChapters: { diff --git a/src/modules/reader/hooks/useReaderSetChaptersState.ts b/src/modules/reader/hooks/useReaderSetChaptersState.ts index cf738c26..2572679c 100644 --- a/src/modules/reader/hooks/useReaderSetChaptersState.ts +++ b/src/modules/reader/hooks/useReaderSetChaptersState.ts @@ -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>, 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().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, + ]); }; diff --git a/src/modules/reader/screens/Reader.tsx b/src/modules/reader/screens/Reader.tsx index 61e9ba03..8272a73d 100644 --- a/src/modules/reader/screens/Reader.tsx +++ b/src/modules/reader/screens/Reader.tsx @@ -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 & Pick< IReaderSettings, - 'shouldSkipDupChapters' | 'backgroundColor' | 'shouldShowReadingModePreview' | 'shouldShowTapZoneLayoutPreview' + | 'shouldSkipDupChapters' + | 'shouldSkipFilteredChapters' + | 'backgroundColor' + | 'shouldShowReadingModePreview' + | 'shouldShowTapZoneLayoutPreview' > & Pick & 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', diff --git a/src/modules/reader/services/ReaderControls.ts b/src/modules/reader/services/ReaderControls.ts index 38221aea..7dc31d32 100644 --- a/src/modules/reader/services/ReaderControls.ts +++ b/src/modules/reader/services/ReaderControls.ts @@ -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) => { diff --git a/src/modules/reader/services/ReaderService.ts b/src/modules/reader/services/ReaderService.ts index 163ef3bd..579077f2 100644 --- a/src/modules/reader/services/ReaderService.ts +++ b/src/modules/reader/services/ReaderService.ts @@ -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; } diff --git a/src/modules/reader/types/Reader.types.ts b/src/modules/reader/types/Reader.types.ts index 4b6342c1..b0617c05 100644 --- a/src/modules/reader/types/Reader.types.ts +++ b/src/modules/reader/types/Reader.types.ts @@ -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;