Require manga and chapter action confirmations

closes #927
This commit is contained in:
schroda
2025-06-28 02:25:49 +02:00
parent 592ca7881e
commit 7702edb02f
6 changed files with 233 additions and 123 deletions

View File

@@ -59,6 +59,8 @@
}, },
"label": { "label": {
"action": "Remove bookmark", "action": "Remove bookmark",
"confirmation_one": "You are about to remove one bookmark",
"confirmation_other": "You are about to remove {{count}} bookmarks",
"error_one": "Could not remove the bookmark", "error_one": "Could not remove the bookmark",
"error_other": "Could not remove the bookmarks", "error_other": "Could not remove the bookmarks",
"success_one": "Chapter bookmark removed", "success_one": "Chapter bookmark removed",
@@ -75,6 +77,8 @@
"action": "Download", "action": "Download",
"ahead": "$t(download.settings.download_ahead.title) ({{count}})", "ahead": "$t(download.settings.download_ahead.title) ({{count}})",
"all": "$t(extension.language.all)", "all": "$t(extension.language.all)",
"confirmation_one": "You are about to download one chapter",
"confirmation_other": "You are about to download {{count}} chapters.\nSuwayomi is not a mass downloader and too many downloads can get you banned from sources and/or cause performance issues.",
"error_one": "Could not add the download", "error_one": "Could not add the download",
"error_other": "Could not add downloads", "error_other": "Could not add downloads",
"next": "$t(reader.button.next_chapter)", "next": "$t(reader.button.next_chapter)",
@@ -92,6 +96,8 @@
}, },
"label": { "label": {
"action": "Delete", "action": "Delete",
"confirmation_one": "You are about to delete one download",
"confirmation_other": "You are about to delete {{count}} downloads",
"error_one": "Could not delete the chapter", "error_one": "Could not delete the chapter",
"error_other": "Could not delete chapters", "error_other": "Could not delete chapters",
"success_one": "Chapter deleted", "success_one": "Chapter deleted",
@@ -113,6 +119,8 @@
"current": "Mark as read", "current": "Mark as read",
"previous": "Mark previous as read" "previous": "Mark previous as read"
}, },
"confirmation_one": "You are about to mark one chapter as read",
"confirmation_other": "You are about to mark {{count}} chapters as read",
"error_one": "Could not mark the chapter as read", "error_one": "Could not mark the chapter as read",
"error_other": "Could not mark chapters as read", "error_other": "Could not mark chapters as read",
"success_one": "Chapter marked as read", "success_one": "Chapter marked as read",
@@ -125,6 +133,8 @@
}, },
"label": { "label": {
"action": "Mark as unread", "action": "Mark as unread",
"confirmation_one": "You are about to mark one chapter as unread",
"confirmation_other": "You are about to mark {{count}} chapters as unread",
"error_one": "Could not mark the chapter as unread", "error_one": "Could not mark the chapter as unread",
"error_other": "Could not mark chapters as unread", "error_other": "Could not mark chapters as unread",
"success_one": "Chapter marked as unread", "success_one": "Chapter marked as unread",
@@ -720,6 +730,8 @@
}, },
"label": { "label": {
"action": "Change categories", "action": "Change categories",
"confirmation_one": "You are about to change the category of one entry",
"confirmation_other": "You are about to change the category of {{count}} entries",
"error_one": "Could not change the categories of the manga", "error_one": "Could not change the categories of the manga",
"error_other": "Could not change the categories of the manga", "error_other": "Could not change the categories of the manga",
"success_one": "Changed categories of manga", "success_one": "Changed categories of manga",
@@ -748,6 +760,8 @@
}, },
"label": { "label": {
"action": "Remove from the library", "action": "Remove from the library",
"confirmation_one": "You are about to remove one entry from your library",
"confirmation_other": "You are about to remove {{count}} entries from your library",
"error_one": "Could not remove manga from the library", "error_one": "Could not remove manga from the library",
"error_other": "Could not remove manga from the library", "error_other": "Could not remove manga from the library",
"success_one": "Removed manga from the library", "success_one": "Removed manga from the library",

View File

@@ -28,12 +28,25 @@ export const CHAPTER_SORT_OPTIONS_TO_TRANSLATION_KEY: Record<ChapterSortMode, Tr
fetchedAt: 'global.sort.label.by_fetch_date', fetchedAt: 'global.sort.label.by_fetch_date',
}; };
export const CHAPTER_ACTION_TO_CONFIRMATION_REQUIRED: Record<
ChapterAction,
{ always: boolean; bulkAction: boolean; bulkActionCountForce?: number }
> = {
download: { always: false, bulkAction: false, bulkActionCountForce: 300 },
delete: { always: true, bulkAction: true },
bookmark: { always: false, bulkAction: false },
unbookmark: { always: false, bulkAction: true },
mark_as_read: { always: false, bulkAction: true },
mark_as_unread: { always: false, bulkAction: true },
};
export const CHAPTER_ACTION_TO_TRANSLATION: { export const CHAPTER_ACTION_TO_TRANSLATION: {
[key in ChapterAction]: { [key in ChapterAction]: {
action: { action: {
single: TranslationKey; single: TranslationKey;
selected: TranslationKey; selected: TranslationKey;
}; };
confirmation?: TranslationKey;
success: TranslationKey; success: TranslationKey;
error: TranslationKey; error: TranslationKey;
}; };
@@ -43,6 +56,7 @@ export const CHAPTER_ACTION_TO_TRANSLATION: {
single: 'chapter.action.download.add.label.action', single: 'chapter.action.download.add.label.action',
selected: 'chapter.action.download.add.button.selected', selected: 'chapter.action.download.add.button.selected',
}, },
confirmation: 'chapter.action.download.add.label.confirmation',
success: 'chapter.action.download.add.label.success', success: 'chapter.action.download.add.label.success',
error: 'chapter.action.download.add.label.error', error: 'chapter.action.download.add.label.error',
}, },
@@ -51,6 +65,7 @@ export const CHAPTER_ACTION_TO_TRANSLATION: {
single: 'chapter.action.download.delete.label.action', single: 'chapter.action.download.delete.label.action',
selected: 'chapter.action.download.delete.button.selected', selected: 'chapter.action.download.delete.button.selected',
}, },
confirmation: 'chapter.action.download.delete.label.confirmation',
success: 'chapter.action.download.delete.label.success', success: 'chapter.action.download.delete.label.success',
error: 'chapter.action.download.delete.label.error', error: 'chapter.action.download.delete.label.error',
}, },
@@ -67,6 +82,7 @@ export const CHAPTER_ACTION_TO_TRANSLATION: {
single: 'chapter.action.bookmark.remove.label.action', single: 'chapter.action.bookmark.remove.label.action',
selected: 'chapter.action.bookmark.remove.button.selected', selected: 'chapter.action.bookmark.remove.button.selected',
}, },
confirmation: 'chapter.action.bookmark.remove.label.confirmation',
success: 'chapter.action.bookmark.remove.label.success', success: 'chapter.action.bookmark.remove.label.success',
error: 'chapter.action.bookmark.remove.label.error', error: 'chapter.action.bookmark.remove.label.error',
}, },
@@ -75,6 +91,7 @@ export const CHAPTER_ACTION_TO_TRANSLATION: {
single: 'chapter.action.mark_as_read.add.label.action.current', single: 'chapter.action.mark_as_read.add.label.action.current',
selected: 'chapter.action.mark_as_read.add.button.selected', selected: 'chapter.action.mark_as_read.add.button.selected',
}, },
confirmation: 'chapter.action.mark_as_read.add.label.confirmation',
success: 'chapter.action.mark_as_read.add.label.success', success: 'chapter.action.mark_as_read.add.label.success',
error: 'chapter.action.mark_as_read.add.label.error', error: 'chapter.action.mark_as_read.add.label.error',
}, },
@@ -83,6 +100,7 @@ export const CHAPTER_ACTION_TO_TRANSLATION: {
single: 'chapter.action.mark_as_read.remove.label.action', single: 'chapter.action.mark_as_read.remove.label.action',
selected: 'chapter.action.mark_as_read.remove.button.selected', selected: 'chapter.action.mark_as_read.remove.button.selected',
}, },
confirmation: 'chapter.action.mark_as_read.remove.label.confirmation',
success: 'chapter.action.mark_as_read.remove.label.success', success: 'chapter.action.mark_as_read.remove.label.success',
error: 'chapter.action.mark_as_read.remove.label.error', error: 'chapter.action.mark_as_read.remove.label.error',
}, },

View File

@@ -26,7 +26,10 @@ import { AppRoutes } from '@/modules/core/AppRoute.constants.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { DOWNLOAD_TYPE_FIELDS } from '@/lib/graphql/fragments/DownloadFragments.ts'; import { DOWNLOAD_TYPE_FIELDS } from '@/lib/graphql/fragments/DownloadFragments.ts';
import { epochToDate, getDateString } from '@/util/DateHelper.ts'; import { epochToDate, getDateString } from '@/util/DateHelper.ts';
import { CHAPTER_ACTION_TO_TRANSLATION } from '@/modules/chapter/Chapter.constants.ts'; import {
CHAPTER_ACTION_TO_CONFIRMATION_REQUIRED,
CHAPTER_ACTION_TO_TRANSLATION,
} from '@/modules/chapter/Chapter.constants.ts';
import { import {
ChapterAction, ChapterAction,
ChapterBookmarkInfo, ChapterBookmarkInfo,
@@ -38,6 +41,8 @@ import {
ChapterScanlatorInfo, ChapterScanlatorInfo,
ChapterSourceOrderInfo, ChapterSourceOrderInfo,
} from '@/modules/chapter/Chapter.types.ts'; } from '@/modules/chapter/Chapter.types.ts';
import { assertIsDefined } from '@/Asserts.ts';
import { awaitConfirmation } from '@/modules/core/utils/AwaitableDialog.tsx';
export class Chapters { export class Chapters {
static getIds(chapters: { id: number }[]): number[] { static getIds(chapters: { id: number }[]): number[] {
@@ -183,19 +188,21 @@ export class Chapters {
.filter((matchingChapters): matchingChapters is [Chapter, Chapter] => matchingChapters !== null); .filter((matchingChapters): matchingChapters is [Chapter, Chapter] => matchingChapters !== null);
} }
static async download(chapterIds: number[]): Promise<void> { static async download(chapterIds: number[], disableConfirmation?: boolean): Promise<void> {
return Chapters.executeAction( return Chapters.executeAction(
'download', 'download',
chapterIds.length, chapterIds.length,
() => requestManager.addChaptersToDownloadQueue(chapterIds).response, () => requestManager.addChaptersToDownloadQueue(chapterIds).response,
disableConfirmation,
); );
} }
static async delete(chapterIds: number[]): Promise<void> { static async delete(chapterIds: number[], disableConfirmation?: boolean): Promise<void> {
return Chapters.executeAction( return Chapters.executeAction(
'delete', 'delete',
chapterIds.length, chapterIds.length,
() => requestManager.deleteDownloadedChapters(chapterIds).response, () => requestManager.deleteDownloadedChapters(chapterIds).response,
disableConfirmation,
); );
} }
@@ -203,6 +210,7 @@ export class Chapters {
chapters: (ChapterIdInfo & ChapterDownloadInfo & ChapterBookmarkInfo)[], chapters: (ChapterIdInfo & ChapterDownloadInfo & ChapterBookmarkInfo)[],
wasManuallyMarkedAsRead: boolean = false, wasManuallyMarkedAsRead: boolean = false,
trackProgressMangaId?: MangaIdInfo['id'], trackProgressMangaId?: MangaIdInfo['id'],
disableConfirmation?: boolean,
): Promise<void> { ): Promise<void> {
const { deleteChaptersManuallyMarkedRead, deleteChaptersWithBookmark, updateProgressManualMarkRead } = const { deleteChaptersManuallyMarkedRead, deleteChaptersWithBookmark, updateProgressManualMarkRead } =
await getMetadataServerSettings(); await getMetadataServerSettings();
@@ -221,14 +229,16 @@ export class Chapters {
trackProgressMangaId: trackProgressMangaId:
updateProgressManualMarkRead && wasManuallyMarkedAsRead ? trackProgressMangaId : undefined, updateProgressManualMarkRead && wasManuallyMarkedAsRead ? trackProgressMangaId : undefined,
}).response, }).response,
disableConfirmation,
); );
} }
static async markAsUnread(chapterIds: number[]): Promise<void> { static async markAsUnread(chapterIds: number[], disableConfirmation?: boolean): Promise<void> {
return Chapters.executeAction( return Chapters.executeAction(
'mark_as_unread', 'mark_as_unread',
chapterIds.length, chapterIds.length,
() => requestManager.updateChapters(chapterIds, { isRead: false }).response, () => requestManager.updateChapters(chapterIds, { isRead: false }).response,
disableConfirmation,
); );
} }
@@ -252,8 +262,33 @@ export class Chapters {
action: ChapterAction, action: ChapterAction,
itemCount: number, itemCount: number,
fnToExecute: () => Promise<unknown>, fnToExecute: () => Promise<unknown>,
disableConfirmation?: boolean,
): Promise<void> { ): Promise<void> {
const { always, bulkAction, bulkActionCountForce } = CHAPTER_ACTION_TO_CONFIRMATION_REQUIRED[action];
const requiresConfirmation =
(!disableConfirmation && (always || (bulkAction && itemCount > 1))) ||
(bulkActionCountForce && itemCount >= bulkActionCountForce);
const confirmationMessage = CHAPTER_ACTION_TO_TRANSLATION[action].confirmation;
try { try {
if (requiresConfirmation) {
assertIsDefined(confirmationMessage);
try {
await awaitConfirmation({
title: translate('global.label.are_you_sure'),
message: translate(confirmationMessage, { count: itemCount }),
actions: {
confirm: {
title: translate('global.button.ok'),
},
},
});
} catch (_) {
return;
}
}
await fnToExecute(); await fnToExecute();
makeToast(translate(CHAPTER_ACTION_TO_TRANSLATION[action].success, { count: itemCount }), 'success'); makeToast(translate(CHAPTER_ACTION_TO_TRANSLATION[action].success, { count: itemCount }), 'success');
} catch (e) { } catch (e) {

View File

@@ -9,6 +9,10 @@
import { MangaStatus } from '@/lib/graphql/generated/graphql.ts'; import { MangaStatus } from '@/lib/graphql/generated/graphql.ts';
import { TranslationKey } from '@/Base.types.ts'; import { TranslationKey } from '@/Base.types.ts';
import { MangaAction, MangaIdInfo, MangaType } from '@/modules/manga/Manga.types.ts'; import { MangaAction, MangaIdInfo, MangaType } from '@/modules/manga/Manga.types.ts';
import {
CHAPTER_ACTION_TO_CONFIRMATION_REQUIRED,
CHAPTER_ACTION_TO_TRANSLATION,
} from '@/modules/chapter/Chapter.constants.ts';
export const FALLBACK_MANGA: MangaIdInfo = { id: -1 }; export const FALLBACK_MANGA: MangaIdInfo = { id: -1 };
@@ -26,53 +30,35 @@ export const MANGA_STATUS_TO_TRANSLATION: Record<MangaStatus, TranslationKey> =
[MangaStatus.Unknown]: 'manga.status.unknown', [MangaStatus.Unknown]: 'manga.status.unknown',
}; };
export const MANGA_ACTION_TO_CONFIRMATION_REQUIRED: Record<
MangaAction,
{ always: boolean; bulkAction: boolean; bulkActionCountForce?: number }
> = {
...CHAPTER_ACTION_TO_CONFIRMATION_REQUIRED,
remove_from_library: { always: true, bulkAction: true },
change_categories: { always: false, bulkAction: false },
migrate: { always: false, bulkAction: false },
track: { always: false, bulkAction: false },
};
export const MANGA_ACTION_TO_TRANSLATION: { export const MANGA_ACTION_TO_TRANSLATION: {
[key in MangaAction]: { [key in MangaAction]: {
action: { action: {
single: TranslationKey; single: TranslationKey;
selected: TranslationKey; selected: TranslationKey;
}; };
confirmation?: TranslationKey;
success: TranslationKey; success: TranslationKey;
error: TranslationKey; error: TranslationKey;
}; };
} = { } = {
download: { ...CHAPTER_ACTION_TO_TRANSLATION,
action: {
single: 'chapter.action.download.add.label.action',
selected: 'chapter.action.download.add.button.selected',
},
success: 'chapter.action.download.add.label.success',
error: 'chapter.action.download.add.label.error',
},
delete: {
action: {
single: 'chapter.action.download.delete.label.action',
selected: 'chapter.action.download.delete.button.selected',
},
success: 'chapter.action.download.delete.label.success',
error: 'chapter.action.download.delete.label.error',
},
mark_as_read: {
action: {
single: 'chapter.action.mark_as_read.add.label.action.current',
selected: 'chapter.action.mark_as_read.add.button.selected',
},
success: 'chapter.action.mark_as_read.add.label.success',
error: 'chapter.action.mark_as_read.add.label.error',
},
mark_as_unread: {
action: {
single: 'chapter.action.mark_as_read.remove.label.action',
selected: 'chapter.action.mark_as_read.remove.button.selected',
},
success: 'chapter.action.mark_as_read.remove.label.success',
error: 'chapter.action.mark_as_read.remove.label.error',
},
remove_from_library: { remove_from_library: {
action: { action: {
single: 'manga.action.library.remove.label.action', single: 'manga.action.library.remove.label.action',
selected: 'manga.action.library.remove.button.selected', selected: 'manga.action.library.remove.button.selected',
}, },
confirmation: 'manga.action.library.remove.label.confirmation',
success: 'manga.action.library.remove.label.success', success: 'manga.action.library.remove.label.success',
error: 'manga.action.library.remove.label.error', error: 'manga.action.library.remove.label.error',
}, },
@@ -81,6 +67,7 @@ export const MANGA_ACTION_TO_TRANSLATION: {
single: 'manga.action.category.label.action', single: 'manga.action.category.label.action',
selected: 'manga.action.category.button.selected', selected: 'manga.action.category.button.selected',
}, },
confirmation: 'manga.action.category.label.confirmation',
success: 'manga.action.category.label.success', success: 'manga.action.category.label.success',
error: 'manga.action.category.label.error', error: 'manga.action.category.label.error',
}, },

View File

@@ -63,7 +63,7 @@ export const useManageMangaLibraryState = (
}); });
} }
await Mangas.removeFromLibrary([manga.id]); await Mangas.removeFromLibrary([manga.id], true);
setIsInLibrary(false); setIsInLibrary(false);
}, [manga.id, confirmRemoval]); }, [manga.id, confirmRemoval]);

View File

@@ -43,11 +43,14 @@ import {
MigrateMode, MigrateMode,
} from '@/modules/manga/Manga.types.ts'; } from '@/modules/manga/Manga.types.ts';
import { import {
MANGA_ACTION_TO_CONFIRMATION_REQUIRED,
MANGA_ACTION_TO_TRANSLATION, MANGA_ACTION_TO_TRANSLATION,
MANGA_TAGS_BY_MANGA_TYPE, MANGA_TAGS_BY_MANGA_TYPE,
SOURCES_BY_MANGA_TYPE, SOURCES_BY_MANGA_TYPE,
} from '@/modules/manga/Manga.constants.ts'; } from '@/modules/manga/Manga.constants.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { awaitConfirmation } from '@/modules/core/utils/AwaitableDialog.tsx';
import { assertIsDefined } from '@/Asserts.ts';
type MangaToMigrate = NonNullable<GetMangaToMigrateQuery['manga']>; type MangaToMigrate = NonNullable<GetMangaToMigrateQuery['manga']>;
type MangaToMigrateTo = NonNullable<GetMangaToMigrateToFetchMutation['fetchManga']>['manga']; type MangaToMigrateTo = NonNullable<GetMangaToMigrateToFetchMutation['fetchManga']>['manga'];
@@ -194,6 +197,7 @@ export class Mangas {
static async downloadChapters( static async downloadChapters(
mangaIds: number[], mangaIds: number[],
{ size, onlyUnread, downloadAhead = false }: DownloadChaptersOptions = {}, { size, onlyUnread, downloadAhead = false }: DownloadChaptersOptions = {},
disableConfirmation?: boolean,
): Promise<void> { ): Promise<void> {
const [chaptersToConsider, unReadDownloadedChapters] = await Promise.all([ const [chaptersToConsider, unReadDownloadedChapters] = await Promise.all([
Mangas.getChapterIdsWithState(mangaIds, { Mangas.getChapterIdsWithState(mangaIds, {
@@ -250,25 +254,34 @@ export class Mangas {
return Promise.resolve(); return Promise.resolve();
} }
return Chapters.download(Chapters.getIds(chapterIdsToDownload)); return Chapters.download(Chapters.getIds(chapterIdsToDownload), disableConfirmation);
} }
static async deleteChapters(mangaIds: number[]): Promise<void> { static async deleteChapters(mangaIds: number[], disableConfirmation?: boolean): Promise<void> {
const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isDownloaded: true }); const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isDownloaded: true });
return Chapters.delete(Chapters.getIds(chapters)); return Chapters.delete(Chapters.getIds(chapters), disableConfirmation);
} }
static async markAsRead(mangaIds: number[], wasManuallyMarkedAsRead: boolean = false): Promise<void> { static async markAsRead(
mangaIds: number[],
wasManuallyMarkedAsRead: boolean = false,
disableConfirmation?: boolean,
): Promise<void> {
const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isRead: false }); const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isRead: false });
return Chapters.markAsRead(chapters, wasManuallyMarkedAsRead, mangaIds.length === 1 ? mangaIds[0] : undefined); return Chapters.markAsRead(
chapters,
wasManuallyMarkedAsRead,
mangaIds.length === 1 ? mangaIds[0] : undefined,
disableConfirmation,
);
} }
static async markAsUnread(mangaIds: number[]): Promise<void> { static async markAsUnread(mangaIds: number[], disableConfirmation?: boolean): Promise<void> {
const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isRead: true }); const chapters = await Mangas.getChapterIdsWithState(mangaIds, { isRead: true });
return Chapters.markAsUnread(Chapters.getIds(chapters)); return Chapters.markAsUnread(Chapters.getIds(chapters), disableConfirmation);
} }
static async removeFromLibrary(mangaIds: number[]): Promise<void> { static async removeFromLibrary(mangaIds: number[], disableConfirmation?: boolean): Promise<void> {
const { removeMangaFromCategories } = await getMetadataServerSettings(); const { removeMangaFromCategories } = await getMetadataServerSettings();
return Mangas.executeAction( return Mangas.executeAction(
'remove_from_library', 'remove_from_library',
@@ -278,14 +291,20 @@ export class Mangas {
updateMangas: { inLibrary: false }, updateMangas: { inLibrary: false },
updateMangasCategories: removeMangaFromCategories ? { clearCategories: true } : undefined, updateMangasCategories: removeMangaFromCategories ? { clearCategories: true } : undefined,
}).response, }).response,
disableConfirmation,
); );
} }
static async changeCategories(mangaIds: number[], patch: UpdateMangaCategoriesPatchInput): Promise<void> { static async changeCategories(
mangaIds: number[],
patch: UpdateMangaCategoriesPatchInput,
disableConfirmation?: boolean,
): Promise<void> {
return Mangas.executeAction( return Mangas.executeAction(
'change_categories', 'change_categories',
mangaIds.length, mangaIds.length,
() => requestManager.updateMangasCategories(mangaIds, patch).response, () => requestManager.updateMangasCategories(mangaIds, patch).response,
disableConfirmation,
); );
} }
@@ -418,88 +437,119 @@ export class Mangas {
migrateTracking, migrateTracking,
deleteChapters, deleteChapters,
}: Omit<MigrateOptions, 'mangaIdToMigrateTo'>, }: Omit<MigrateOptions, 'mangaIdToMigrateTo'>,
disableConfirmation?: boolean,
): Promise<void> { ): Promise<void> {
return Mangas.executeAction('migrate', 1, async () => { return Mangas.executeAction(
const [{ data: mangaToMigrateData }, { data: mangaToMigrateToData }, { removeMangaFromCategories }] = 'migrate',
await Promise.all([ 1,
requestManager.getMangaToMigrate(mangaId, { async () => {
migrateChapters, const [{ data: mangaToMigrateData }, { data: mangaToMigrateToData }, { removeMangaFromCategories }] =
migrateCategories, await Promise.all([
migrateTracking, requestManager.getMangaToMigrate(mangaId, {
deleteChapters, migrateChapters,
}).response, migrateCategories,
requestManager.getMangaToMigrateToFetch(mangaIdToMigrateTo, { migrateTracking,
migrateChapters, deleteChapters,
migrateCategories, }).response,
migrateTracking, requestManager.getMangaToMigrateToFetch(mangaIdToMigrateTo, {
apolloOptions: { errorPolicy: 'all' }, migrateChapters,
}).response, migrateCategories,
getMetadataServerSettings(), migrateTracking,
]); apolloOptions: { errorPolicy: 'all' },
}).response,
getMetadataServerSettings(),
]);
if (!mangaToMigrateData.manga || !mangaToMigrateToData?.fetchManga?.manga) { if (!mangaToMigrateData.manga || !mangaToMigrateToData?.fetchManga?.manga) {
throw new Error('Mangas::migrate: missing manga data'); throw new Error('Mangas::migrate: missing manga data');
}
if (migrateChapters && !mangaToMigrateData.manga.chapters) {
throw new Error('Mangas::migrate: missing chapters data');
}
if (!mangaToMigrateToData.fetchChapters?.chapters) {
mangaToMigrateToData.fetchChapters = { chapters: [] };
}
const performMigrationAction = async (
migrateAction: keyof MigrateFuncReturn,
...actions: [boolean | undefined, MigrateFuncReturn][]
) =>
Promise.all(
actions
.filter(([performAction]) => performAction)
.map(([, action]) => action[migrateAction]())
.flat(),
);
const performMigrationActions = async (...actions: [boolean | undefined, MigrateFuncReturn][]) => {
const migrationActions: TupleUnion<keyof MigrateFuncReturn> = ['copy', 'cleanup'];
for (const migrationAction of migrationActions) {
// the migration actions (copy, cleanup) are supposed to be run sequentially to ensure that the cleanup
// only happens in case the copy succeeded
// eslint-disable-next-line no-await-in-loop
await performMigrationAction(migrationAction, ...actions);
} }
};
await performMigrationActions( if (migrateChapters && !mangaToMigrateData.manga.chapters) {
[ throw new Error('Mangas::migrate: missing chapters data');
migrateChapters, }
Mangas.migrateChapters(mode, mangaToMigrateData.manga, mangaToMigrateToData, !!deleteChapters),
], if (!mangaToMigrateToData.fetchChapters?.chapters) {
[ mangaToMigrateToData.fetchChapters = { chapters: [] };
migrateTracking, }
Mangas.migrateTracking(mode, mangaToMigrateData.manga, mangaToMigrateToData.fetchManga.manga),
], const performMigrationAction = async (
[ migrateAction: keyof MigrateFuncReturn,
true, ...actions: [boolean | undefined, MigrateFuncReturn][]
Mangas.migrateManga( ) =>
mode, Promise.all(
mangaToMigrateData.manga, actions
mangaToMigrateToData.fetchManga.manga, .filter(([performAction]) => performAction)
!!migrateCategories, .map(([, action]) => action[migrateAction]())
removeMangaFromCategories, .flat(),
), );
],
); const performMigrationActions = async (...actions: [boolean | undefined, MigrateFuncReturn][]) => {
}); const migrationActions: TupleUnion<keyof MigrateFuncReturn> = ['copy', 'cleanup'];
for (const migrationAction of migrationActions) {
// the migration actions (copy, cleanup) are supposed to be run sequentially to ensure that the cleanup
// only happens in case the copy succeeded
// eslint-disable-next-line no-await-in-loop
await performMigrationAction(migrationAction, ...actions);
}
};
await performMigrationActions(
[
migrateChapters,
Mangas.migrateChapters(mode, mangaToMigrateData.manga, mangaToMigrateToData, !!deleteChapters),
],
[
migrateTracking,
Mangas.migrateTracking(mode, mangaToMigrateData.manga, mangaToMigrateToData.fetchManga.manga),
],
[
true,
Mangas.migrateManga(
mode,
mangaToMigrateData.manga,
mangaToMigrateToData.fetchManga.manga,
!!migrateCategories,
removeMangaFromCategories,
),
],
);
},
disableConfirmation,
);
} }
private static async executeAction( private static async executeAction(
action: MangaAction, action: MangaAction,
itemCount: number, itemCount: number,
fnToExecute: () => Promise<unknown>, fnToExecute: () => Promise<unknown>,
disableConfirmation?: boolean,
): Promise<void> { ): Promise<void> {
const { always, bulkAction, bulkActionCountForce } = MANGA_ACTION_TO_CONFIRMATION_REQUIRED[action];
const requiresConfirmation =
!disableConfirmation &&
(always || (bulkAction && itemCount > 1) || (bulkActionCountForce && itemCount >= bulkActionCountForce));
const confirmationMessage = MANGA_ACTION_TO_TRANSLATION[action].confirmation;
try { try {
if (requiresConfirmation) {
assertIsDefined(confirmationMessage);
try {
await awaitConfirmation({
title: translate('global.label.are_you_sure'),
message: translate(confirmationMessage, { count: itemCount }),
actions: {
confirm: {
title: translate('global.button.ok'),
},
},
});
} catch (_) {
return;
}
}
await fnToExecute(); await fnToExecute();
makeToast(translate(MANGA_ACTION_TO_TRANSLATION[action].success, { count: itemCount }), 'success'); makeToast(translate(MANGA_ACTION_TO_TRANSLATION[action].success, { count: itemCount }), 'success');
} catch (e) { } catch (e) {
@@ -524,22 +574,28 @@ export class Mangas {
size, size,
...migrateOptions ...migrateOptions
}: PerformActionOptions<Action>, }: PerformActionOptions<Action>,
disableConfirmation?: boolean,
): Promise<void> { ): Promise<void> {
switch (action) { switch (action) {
case 'download': case 'download':
return Mangas.downloadChapters(mangaIds, { downloadAhead, onlyUnread, size }); return Mangas.downloadChapters(mangaIds, { downloadAhead, onlyUnread, size }, disableConfirmation);
case 'delete': case 'delete':
return Mangas.deleteChapters(mangaIds); return Mangas.deleteChapters(mangaIds, disableConfirmation);
case 'mark_as_read': case 'mark_as_read':
return Mangas.markAsRead(mangaIds, wasManuallyMarkedAsRead!); return Mangas.markAsRead(mangaIds, wasManuallyMarkedAsRead!, disableConfirmation);
case 'mark_as_unread': case 'mark_as_unread':
return Mangas.markAsUnread(mangaIds); return Mangas.markAsUnread(mangaIds, disableConfirmation);
case 'remove_from_library': case 'remove_from_library':
return Mangas.removeFromLibrary(mangaIds); return Mangas.removeFromLibrary(mangaIds, disableConfirmation);
case 'change_categories': case 'change_categories':
return Mangas.changeCategories(mangaIds, changeCategoriesPatch!); return Mangas.changeCategories(mangaIds, changeCategoriesPatch!, disableConfirmation);
case 'migrate': { case 'migrate': {
return Mangas.migrate(mangaIds[0], mangaIdToMigrateTo!, migrateOptions as unknown as MigrateOptions); return Mangas.migrate(
mangaIds[0],
mangaIdToMigrateTo!,
migrateOptions as unknown as MigrateOptions,
disableConfirmation,
);
} }
default: default:
throw new Error(`Mangas::performAction: unknown action "${action}"`); throw new Error(`Mangas::performAction: unknown action "${action}"`);