Support library batch selection migration

Closes #1089
This commit is contained in:
schroda
2026-04-23 15:18:35 +02:00
parent 172c522d01
commit 527769ec5e
17 changed files with 172 additions and 144 deletions

View File

@@ -15,7 +15,6 @@ import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import Label from '@mui/icons-material/Label';
import { useMemo, useState } from 'react';
import SyncAltIcon from '@mui/icons-material/SyncAlt';
import { Link } from 'react-router-dom';
import SyncIcon from '@mui/icons-material/Sync';
import Dialog from '@mui/material/Dialog';
import { AwaitableComponent } from 'awaitable-component';
@@ -35,7 +34,6 @@ import { NestedMenuItem } from '@/base/components/menu/NestedMenuItem.tsx';
import type { MangaChapterStatFieldsFragment, MangaType } from '@/lib/graphql/generated/graphql.ts';
import type { MangaAction, MangaDownloadInfo, MangaIdInfo, MangaUnreadInfo } from '@/features/manga/Manga.types.ts';
import { MANGA_ACTION_TO_TRANSLATION } from '@/features/manga/Manga.constants.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { CategorySelect } from '@/features/category/components/CategorySelect.tsx';
import { STABLE_EMPTY_ARRAY } from '@/base/Base.constants.ts';
@@ -57,7 +55,7 @@ type Props =
export const MangaActionMenuItems = ({
manga,
handleSelection,
selectedMangas: passedSelectedMangas,
selectedMangas = STABLE_EMPTY_ARRAY,
onClose,
setHideMenu,
}: Props) => {
@@ -66,7 +64,6 @@ export const MangaActionMenuItems = ({
const [isTrackDialogOpen, setIsTrackDialogOpen] = useState(false);
const isSingleMode = !!manga;
const selectedMangas = passedSelectedMangas ?? STABLE_EMPTY_ARRAY;
const getMenuItemTitle = createGetMenuItemTitle(isSingleMode, MANGA_ACTION_TO_TRANSLATION);
const shouldShowMenuItem = createShouldShowMenuItem(isSingleMode);
@@ -148,19 +145,11 @@ export const MangaActionMenuItems = ({
title={getMenuItemTitle('mark_as_unread', readMangas.length)}
/>
)}
{isSingleMode && (
<Link
to={AppRoutes.migrate.childRoutes.singleMangaSearch.path(
manga?.sourceId ?? -1,
manga?.id ?? -1,
manga?.title,
)}
state={{ mangaTitle: manga?.title }}
style={{ textDecoration: 'none', color: 'inherit' }}
>
<MenuItem Icon={SyncAltIcon} title={getMenuItemTitle('migrate', selectedMangas.length)} />
</Link>
)}
<MenuItem
title={getMenuItemTitle('migrate', selectedMangas.length)}
Icon={SyncAltIcon}
onClick={() => performAction('migrate', selectedMangas)}
/>
{isSingleMode && (
<MenuItem
onClick={() => {
@@ -175,7 +164,7 @@ export const MangaActionMenuItems = ({
onClick={() => {
AwaitableComponent.show(CategorySelect, {
mangaId: manga?.id,
mangaIds: passedSelectedMangas ? Mangas.getIds(selectedMangas) : undefined,
mangaIds: selectedMangas ? Mangas.getIds(selectedMangas) : undefined,
addToLibrary: false,
});
setHideMenu(true);

View File

@@ -22,13 +22,15 @@ import { GridLayout } from '@/base/Base.types.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
import { makeToast } from '@/base/utils/Toast.ts';
import { Mangas } from '@/features/manga/services/Mangas.ts';
import { t } from '@lingui/core/macro';
import { useParams } from 'react-router-dom';
import { ReactRouter } from '@/lib/react-router/ReactRouter.ts';
import { MigrationOptionsDialog } from '@/features/migration/components/MigrationOptionsDialog.tsx';
import { AwaitableComponent } from 'awaitable-component';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler';
import { MangaMigration } from '@/features/migration/MangaMigration.ts';
import { MANGA_ACTION_TO_TRANSLATION } from '@/features/manga/Manga.constants.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
const getMangaLinkTo = (
mode: MangaCardMode,
@@ -123,8 +125,15 @@ export const MangaCard = memo((props: MangaCardProps) => {
optionsDialog.update({ isMigrating: true });
try {
await Mangas.migrate(migrationSourceMangaId, id, options);
try {
await MangaMigration.migrate(migrationSourceMangaId, id, options);
} catch (e) {
makeToast(
t(MANGA_ACTION_TO_TRANSLATION['migrate'].error),
'error',
getErrorMessage(e),
);
}
optionsDialog.submit(options);
ReactRouter.navigate(AppRoutes.manga.path(id), { replace: true });

View File

@@ -22,8 +22,7 @@ import { Chapters } from '@/features/chapter/services/Chapters.ts';
import { makeToast } from '@/base/utils/Toast.ts';
import { getMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
import { GET_MANGAS_BASE } from '@/lib/graphql/manga/MangaQuery.ts';
import { MANGA_BASE_FIELDS } from '@/lib/graphql/manga/MangaFragments.ts';
import type { MigrateOptions } from '@/features/migration/Migration.types.ts';
import { MANGA_BASE_FIELDS, MANGA_MIGRATION_FIELDS } from '@/lib/graphql/manga/MangaFragments.ts';
import type {
MangaAction,
MangaArtistInfo,
@@ -49,7 +48,10 @@ import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { assertIsDefined } from '@/base/Asserts.ts';
import { Confirmation } from '@/base/AppAwaitableComponent.ts';
import { UrlUtil } from '@/lib/UrlUtil.ts';
import { MangaMigration } from '@/features/migration/MangaMigration.ts';
import { MigrationManager } from '@/features/migration/MigrationManager.ts';
import uniq from 'lodash/fp/uniq';
import { ReactRouter } from '@/lib/react-router/ReactRouter.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
type DownloadChaptersOptions = {
size?: number;
@@ -61,23 +63,18 @@ type ChangeCategoriesOptions = { changeCategoriesPatch: UpdateMangaCategoriesPat
type MarkAsReadActionOption = MarkAsReadOptions &
PropertiesNever<ChangeCategoriesOptions> &
PropertiesNever<MigrateOptions> &
PropertiesNever<DownloadChaptersOptions>;
type ChangeCategoriesActionOption = PropertiesNever<MarkAsReadOptions> &
ChangeCategoriesOptions &
PropertiesNever<MigrateOptions> &
PropertiesNever<DownloadChaptersOptions>;
type MigrateActionOption = PropertiesNever<MarkAsReadOptions> &
PropertiesNever<ChangeCategoriesOptions> &
MigrateOptions &
PropertiesNever<DownloadChaptersOptions>;
type DownloadActionOption = PropertiesNever<MarkAsReadOptions> &
PropertiesNever<ChangeCategoriesOptions> &
PropertiesNever<MigrateOptions> &
DownloadChaptersOptions;
type DefaultActionOption = Partial<MarkAsReadOptions> &
Partial<ChangeCategoriesOptions> &
Partial<MigrateOptions> &
Partial<DownloadChaptersOptions>;
type PerformActionOptions<Action extends MangaAction> = Action extends 'mark_as_read'
@@ -298,18 +295,22 @@ export class Mangas {
);
}
static async migrate(
mangaId: MangaIdInfo['id'],
mangaIdToMigrateTo: number,
options: Omit<MigrateOptions, 'mangaIdToMigrateTo'>,
disableConfirmation?: boolean,
): Promise<void> {
return Mangas.executeAction(
'migrate',
1,
() => MangaMigration.migrate(mangaId, mangaIdToMigrateTo, options),
disableConfirmation,
);
static async migrate(mangaIds: MangaIdInfo['id'][]): Promise<void> {
const mangas = mangaIds.map((mangaId) => {
const manga = Mangas.getFromCache(mangaId, MANGA_MIGRATION_FIELDS, 'MANGA_MIGRATION_FIELDS');
assertIsDefined(manga);
return manga;
});
const sourceIds = uniq(mangas.map((manga) => manga.sourceId));
MigrationManager.selectSources(sourceIds);
MigrationManager.selectMangas(mangas);
const isBulkMigration = mangas.length > 1;
if (isBulkMigration) {
ReactRouter.navigate(AppRoutes.migrate.path);
}
}
private static async executeAction(
@@ -367,11 +368,9 @@ export class Mangas {
{
wasManuallyMarkedAsRead,
changeCategoriesPatch,
mangaIdToMigrateTo,
downloadAhead,
onlyUnread,
size,
...migrateOptions
}: PerformActionOptions<Action>,
disableConfirmation?: boolean,
): Promise<void> {
@@ -389,12 +388,7 @@ export class Mangas {
case 'change_categories':
return Mangas.changeCategories(mangaIds, changeCategoriesPatch!, disableConfirmation);
case 'migrate': {
return Mangas.migrate(
mangaIds[0],
mangaIdToMigrateTo!,
migrateOptions as unknown as MigrateOptions,
disableConfirmation,
);
return Mangas.migrate(mangaIds);
}
default:
throw new Error(`Mangas::performAction: unknown action "${action}"`);