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.
This commit is contained in:
@@ -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<TChapter, 'isDownloaded'>
|
||||
export type ChapterBookmarkInfo = ChapterIdInfo & Pick<TChapter, 'isBookmarked'>;
|
||||
export type ChapterReadInfo = ChapterIdInfo & Pick<TChapter, 'isRead'>;
|
||||
export type ChapterNumberInfo = ChapterIdInfo & Pick<TChapter, 'chapterNumber'>;
|
||||
export type ChapterScanlatorInfo = ChapterIdInfo & Pick<TChapter, 'scanlator'>;
|
||||
|
||||
export class Chapters {
|
||||
static getIds(chapters: { id: number }[]): number[] {
|
||||
return chapters.map((chapter) => chapter.id);
|
||||
}
|
||||
|
||||
static getFromCache<T>(
|
||||
id: number,
|
||||
fragment: DocumentNode = FULL_CHAPTER_FIELDS,
|
||||
fragmentName: string = 'FULL_CHAPTER_FIELDS',
|
||||
): T | null {
|
||||
return requestManager.graphQLClient.client.cache.readFragment<T>({
|
||||
id: requestManager.graphQLClient.client.cache.identify({
|
||||
__typename: 'ChapterType',
|
||||
id,
|
||||
}),
|
||||
fragment,
|
||||
fragmentName,
|
||||
});
|
||||
}
|
||||
|
||||
static isDownloading(id: number): boolean {
|
||||
return !!requestManager.graphQLClient.client.cache.readFragment<TChapter>({
|
||||
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<T extends ChapterScanlatorInfo & ChapterNumberInfo>(currentChapter: T, chapters: T[]): T[] {
|
||||
const chapterNumberToChapters = new Map<ChapterNumberInfo['chapterNumber'], T[]>();
|
||||
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<Chapter extends ChapterScanlatorInfo & ChapterNumberInfo & ChapterReadInfo>(
|
||||
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<Chapter extends ChapterScanlatorInfo & ChapterNumberInfo & ChapterReadInfo>(
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']> | Scalars['Int']['input'];
|
||||
deleteChapters: Scalars['Boolean']['input'];
|
||||
mangaIds: Array<Scalars['Int']['input']> | Scalars['Int']['input'];
|
||||
latestReadChapterIds: Array<Scalars['Int']['input']> | 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<ClearDownloaderInput>;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1762,25 +1762,21 @@ export class RequestManager {
|
||||
id: number,
|
||||
patch: UpdateChapterPatchInput & {
|
||||
chapterIdToDelete?: number;
|
||||
downloadAheadMangaId?: number;
|
||||
},
|
||||
options?: MutationOptions<UpdateChapterMutation, UpdateChapterMutationVariables>,
|
||||
): AbortableApolloMutationResponse<UpdateChapterMutation> {
|
||||
const { chapterIdToDelete = -1, downloadAheadMangaId = -1, ...updatePatch } = patch;
|
||||
const { chapterIdToDelete = -1, ...updatePatch } = patch;
|
||||
|
||||
return this.doRequest<UpdateChapterMutation, UpdateChapterMutationVariables>(
|
||||
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<UpdateChaptersMutation, UpdateChaptersMutationVariables>,
|
||||
): AbortableApolloMutationResponse<UpdateChaptersMutation> {
|
||||
const { chapterIdsToDelete = [], mangaIds = [], ...updatePatch } = patch;
|
||||
|
||||
const downloadAhead = !!mangaIds.length;
|
||||
const { chapterIdsToDelete = [], ...updatePatch } = patch;
|
||||
|
||||
return this.doRequest<UpdateChaptersMutation, UpdateChaptersMutationVariables>(
|
||||
GQLMethod.MUTATION,
|
||||
@@ -1826,9 +1820,6 @@ export class RequestManager {
|
||||
getLastPageRead: patch.lastPageRead != null,
|
||||
chapterIdsToDelete,
|
||||
deleteChapters: !!chapterIdsToDelete.length,
|
||||
mangaIds,
|
||||
downloadAhead,
|
||||
latestReadChapterIds: downloadAhead ? ids : [],
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user