From 2acf2b6d33742b9b09becce3912347f0d9f3f1f3 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Fri, 16 Feb 2024 19:16:18 +0100 Subject: [PATCH] Feature/download ahead trigger chapter downloads client side (#597) * Trigger download ahead while reading client side * Trigger download ahead while reading earlier Download ahead was only triggered once the chapter got marked as read * Optionally ignore dupe chapters for download ahead * Consider current chapters scanlator for dupe check * Update download ahead related settings - add new metadata setting for download ahead limit - previous setting - rename - update usage (auto download new chapters limit) * Select chapter to auto delete while reading depending on ignore dupe setting Currently, the n-th to last chapter always got deleted ignoring if "ignore dupes" setting was enabled or not. * Only auto delete chapter while reading if it is read In case not the last read but the n-th to last read chapter should get auto deleted while reading, it currently just got deleted ignoring if it has been read or not. --- .../downloads/DownloadAheadSetting.tsx | 30 ++-- src/i18n/locale/en.json | 7 +- src/lib/data/Chapters.ts | 97 +++++++++++- src/lib/graphql/generated/graphql.ts | 10 +- src/lib/graphql/mutations/ChapterMutation.ts | 13 -- src/lib/requests/RequestManager.ts | 15 +- src/screens/Reader.tsx | 148 ++++++++++-------- src/screens/settings/DownloadSettings.tsx | 26 ++- src/typings.ts | 1 + src/util/metadataServerSettings.ts | 1 + 10 files changed, 232 insertions(+), 116 deletions(-) diff --git a/src/components/settings/downloads/DownloadAheadSetting.tsx b/src/components/settings/downloads/DownloadAheadSetting.tsx index d848796f..106cda57 100644 --- a/src/components/settings/downloads/DownloadAheadSetting.tsx +++ b/src/components/settings/downloads/DownloadAheadSetting.tsx @@ -8,10 +8,12 @@ import { useTranslation } from 'react-i18next'; import { List, ListItem, ListItemText, Switch } from '@mui/material'; -import { useCallback } from 'react'; -import { requestManager } from '@/lib/requests/RequestManager.ts'; import { NumberSetting } from '@/components/settings/NumberSetting.tsx'; import { getPersistedServerSetting, usePersistedValue } from '@/util/usePersistedValue.tsx'; +import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts'; +import { MetadataServerSettings } from '@/typings.ts'; +import { convertToGqlMeta, requestUpdateServerMetadata } from '@/util/metadata.ts'; +import { makeToast } from '@/components/util/Toast.tsx'; const MIN_LIMIT = 2; const MAX_LIMIT = 10; @@ -20,10 +22,12 @@ const DEFAULT_LIMIT = MIN_LIMIT; export const DownloadAheadSetting = () => { const { t } = useTranslation(); - const { data } = requestManager.useGetServerSettings(); - const downloadAheadLimit = data?.settings.autoDownloadAheadLimit; + const { + metadata, + settings: { downloadAheadLimit }, + } = useMetadataServerSettings(); + const shouldDownloadAhead = !!downloadAheadLimit; - const [mutateSettings] = requestManager.useUpdateServerSettings(); const [currentDownloadAheadLimit, persistDownloadAheadLimit] = usePersistedValue( 'lastDownloadAheadLimit', DEFAULT_LIMIT, @@ -31,15 +35,12 @@ export const DownloadAheadSetting = () => { getPersistedServerSetting, ); - const updateSetting = useCallback( - (autoDownloadAheadLimit: number) => { - persistDownloadAheadLimit( - autoDownloadAheadLimit === 0 ? currentDownloadAheadLimit : autoDownloadAheadLimit, - ); - mutateSettings({ variables: { input: { settings: { autoDownloadAheadLimit } } } }); - }, - [currentDownloadAheadLimit], - ); + const updateSetting = (value: MetadataServerSettings['downloadAheadLimit']) => { + persistDownloadAheadLimit(value === 0 ? currentDownloadAheadLimit : value); + requestUpdateServerMetadata(convertToGqlMeta(metadata)! ?? {}, [['downloadAheadLimit', value]]).catch(() => + makeToast(t('search.error.label.failed_to_save_settings'), 'warning'), + ); + }; const setDoAutoUpdates = (enable: boolean) => { const globalUpdateInterval = enable ? currentDownloadAheadLimit : 0; @@ -68,7 +69,6 @@ export const DownloadAheadSetting = () => { defaultValue={DEFAULT_LIMIT} showSlider dialogDescription={t('download.settings.download_ahead.label.description')} - dialogDisclaimer={t('download.settings.download_ahead.label.disclaimer')} valueUnit={t('chapter.title')} handleUpdate={updateSetting} disabled={!shouldDownloadAhead} diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index ab6f7db4..9ae08e4c 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -167,6 +167,12 @@ "include_in_download": "Entries in excluded categories will not be downloaded even if they are also in included categories" } }, + "download_limit": { + "label": { + "description": "Limit the amount of new chapters that are going to get downloaded.", + "title": "Chapter download limit" + } + }, "label": { "ignore_with_unread_chapters": "Ignore automatic chapter downloads for entries with unread chapters", "new_chapters": "Download new chapters" @@ -197,7 +203,6 @@ "download_ahead": { "label": { "description": "How many chapters should get downloaded when marking a chapter as read while reading.", - "disclaimer": "This limit will also be applied to the automatic download of new chapters during an update", "unread_chapters_to_download": "Number of unread chapters to download", "value": "{{chapters}} $t(chapter.title)", "while_reading": "Auto download while reading" diff --git a/src/lib/data/Chapters.ts b/src/lib/data/Chapters.ts index ccf07c59..ea2c68ac 100644 --- a/src/lib/data/Chapters.ts +++ b/src/lib/data/Chapters.ts @@ -7,10 +7,13 @@ */ import { t as translate } from 'i18next'; -import { TChapter, TranslationKey } from '@/typings.ts'; +import gql from 'graphql-tag'; +import { DocumentNode } from '@apollo/client'; +import { ChapterOffset, TChapter, TranslationKey } from '@/typings.ts'; import { makeToast } from '@/components/util/Toast.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { getMetadataServerSettings } from '@/util/metadataServerSettings.ts'; +import { FULL_CHAPTER_FIELDS } from '@/lib/graphql/Fragments.ts'; export type ChapterAction = 'download' | 'delete' | 'bookmark' | 'unbookmark' | 'mark_as_read' | 'mark_as_unread'; @@ -79,12 +82,48 @@ export type ChapterDownloadInfo = ChapterIdInfo & Pick export type ChapterBookmarkInfo = ChapterIdInfo & Pick; export type ChapterReadInfo = ChapterIdInfo & Pick; export type ChapterNumberInfo = ChapterIdInfo & Pick; +export type ChapterScanlatorInfo = ChapterIdInfo & Pick; export class Chapters { static getIds(chapters: { id: number }[]): number[] { return chapters.map((chapter) => chapter.id); } + static getFromCache( + id: number, + fragment: DocumentNode = FULL_CHAPTER_FIELDS, + fragmentName: string = 'FULL_CHAPTER_FIELDS', + ): T | null { + return requestManager.graphQLClient.client.cache.readFragment({ + id: requestManager.graphQLClient.client.cache.identify({ + __typename: 'ChapterType', + id, + }), + fragment, + fragmentName, + }); + } + + static isDownloading(id: number): boolean { + return !!requestManager.graphQLClient.client.cache.readFragment({ + id: requestManager.graphQLClient.client.cache.identify({ + __typename: 'DownloadType', + chapter: { + __ref: requestManager.graphQLClient.client.cache.identify({ + __typename: 'ChapterType', + id, + }), + }, + }), + fragment: gql` + fragment CHAPTER_DOWNLOAD_QUEUE_CHECK on ChapterType { + id + } + `, + fragmentName: 'CHAPTER_DOWNLOAD_QUEUE_CHECK', + }); + } + static isDownloaded({ isDownloaded }: ChapterDownloadInfo): boolean { return isDownloaded; } @@ -258,4 +297,60 @@ export class Chapters { throw new Error(`Chapters::performAction: unknown action "${action}"`); } } + + static removeDuplicates(currentChapter: T, chapters: T[]): T[] { + const chapterNumberToChapters = new Map(); + chapters.forEach((chapter) => { + const duplicateChapters = chapterNumberToChapters.get(chapter.chapterNumber) ?? []; + chapterNumberToChapters.set(chapter.chapterNumber, [...duplicateChapters, chapter]); + }); + + return [...chapterNumberToChapters.values()].map( + (groupedChapters) => + groupedChapters.find((chapter) => chapter.id === currentChapter.id) ?? + groupedChapters.findLast((chapter) => chapter.scanlator === currentChapter.scanlator) ?? + groupedChapters.slice(-1)[0], + ); + } + + static getNextChapter( + currentChapter: Chapter, + chapters: Chapter[], + { + offset = ChapterOffset.NEXT, + ...options + }: { offset?: ChapterOffset; onlyUnread?: boolean; skipDupe?: boolean } = {}, + ): Chapter | undefined { + const nextChapters = Chapters.getNextChapters(currentChapter, chapters, { offset, ...options }); + + const isNextChapterOffset = offset === ChapterOffset.NEXT; + const sliceStartIndex = isNextChapterOffset ? -1 : 0; + const sliceEndIndex = isNextChapterOffset ? undefined : 1; + + return nextChapters.slice(sliceStartIndex, sliceEndIndex)[0]; + } + + static getNextChapters( + fromChapter: Chapter, + chapters: Chapter[], + { + offset = ChapterOffset.NEXT, + onlyUnread = false, + skipDupe = false, + }: { offset?: ChapterOffset; onlyUnread?: boolean; skipDupe?: boolean } = {}, + ): Chapter[] { + const fromChapterIndex = chapters.findIndex((chapter) => chapter.id === fromChapter.id); + + const isNextChapterOffset = offset === ChapterOffset.NEXT; + const sliceStartIndex = isNextChapterOffset ? 0 : fromChapterIndex; + const sliceEndIndex = isNextChapterOffset ? fromChapterIndex + 1 : undefined; + + const nextChaptersIncludingCurrent = chapters.slice(sliceStartIndex, sliceEndIndex); + const uniqueNextChapters = skipDupe + ? Chapters.removeDuplicates(fromChapter, nextChaptersIncludingCurrent) + : nextChaptersIncludingCurrent; + const nextChapters = uniqueNextChapters.toSpliced(isNextChapterOffset ? -1 : 0, 1); + + return onlyUnread ? Chapters.getNonRead(nextChapters) : nextChapters; + } } diff --git a/src/lib/graphql/generated/graphql.ts b/src/lib/graphql/generated/graphql.ts index a5a77dac..93fc9f33 100644 --- a/src/lib/graphql/generated/graphql.ts +++ b/src/lib/graphql/generated/graphql.ts @@ -2608,15 +2608,12 @@ export type UpdateChapterMutationVariables = Exact<{ getBookmarked: Scalars['Boolean']['input']; getRead: Scalars['Boolean']['input']; getLastPageRead: Scalars['Boolean']['input']; - id: Scalars['Int']['input']; chapterIdToDelete: Scalars['Int']['input']; deleteChapter: Scalars['Boolean']['input']; - mangaId: Scalars['Int']['input']; - downloadAhead: Scalars['Boolean']['input']; }>; -export type UpdateChapterMutation = { __typename?: 'Mutation', updateChapter: { __typename?: 'UpdateChapterPayload', clientMutationId?: string | null, chapter: { __typename?: 'ChapterType', id: number, isBookmarked?: boolean, isRead?: boolean, lastReadAt?: any, lastPageRead?: number, manga?: { __typename?: 'MangaType', id: number, unreadCount: number, lastReadChapter?: { __typename?: 'ChapterType', id: number } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number } | null } } }, deleteDownloadedChapter?: { __typename?: 'DeleteDownloadedChapterPayload', clientMutationId?: string | null, chapters: { __typename?: 'ChapterType', id: number, isDownloaded: boolean, manga: { __typename?: 'MangaType', id: number, downloadCount: number } } }, downloadAhead?: { __typename?: 'DownloadAheadPayload', clientMutationId?: string | null } }; +export type UpdateChapterMutation = { __typename?: 'Mutation', updateChapter: { __typename?: 'UpdateChapterPayload', clientMutationId?: string | null, chapter: { __typename?: 'ChapterType', id: number, isBookmarked?: boolean, isRead?: boolean, lastReadAt?: any, lastPageRead?: number, manga?: { __typename?: 'MangaType', id: number, unreadCount: number, lastReadChapter?: { __typename?: 'ChapterType', id: number } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number } | null } } }, deleteDownloadedChapter?: { __typename?: 'DeleteDownloadedChapterPayload', clientMutationId?: string | null, chapters: { __typename?: 'ChapterType', id: number, isDownloaded: boolean, manga: { __typename?: 'MangaType', id: number, downloadCount: number } } } }; export type UpdateChaptersMutationVariables = Exact<{ input: UpdateChaptersInput; @@ -2625,13 +2622,10 @@ export type UpdateChaptersMutationVariables = Exact<{ getLastPageRead: Scalars['Boolean']['input']; chapterIdsToDelete: Array | Scalars['Int']['input']; deleteChapters: Scalars['Boolean']['input']; - mangaIds: Array | Scalars['Int']['input']; - latestReadChapterIds: Array | Scalars['Int']['input']; - downloadAhead: Scalars['Boolean']['input']; }>; -export type UpdateChaptersMutation = { __typename?: 'Mutation', updateChapters: { __typename?: 'UpdateChaptersPayload', clientMutationId?: string | null, chapters: Array<{ __typename?: 'ChapterType', id: number, isBookmarked?: boolean, isRead?: boolean, lastReadAt?: any, lastPageRead?: number, manga?: { __typename?: 'MangaType', id: number, unreadCount: number, lastReadChapter?: { __typename?: 'ChapterType', id: number } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number } | null } }> }, deleteDownloadedChapters?: { __typename?: 'DeleteDownloadedChaptersPayload', clientMutationId?: string | null, chapters: Array<{ __typename?: 'ChapterType', id: number, isDownloaded: boolean, manga: { __typename?: 'MangaType', id: number, downloadCount: number } }> }, downloadAhead?: { __typename?: 'DownloadAheadPayload', clientMutationId?: string | null } }; +export type UpdateChaptersMutation = { __typename?: 'Mutation', updateChapters: { __typename?: 'UpdateChaptersPayload', clientMutationId?: string | null, chapters: Array<{ __typename?: 'ChapterType', id: number, isBookmarked?: boolean, isRead?: boolean, lastReadAt?: any, lastPageRead?: number, manga?: { __typename?: 'MangaType', id: number, unreadCount: number, lastReadChapter?: { __typename?: 'ChapterType', id: number } | null, latestReadChapter?: { __typename?: 'ChapterType', id: number } | null } }> }, deleteDownloadedChapters?: { __typename?: 'DeleteDownloadedChaptersPayload', clientMutationId?: string | null, chapters: Array<{ __typename?: 'ChapterType', id: number, isDownloaded: boolean, manga: { __typename?: 'MangaType', id: number, downloadCount: number } }> } }; export type ClearDownloaderMutationVariables = Exact<{ input?: InputMaybe; diff --git a/src/lib/graphql/mutations/ChapterMutation.ts b/src/lib/graphql/mutations/ChapterMutation.ts index cc46bd4f..f1232a4c 100644 --- a/src/lib/graphql/mutations/ChapterMutation.ts +++ b/src/lib/graphql/mutations/ChapterMutation.ts @@ -93,11 +93,8 @@ export const UPDATE_CHAPTER = gql` $getBookmarked: Boolean! $getRead: Boolean! $getLastPageRead: Boolean! - $id: Int! $chapterIdToDelete: Int! $deleteChapter: Boolean! - $mangaId: Int! - $downloadAhead: Boolean! ) { updateChapter(input: $input) { clientMutationId @@ -130,9 +127,6 @@ export const UPDATE_CHAPTER = gql` } } } - downloadAhead(input: { mangaIds: [$mangaId], latestReadChapterIds: [$id] }) @include(if: $downloadAhead) { - clientMutationId - } } `; @@ -144,9 +138,6 @@ export const UPDATE_CHAPTERS = gql` $getLastPageRead: Boolean! $chapterIdsToDelete: [Int!]! $deleteChapters: Boolean! - $mangaIds: [Int!]! - $latestReadChapterIds: [Int!]! - $downloadAhead: Boolean! ) { updateChapters(input: $input) { clientMutationId @@ -179,9 +170,5 @@ export const UPDATE_CHAPTERS = gql` } } } - downloadAhead(input: { mangaIds: $mangaIds, latestReadChapterIds: $latestReadChapterIds }) - @include(if: $downloadAhead) { - clientMutationId - } } `; diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index d00de566..f4400a87 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -1762,25 +1762,21 @@ export class RequestManager { id: number, patch: UpdateChapterPatchInput & { chapterIdToDelete?: number; - downloadAheadMangaId?: number; }, options?: MutationOptions, ): AbortableApolloMutationResponse { - const { chapterIdToDelete = -1, downloadAheadMangaId = -1, ...updatePatch } = patch; + const { chapterIdToDelete = -1, ...updatePatch } = patch; return this.doRequest( GQLMethod.MUTATION, UPDATE_CHAPTER, { - id, input: { id, patch: updatePatch }, getBookmarked: patch.isBookmarked != null, getRead: patch.isRead != null, getLastPageRead: patch.lastPageRead != null, chapterIdToDelete, deleteChapter: chapterIdToDelete >= 0, - mangaId: downloadAheadMangaId, - downloadAhead: downloadAheadMangaId !== -1, }, options, ); @@ -1809,12 +1805,10 @@ export class RequestManager { public updateChapters( ids: number[], - patch: UpdateChapterPatchInput & { chapterIdsToDelete?: number[]; mangaIds?: number[] }, + patch: UpdateChapterPatchInput & { chapterIdsToDelete?: number[] }, options?: MutationOptions, ): AbortableApolloMutationResponse { - const { chapterIdsToDelete = [], mangaIds = [], ...updatePatch } = patch; - - const downloadAhead = !!mangaIds.length; + const { chapterIdsToDelete = [], ...updatePatch } = patch; return this.doRequest( GQLMethod.MUTATION, @@ -1826,9 +1820,6 @@ export class RequestManager { getLastPageRead: patch.lastPageRead != null, chapterIdsToDelete, deleteChapters: !!chapterIdsToDelete.length, - mangaIds, - downloadAhead, - latestReadChapterIds: downloadAhead ? ids : [], }, options, ); diff --git a/src/screens/Reader.tsx b/src/screens/Reader.tsx index 94213b70..d992bb7f 100644 --- a/src/screens/Reader.tsx +++ b/src/screens/Reader.tsx @@ -39,32 +39,7 @@ import { useDebounce } from '@/util/useDebounce.ts'; import { UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts'; import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts'; import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts'; -import { FULL_CHAPTER_FIELDS } from '@/lib/graphql/Fragments.ts'; - -const isDupChapter = async (chapterIndex: number, currentChapter: TChapter) => { - const nextChapter = await requestManager.getChapter(currentChapter.manga.id, chapterIndex).response; - - return nextChapter.data.chapter.chapterNumber === currentChapter.chapterNumber; -}; - -/** - * In case duplicated chapters should be skipped the function will check all next/prev chapters until - * - a non duplicated chapter was found - * - no prev/next chapter exists => chapter request will fail and error will be raised up - */ -const getOffsetChapter = async ( - chapterIndex: number, - currentChapter: TChapter, - skipDupChapters: boolean, - offset: ChapterOffset, -): Promise => { - const shouldSkipChapter = skipDupChapters && (await isDupChapter(chapterIndex, currentChapter)); - if (shouldSkipChapter) { - return getOffsetChapter(chapterIndex + offset, currentChapter, skipDupChapters, offset); - } - - return chapterIndex; -}; +import { Chapters } from '@/lib/data/Chapters.ts'; const getReaderComponent = (readerType: ReaderType) => { switch (readerType) { @@ -184,38 +159,53 @@ export function Reader() { const { settings: metadataSettings } = useMetadataServerSettings(); + const prevChapters = useMemo( + () => + Chapters.getNextChapters(chapter, mangaChapters ?? [], { + offset: ChapterOffset.PREV, + skipDupe: settings.skipDupChapters, + }), + [chapter, mangaChapters, settings.skipDupChapters], + ); + const nextChapters = useMemo( + () => Chapters.getNextChapters(chapter, mangaChapters ?? [], { skipDupe: settings.skipDupChapters }), + [chapter, mangaChapters, settings.skipDupChapters], + ); + const prevChapter = useMemo( + () => + Chapters.getNextChapter(chapter, mangaChapters ?? [], { + offset: ChapterOffset.PREV, + skipDupe: settings.skipDupChapters, + }), + [chapter, mangaChapters, settings.skipDupChapters], + ); + const nextChapter = useMemo( + () => + Chapters.getNextChapter(chapter, mangaChapters ?? [], { + skipDupe: settings.skipDupChapters, + }), + [chapter, mangaChapters, settings.skipDupChapters], + ); + const updateChapter = (patch: UpdateChapterPatchInput) => { - const getChapterFromCache = (id: number) => - requestManager.graphQLClient.client.cache.readFragment({ - id: requestManager.graphQLClient.client.cache.identify({ - __typename: 'ChapterType', - id, - }), - fragment: FULL_CHAPTER_FIELDS, - fragmentName: 'FULL_CHAPTER_FIELDS', - }); - - const isAutoDeletionEnabled = !!patch.isRead && !!metadataSettings.deleteChaptersWhileReading; - const getChapterIdToDelete = () => { + const isAutoDeletionEnabled = !!patch.isRead && !!metadataSettings.deleteChaptersWhileReading; if (!isAutoDeletionEnabled || !mangaChapters) { return -1; } - const chapterToDeleteSourceOrder = Number(chapterIndex) - (metadataSettings.deleteChaptersWhileReading - 1); - const chapterToDelete = mangaChapters.find( - (mangaChapter) => mangaChapter.sourceOrder === chapterToDeleteSourceOrder, - ); + const chapterToDelete = [chapter, ...prevChapters][metadataSettings.deleteChaptersWhileReading - 1]; if (!chapterToDelete) { return -1; } - const chapterToDeleteUpToDateData = getChapterFromCache(chapterToDelete.id); + // chapter has to exist in the cache since the reader fetches all chapters of the manga + const chapterToDeleteUpToDateData = Chapters.getFromCache(chapterToDelete.id)!; const shouldDeleteChapter = - chapterToDeleteUpToDateData?.isDownloaded && - (!chapterToDeleteUpToDateData?.isBookmarked || metadataSettings.deleteChaptersWithBookmark); + chapterToDeleteUpToDateData.isRead && + Chapters.isAutoDeletable(chapterToDeleteUpToDateData, metadataSettings.deleteChaptersWithBookmark); if (!shouldDeleteChapter) { return -1; } @@ -223,23 +213,48 @@ export function Reader() { return chapterToDelete.id; }; - const currentChapter = getChapterFromCache(chapter.id); - const nextChapterId = mangaChapters?.[(mangaChapters?.length ?? 0) - Number(chapterIndex) - 1]?.id; - const nextChapter = nextChapterId ? getChapterFromCache(nextChapterId) : null; + const downloadAhead = () => { + const currentChapter = Chapters.getFromCache(chapter.id); - const shouldDownloadAhead = - isDownloadAheadEnabled && - chapter.manga.inLibrary && - !chapter.isRead && - !!patch.isRead && - !!currentChapter?.isDownloaded && - !!nextChapter?.isDownloaded; + const inDownloadRange = (patch.lastPageRead ?? 0) / chapter.pageCount > 0.25; + const shouldCheckDownloadAhead = + isDownloadAheadEnabled && chapter.manga.inLibrary && !!currentChapter?.isDownloaded && inDownloadRange; + + if (shouldCheckDownloadAhead) { + const nextChapterUpToDate = nextChapter ? Chapters.getFromCache(nextChapter.id) : null; + + if (!nextChapterUpToDate?.isDownloaded) { + return; + } + + const nextChaptersUpToDate = Chapters.getNonRead(nextChapters).map( + // the chapters have to be in the cache since the reader fetches the whole chapter list of the manga + (mangaChapter) => Chapters.getFromCache(mangaChapter.id)!, + ); + + const chapterIdsToDownload = nextChaptersUpToDate + // "settingsData" can't be undefined since this would not get executed otherwise + .slice(-settingsData!.settings.autoDownloadAheadLimit) + .filter((mangaChapter) => !mangaChapter.isDownloaded) + .map((mangaChapter) => mangaChapter.id) + .filter((id) => !Chapters.isDownloading(id)); + + if (!chapterIdsToDownload.length) { + return; + } + + Chapters.download(chapterIdsToDownload!).catch( + defaultPromiseErrorHandler('Reader::updateChapter: shouldDownloadAhead'), + ); + } + }; + + downloadAhead(); requestManager .updateChapter(chapter.id, { ...patch, chapterIdToDelete: getChapterIdToDelete(), - downloadAheadMangaId: shouldDownloadAhead ? chapter.manga.id : undefined, }) .response.catch(); }; @@ -256,9 +271,12 @@ export function Reader() { setRetrievingNextChapter(true); setCurPage(0); try { - setHistory( - await getOffsetChapter(chapter.sourceOrder + offset, chapter, settings.skipDupChapters, offset), - ); + const chapterToOpen = offset === ChapterOffset.NEXT ? nextChapter : prevChapter; + if (!chapterToOpen) { + throw new Error('Failed to find next chapter'); + } + + setHistory(chapterToOpen.sourceOrder); } catch (error) { const offsetToTranslationKeyMap: { [chapterOffset in ChapterOffset]: TranslationKey } = { [ChapterOffset.PREV]: 'reader.error.label.unable_to_get_prev_chapter_skip_dup', @@ -344,7 +362,7 @@ export function Reader() { }); }, [curPageDebounced, isDownloadAheadEnabled]); - const nextChapter = useCallback(() => { + const loadNextChapter = useCallback(() => { const doesNextChapterExist = chapter.sourceOrder < manga.chapters.totalCount; if (!doesNextChapterExist) { return; @@ -366,11 +384,11 @@ export function Reader() { manga.chapters.totalCount, chapter.pageCount, manga.id, - settings.skipDupChapters, isDownloadAheadEnabled, + nextChapter?.id, ]); - const prevChapter = useCallback(() => { + const loadPrevChapter = useCallback(() => { if (chapter.sourceOrder > 1) { openNextChapter(ChapterOffset.PREV, (prevChapterIndex) => navigate(`/manga/${manga.id}/chapter/${prevChapterIndex}`, { @@ -379,7 +397,7 @@ export function Reader() { }), ); } - }, [chapter.sourceOrder, manga.id, settings.skipDupChapters]); + }, [chapter.sourceOrder, manga.id, prevChapter?.id]); if (isLoading) { return ( @@ -431,8 +449,8 @@ export function Reader() { settings={settings} manga={manga} chapter={chapter} - nextChapter={nextChapter} - prevChapter={prevChapter} + nextChapter={loadNextChapter} + prevChapter={loadPrevChapter} /> ); diff --git a/src/screens/settings/DownloadSettings.tsx b/src/screens/settings/DownloadSettings.tsx index a5d085f2..007d436b 100644 --- a/src/screens/settings/DownloadSettings.tsx +++ b/src/screens/settings/DownloadSettings.tsx @@ -21,6 +21,7 @@ import { convertToGqlMeta, requestUpdateServerMetadata } from '@/util/metadata.t import { makeToast } from '@/components/util/Toast.tsx'; import { DeleteChaptersWhileReadingSetting } from '@/components/settings/downloads/DeleteChaptersWhileReadingSetting.tsx'; import { CategoriesInclusionSetting } from '@/components/settings/CategoriesInclusionSetting.tsx'; +import { NumberSetting } from '@/components/settings/NumberSetting.tsx'; type DownloadSettingsType = Pick< ServerSettings, @@ -100,7 +101,7 @@ export const DownloadSettings = () => { + {t('download.settings.delete_chapters.title')} } @@ -143,6 +144,29 @@ export const DownloadSettings = () => { onChange={(e) => updateSetting('autoDownloadNewChapters', e.target.checked)} /> + updateSetting('autoDownloadAheadLimit', downloadAheadLimit)} + /> ({ deleteChaptersManuallyMarkedRead: false, deleteChaptersWhileReading: 0, deleteChaptersWithBookmark: false, + downloadAheadLimit: 0, // library showAddToLibraryCategorySelectDialog: true,