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:
@@ -8,10 +8,12 @@
|
|||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { List, ListItem, ListItemText, Switch } from '@mui/material';
|
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 { NumberSetting } from '@/components/settings/NumberSetting.tsx';
|
||||||
import { getPersistedServerSetting, usePersistedValue } from '@/util/usePersistedValue.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 MIN_LIMIT = 2;
|
||||||
const MAX_LIMIT = 10;
|
const MAX_LIMIT = 10;
|
||||||
@@ -20,10 +22,12 @@ const DEFAULT_LIMIT = MIN_LIMIT;
|
|||||||
export const DownloadAheadSetting = () => {
|
export const DownloadAheadSetting = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { data } = requestManager.useGetServerSettings();
|
const {
|
||||||
const downloadAheadLimit = data?.settings.autoDownloadAheadLimit;
|
metadata,
|
||||||
|
settings: { downloadAheadLimit },
|
||||||
|
} = useMetadataServerSettings();
|
||||||
|
|
||||||
const shouldDownloadAhead = !!downloadAheadLimit;
|
const shouldDownloadAhead = !!downloadAheadLimit;
|
||||||
const [mutateSettings] = requestManager.useUpdateServerSettings();
|
|
||||||
const [currentDownloadAheadLimit, persistDownloadAheadLimit] = usePersistedValue(
|
const [currentDownloadAheadLimit, persistDownloadAheadLimit] = usePersistedValue(
|
||||||
'lastDownloadAheadLimit',
|
'lastDownloadAheadLimit',
|
||||||
DEFAULT_LIMIT,
|
DEFAULT_LIMIT,
|
||||||
@@ -31,15 +35,12 @@ export const DownloadAheadSetting = () => {
|
|||||||
getPersistedServerSetting,
|
getPersistedServerSetting,
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateSetting = useCallback(
|
const updateSetting = (value: MetadataServerSettings['downloadAheadLimit']) => {
|
||||||
(autoDownloadAheadLimit: number) => {
|
persistDownloadAheadLimit(value === 0 ? currentDownloadAheadLimit : value);
|
||||||
persistDownloadAheadLimit(
|
requestUpdateServerMetadata(convertToGqlMeta(metadata)! ?? {}, [['downloadAheadLimit', value]]).catch(() =>
|
||||||
autoDownloadAheadLimit === 0 ? currentDownloadAheadLimit : autoDownloadAheadLimit,
|
makeToast(t('search.error.label.failed_to_save_settings'), 'warning'),
|
||||||
);
|
);
|
||||||
mutateSettings({ variables: { input: { settings: { autoDownloadAheadLimit } } } });
|
};
|
||||||
},
|
|
||||||
[currentDownloadAheadLimit],
|
|
||||||
);
|
|
||||||
|
|
||||||
const setDoAutoUpdates = (enable: boolean) => {
|
const setDoAutoUpdates = (enable: boolean) => {
|
||||||
const globalUpdateInterval = enable ? currentDownloadAheadLimit : 0;
|
const globalUpdateInterval = enable ? currentDownloadAheadLimit : 0;
|
||||||
@@ -68,7 +69,6 @@ export const DownloadAheadSetting = () => {
|
|||||||
defaultValue={DEFAULT_LIMIT}
|
defaultValue={DEFAULT_LIMIT}
|
||||||
showSlider
|
showSlider
|
||||||
dialogDescription={t('download.settings.download_ahead.label.description')}
|
dialogDescription={t('download.settings.download_ahead.label.description')}
|
||||||
dialogDisclaimer={t('download.settings.download_ahead.label.disclaimer')}
|
|
||||||
valueUnit={t('chapter.title')}
|
valueUnit={t('chapter.title')}
|
||||||
handleUpdate={updateSetting}
|
handleUpdate={updateSetting}
|
||||||
disabled={!shouldDownloadAhead}
|
disabled={!shouldDownloadAhead}
|
||||||
|
|||||||
@@ -167,6 +167,12 @@
|
|||||||
"include_in_download": "Entries in excluded categories will not be downloaded even if they are also in included categories"
|
"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": {
|
"label": {
|
||||||
"ignore_with_unread_chapters": "Ignore automatic chapter downloads for entries with unread chapters",
|
"ignore_with_unread_chapters": "Ignore automatic chapter downloads for entries with unread chapters",
|
||||||
"new_chapters": "Download new chapters"
|
"new_chapters": "Download new chapters"
|
||||||
@@ -197,7 +203,6 @@
|
|||||||
"download_ahead": {
|
"download_ahead": {
|
||||||
"label": {
|
"label": {
|
||||||
"description": "How many chapters should get downloaded when marking a chapter as read while reading.",
|
"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",
|
"unread_chapters_to_download": "Number of unread chapters to download",
|
||||||
"value": "{{chapters}} $t(chapter.title)",
|
"value": "{{chapters}} $t(chapter.title)",
|
||||||
"while_reading": "Auto download while reading"
|
"while_reading": "Auto download while reading"
|
||||||
|
|||||||
@@ -7,10 +7,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { t as translate } from 'i18next';
|
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 { makeToast } from '@/components/util/Toast.tsx';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { getMetadataServerSettings } from '@/util/metadataServerSettings.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';
|
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 ChapterBookmarkInfo = ChapterIdInfo & Pick<TChapter, 'isBookmarked'>;
|
||||||
export type ChapterReadInfo = ChapterIdInfo & Pick<TChapter, 'isRead'>;
|
export type ChapterReadInfo = ChapterIdInfo & Pick<TChapter, 'isRead'>;
|
||||||
export type ChapterNumberInfo = ChapterIdInfo & Pick<TChapter, 'chapterNumber'>;
|
export type ChapterNumberInfo = ChapterIdInfo & Pick<TChapter, 'chapterNumber'>;
|
||||||
|
export type ChapterScanlatorInfo = ChapterIdInfo & Pick<TChapter, 'scanlator'>;
|
||||||
|
|
||||||
export class Chapters {
|
export class Chapters {
|
||||||
static getIds(chapters: { id: number }[]): number[] {
|
static getIds(chapters: { id: number }[]): number[] {
|
||||||
return chapters.map((chapter) => chapter.id);
|
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 {
|
static isDownloaded({ isDownloaded }: ChapterDownloadInfo): boolean {
|
||||||
return isDownloaded;
|
return isDownloaded;
|
||||||
}
|
}
|
||||||
@@ -258,4 +297,60 @@ export class Chapters {
|
|||||||
throw new Error(`Chapters::performAction: unknown action "${action}"`);
|
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'];
|
getBookmarked: Scalars['Boolean']['input'];
|
||||||
getRead: Scalars['Boolean']['input'];
|
getRead: Scalars['Boolean']['input'];
|
||||||
getLastPageRead: Scalars['Boolean']['input'];
|
getLastPageRead: Scalars['Boolean']['input'];
|
||||||
id: Scalars['Int']['input'];
|
|
||||||
chapterIdToDelete: Scalars['Int']['input'];
|
chapterIdToDelete: Scalars['Int']['input'];
|
||||||
deleteChapter: Scalars['Boolean']['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<{
|
export type UpdateChaptersMutationVariables = Exact<{
|
||||||
input: UpdateChaptersInput;
|
input: UpdateChaptersInput;
|
||||||
@@ -2625,13 +2622,10 @@ export type UpdateChaptersMutationVariables = Exact<{
|
|||||||
getLastPageRead: Scalars['Boolean']['input'];
|
getLastPageRead: Scalars['Boolean']['input'];
|
||||||
chapterIdsToDelete: Array<Scalars['Int']['input']> | Scalars['Int']['input'];
|
chapterIdsToDelete: Array<Scalars['Int']['input']> | Scalars['Int']['input'];
|
||||||
deleteChapters: Scalars['Boolean']['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<{
|
export type ClearDownloaderMutationVariables = Exact<{
|
||||||
input?: InputMaybe<ClearDownloaderInput>;
|
input?: InputMaybe<ClearDownloaderInput>;
|
||||||
|
|||||||
@@ -93,11 +93,8 @@ export const UPDATE_CHAPTER = gql`
|
|||||||
$getBookmarked: Boolean!
|
$getBookmarked: Boolean!
|
||||||
$getRead: Boolean!
|
$getRead: Boolean!
|
||||||
$getLastPageRead: Boolean!
|
$getLastPageRead: Boolean!
|
||||||
$id: Int!
|
|
||||||
$chapterIdToDelete: Int!
|
$chapterIdToDelete: Int!
|
||||||
$deleteChapter: Boolean!
|
$deleteChapter: Boolean!
|
||||||
$mangaId: Int!
|
|
||||||
$downloadAhead: Boolean!
|
|
||||||
) {
|
) {
|
||||||
updateChapter(input: $input) {
|
updateChapter(input: $input) {
|
||||||
clientMutationId
|
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!
|
$getLastPageRead: Boolean!
|
||||||
$chapterIdsToDelete: [Int!]!
|
$chapterIdsToDelete: [Int!]!
|
||||||
$deleteChapters: Boolean!
|
$deleteChapters: Boolean!
|
||||||
$mangaIds: [Int!]!
|
|
||||||
$latestReadChapterIds: [Int!]!
|
|
||||||
$downloadAhead: Boolean!
|
|
||||||
) {
|
) {
|
||||||
updateChapters(input: $input) {
|
updateChapters(input: $input) {
|
||||||
clientMutationId
|
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,
|
id: number,
|
||||||
patch: UpdateChapterPatchInput & {
|
patch: UpdateChapterPatchInput & {
|
||||||
chapterIdToDelete?: number;
|
chapterIdToDelete?: number;
|
||||||
downloadAheadMangaId?: number;
|
|
||||||
},
|
},
|
||||||
options?: MutationOptions<UpdateChapterMutation, UpdateChapterMutationVariables>,
|
options?: MutationOptions<UpdateChapterMutation, UpdateChapterMutationVariables>,
|
||||||
): AbortableApolloMutationResponse<UpdateChapterMutation> {
|
): AbortableApolloMutationResponse<UpdateChapterMutation> {
|
||||||
const { chapterIdToDelete = -1, downloadAheadMangaId = -1, ...updatePatch } = patch;
|
const { chapterIdToDelete = -1, ...updatePatch } = patch;
|
||||||
|
|
||||||
return this.doRequest<UpdateChapterMutation, UpdateChapterMutationVariables>(
|
return this.doRequest<UpdateChapterMutation, UpdateChapterMutationVariables>(
|
||||||
GQLMethod.MUTATION,
|
GQLMethod.MUTATION,
|
||||||
UPDATE_CHAPTER,
|
UPDATE_CHAPTER,
|
||||||
{
|
{
|
||||||
id,
|
|
||||||
input: { id, patch: updatePatch },
|
input: { id, patch: updatePatch },
|
||||||
getBookmarked: patch.isBookmarked != null,
|
getBookmarked: patch.isBookmarked != null,
|
||||||
getRead: patch.isRead != null,
|
getRead: patch.isRead != null,
|
||||||
getLastPageRead: patch.lastPageRead != null,
|
getLastPageRead: patch.lastPageRead != null,
|
||||||
chapterIdToDelete,
|
chapterIdToDelete,
|
||||||
deleteChapter: chapterIdToDelete >= 0,
|
deleteChapter: chapterIdToDelete >= 0,
|
||||||
mangaId: downloadAheadMangaId,
|
|
||||||
downloadAhead: downloadAheadMangaId !== -1,
|
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
@@ -1809,12 +1805,10 @@ export class RequestManager {
|
|||||||
|
|
||||||
public updateChapters(
|
public updateChapters(
|
||||||
ids: number[],
|
ids: number[],
|
||||||
patch: UpdateChapterPatchInput & { chapterIdsToDelete?: number[]; mangaIds?: number[] },
|
patch: UpdateChapterPatchInput & { chapterIdsToDelete?: number[] },
|
||||||
options?: MutationOptions<UpdateChaptersMutation, UpdateChaptersMutationVariables>,
|
options?: MutationOptions<UpdateChaptersMutation, UpdateChaptersMutationVariables>,
|
||||||
): AbortableApolloMutationResponse<UpdateChaptersMutation> {
|
): AbortableApolloMutationResponse<UpdateChaptersMutation> {
|
||||||
const { chapterIdsToDelete = [], mangaIds = [], ...updatePatch } = patch;
|
const { chapterIdsToDelete = [], ...updatePatch } = patch;
|
||||||
|
|
||||||
const downloadAhead = !!mangaIds.length;
|
|
||||||
|
|
||||||
return this.doRequest<UpdateChaptersMutation, UpdateChaptersMutationVariables>(
|
return this.doRequest<UpdateChaptersMutation, UpdateChaptersMutationVariables>(
|
||||||
GQLMethod.MUTATION,
|
GQLMethod.MUTATION,
|
||||||
@@ -1826,9 +1820,6 @@ export class RequestManager {
|
|||||||
getLastPageRead: patch.lastPageRead != null,
|
getLastPageRead: patch.lastPageRead != null,
|
||||||
chapterIdsToDelete,
|
chapterIdsToDelete,
|
||||||
deleteChapters: !!chapterIdsToDelete.length,
|
deleteChapters: !!chapterIdsToDelete.length,
|
||||||
mangaIds,
|
|
||||||
downloadAhead,
|
|
||||||
latestReadChapterIds: downloadAhead ? ids : [],
|
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,32 +39,7 @@ import { useDebounce } from '@/util/useDebounce.ts';
|
|||||||
import { UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
import { UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
import { useMetadataServerSettings } from '@/util/metadataServerSettings.ts';
|
||||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||||
import { FULL_CHAPTER_FIELDS } from '@/lib/graphql/Fragments.ts';
|
import { Chapters } from '@/lib/data/Chapters.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<number> => {
|
|
||||||
const shouldSkipChapter = skipDupChapters && (await isDupChapter(chapterIndex, currentChapter));
|
|
||||||
if (shouldSkipChapter) {
|
|
||||||
return getOffsetChapter(chapterIndex + offset, currentChapter, skipDupChapters, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapterIndex;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getReaderComponent = (readerType: ReaderType) => {
|
const getReaderComponent = (readerType: ReaderType) => {
|
||||||
switch (readerType) {
|
switch (readerType) {
|
||||||
@@ -184,38 +159,53 @@ export function Reader() {
|
|||||||
|
|
||||||
const { settings: metadataSettings } = useMetadataServerSettings();
|
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 updateChapter = (patch: UpdateChapterPatchInput) => {
|
||||||
const getChapterFromCache = (id: number) =>
|
|
||||||
requestManager.graphQLClient.client.cache.readFragment<TChapter>({
|
|
||||||
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 getChapterIdToDelete = () => {
|
||||||
|
const isAutoDeletionEnabled = !!patch.isRead && !!metadataSettings.deleteChaptersWhileReading;
|
||||||
if (!isAutoDeletionEnabled || !mangaChapters) {
|
if (!isAutoDeletionEnabled || !mangaChapters) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chapterToDeleteSourceOrder = Number(chapterIndex) - (metadataSettings.deleteChaptersWhileReading - 1);
|
const chapterToDelete = [chapter, ...prevChapters][metadataSettings.deleteChaptersWhileReading - 1];
|
||||||
const chapterToDelete = mangaChapters.find(
|
|
||||||
(mangaChapter) => mangaChapter.sourceOrder === chapterToDeleteSourceOrder,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!chapterToDelete) {
|
if (!chapterToDelete) {
|
||||||
return -1;
|
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<TChapter>(chapterToDelete.id)!;
|
||||||
|
|
||||||
const shouldDeleteChapter =
|
const shouldDeleteChapter =
|
||||||
chapterToDeleteUpToDateData?.isDownloaded &&
|
chapterToDeleteUpToDateData.isRead &&
|
||||||
(!chapterToDeleteUpToDateData?.isBookmarked || metadataSettings.deleteChaptersWithBookmark);
|
Chapters.isAutoDeletable(chapterToDeleteUpToDateData, metadataSettings.deleteChaptersWithBookmark);
|
||||||
if (!shouldDeleteChapter) {
|
if (!shouldDeleteChapter) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -223,23 +213,48 @@ export function Reader() {
|
|||||||
return chapterToDelete.id;
|
return chapterToDelete.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentChapter = getChapterFromCache(chapter.id);
|
const downloadAhead = () => {
|
||||||
const nextChapterId = mangaChapters?.[(mangaChapters?.length ?? 0) - Number(chapterIndex) - 1]?.id;
|
const currentChapter = Chapters.getFromCache<TChapter>(chapter.id);
|
||||||
const nextChapter = nextChapterId ? getChapterFromCache(nextChapterId) : null;
|
|
||||||
|
|
||||||
const shouldDownloadAhead =
|
const inDownloadRange = (patch.lastPageRead ?? 0) / chapter.pageCount > 0.25;
|
||||||
isDownloadAheadEnabled &&
|
const shouldCheckDownloadAhead =
|
||||||
chapter.manga.inLibrary &&
|
isDownloadAheadEnabled && chapter.manga.inLibrary && !!currentChapter?.isDownloaded && inDownloadRange;
|
||||||
!chapter.isRead &&
|
|
||||||
!!patch.isRead &&
|
if (shouldCheckDownloadAhead) {
|
||||||
!!currentChapter?.isDownloaded &&
|
const nextChapterUpToDate = nextChapter ? Chapters.getFromCache<TChapter>(nextChapter.id) : null;
|
||||||
!!nextChapter?.isDownloaded;
|
|
||||||
|
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<TChapter>(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
|
requestManager
|
||||||
.updateChapter(chapter.id, {
|
.updateChapter(chapter.id, {
|
||||||
...patch,
|
...patch,
|
||||||
chapterIdToDelete: getChapterIdToDelete(),
|
chapterIdToDelete: getChapterIdToDelete(),
|
||||||
downloadAheadMangaId: shouldDownloadAhead ? chapter.manga.id : undefined,
|
|
||||||
})
|
})
|
||||||
.response.catch();
|
.response.catch();
|
||||||
};
|
};
|
||||||
@@ -256,9 +271,12 @@ export function Reader() {
|
|||||||
setRetrievingNextChapter(true);
|
setRetrievingNextChapter(true);
|
||||||
setCurPage(0);
|
setCurPage(0);
|
||||||
try {
|
try {
|
||||||
setHistory(
|
const chapterToOpen = offset === ChapterOffset.NEXT ? nextChapter : prevChapter;
|
||||||
await getOffsetChapter(chapter.sourceOrder + offset, chapter, settings.skipDupChapters, offset),
|
if (!chapterToOpen) {
|
||||||
);
|
throw new Error('Failed to find next chapter');
|
||||||
|
}
|
||||||
|
|
||||||
|
setHistory(chapterToOpen.sourceOrder);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const offsetToTranslationKeyMap: { [chapterOffset in ChapterOffset]: TranslationKey } = {
|
const offsetToTranslationKeyMap: { [chapterOffset in ChapterOffset]: TranslationKey } = {
|
||||||
[ChapterOffset.PREV]: 'reader.error.label.unable_to_get_prev_chapter_skip_dup',
|
[ChapterOffset.PREV]: 'reader.error.label.unable_to_get_prev_chapter_skip_dup',
|
||||||
@@ -344,7 +362,7 @@ export function Reader() {
|
|||||||
});
|
});
|
||||||
}, [curPageDebounced, isDownloadAheadEnabled]);
|
}, [curPageDebounced, isDownloadAheadEnabled]);
|
||||||
|
|
||||||
const nextChapter = useCallback(() => {
|
const loadNextChapter = useCallback(() => {
|
||||||
const doesNextChapterExist = chapter.sourceOrder < manga.chapters.totalCount;
|
const doesNextChapterExist = chapter.sourceOrder < manga.chapters.totalCount;
|
||||||
if (!doesNextChapterExist) {
|
if (!doesNextChapterExist) {
|
||||||
return;
|
return;
|
||||||
@@ -366,11 +384,11 @@ export function Reader() {
|
|||||||
manga.chapters.totalCount,
|
manga.chapters.totalCount,
|
||||||
chapter.pageCount,
|
chapter.pageCount,
|
||||||
manga.id,
|
manga.id,
|
||||||
settings.skipDupChapters,
|
|
||||||
isDownloadAheadEnabled,
|
isDownloadAheadEnabled,
|
||||||
|
nextChapter?.id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const prevChapter = useCallback(() => {
|
const loadPrevChapter = useCallback(() => {
|
||||||
if (chapter.sourceOrder > 1) {
|
if (chapter.sourceOrder > 1) {
|
||||||
openNextChapter(ChapterOffset.PREV, (prevChapterIndex) =>
|
openNextChapter(ChapterOffset.PREV, (prevChapterIndex) =>
|
||||||
navigate(`/manga/${manga.id}/chapter/${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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -431,8 +449,8 @@ export function Reader() {
|
|||||||
settings={settings}
|
settings={settings}
|
||||||
manga={manga}
|
manga={manga}
|
||||||
chapter={chapter}
|
chapter={chapter}
|
||||||
nextChapter={nextChapter}
|
nextChapter={loadNextChapter}
|
||||||
prevChapter={prevChapter}
|
prevChapter={loadPrevChapter}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { convertToGqlMeta, requestUpdateServerMetadata } from '@/util/metadata.t
|
|||||||
import { makeToast } from '@/components/util/Toast.tsx';
|
import { makeToast } from '@/components/util/Toast.tsx';
|
||||||
import { DeleteChaptersWhileReadingSetting } from '@/components/settings/downloads/DeleteChaptersWhileReadingSetting.tsx';
|
import { DeleteChaptersWhileReadingSetting } from '@/components/settings/downloads/DeleteChaptersWhileReadingSetting.tsx';
|
||||||
import { CategoriesInclusionSetting } from '@/components/settings/CategoriesInclusionSetting.tsx';
|
import { CategoriesInclusionSetting } from '@/components/settings/CategoriesInclusionSetting.tsx';
|
||||||
|
import { NumberSetting } from '@/components/settings/NumberSetting.tsx';
|
||||||
|
|
||||||
type DownloadSettingsType = Pick<
|
type DownloadSettingsType = Pick<
|
||||||
ServerSettings,
|
ServerSettings,
|
||||||
@@ -100,7 +101,7 @@ export const DownloadSettings = () => {
|
|||||||
</ListItem>
|
</ListItem>
|
||||||
<List
|
<List
|
||||||
subheader={
|
subheader={
|
||||||
<ListSubheader component="div" id="download-settings-auto-download">
|
<ListSubheader component="div" id="download-settings-auto-delete-downloads">
|
||||||
{t('download.settings.delete_chapters.title')}
|
{t('download.settings.delete_chapters.title')}
|
||||||
</ListSubheader>
|
</ListSubheader>
|
||||||
}
|
}
|
||||||
@@ -143,6 +144,29 @@ export const DownloadSettings = () => {
|
|||||||
onChange={(e) => updateSetting('autoDownloadNewChapters', e.target.checked)}
|
onChange={(e) => updateSetting('autoDownloadNewChapters', e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
<NumberSetting
|
||||||
|
disabled={!downloadSettings?.autoDownloadNewChapters}
|
||||||
|
settingTitle={t('download.settings.auto_download.download_limit.label.title')}
|
||||||
|
dialogDescription={t('download.settings.auto_download.download_limit.label.description')}
|
||||||
|
value={downloadSettings?.autoDownloadAheadLimit ?? 0}
|
||||||
|
settingValue={
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
downloadSettings?.autoDownloadAheadLimit !== undefined
|
||||||
|
? !downloadSettings.autoDownloadAheadLimit
|
||||||
|
? t('global.label.none')
|
||||||
|
: t('download.settings.download_ahead.label.value', {
|
||||||
|
chapters: downloadSettings.autoDownloadAheadLimit,
|
||||||
|
count: downloadSettings.autoDownloadAheadLimit,
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
defaultValue={0}
|
||||||
|
minValue={0}
|
||||||
|
maxValue={20}
|
||||||
|
showSlider
|
||||||
|
valueUnit={t('chapter.title')}
|
||||||
|
handleUpdate={(downloadAheadLimit) => updateSetting('autoDownloadAheadLimit', downloadAheadLimit)}
|
||||||
|
/>
|
||||||
<ListItem disabled={!downloadSettings?.autoDownloadNewChapters}>
|
<ListItem disabled={!downloadSettings?.autoDownloadNewChapters}>
|
||||||
<ListItemText primary={t('download.settings.auto_download.label.ignore_with_unread_chapters')} />
|
<ListItemText primary={t('download.settings.auto_download.label.ignore_with_unread_chapters')} />
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ export type MetadataServerSettings = {
|
|||||||
deleteChaptersManuallyMarkedRead: boolean;
|
deleteChaptersManuallyMarkedRead: boolean;
|
||||||
deleteChaptersWhileReading: number;
|
deleteChaptersWhileReading: number;
|
||||||
deleteChaptersWithBookmark: boolean;
|
deleteChaptersWithBookmark: boolean;
|
||||||
|
downloadAheadLimit: number;
|
||||||
|
|
||||||
// library
|
// library
|
||||||
showAddToLibraryCategorySelectDialog: boolean;
|
showAddToLibraryCategorySelectDialog: boolean;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const getDefaultSettings = (): MetadataServerSettings => ({
|
|||||||
deleteChaptersManuallyMarkedRead: false,
|
deleteChaptersManuallyMarkedRead: false,
|
||||||
deleteChaptersWhileReading: 0,
|
deleteChaptersWhileReading: 0,
|
||||||
deleteChaptersWithBookmark: false,
|
deleteChaptersWithBookmark: false,
|
||||||
|
downloadAheadLimit: 0,
|
||||||
|
|
||||||
// library
|
// library
|
||||||
showAddToLibraryCategorySelectDialog: true,
|
showAddToLibraryCategorySelectDialog: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user