@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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}"`);
|
||||
|
||||
@@ -34,7 +34,7 @@ export const MAX_SOURCES_IN_PARALLEL = 6;
|
||||
|
||||
export const DEFAULT_MIGRATION_STATE: MigrationState = {
|
||||
phase: MigrationPhase.IDLE,
|
||||
sourceId: null,
|
||||
sourceIds: null,
|
||||
entries: {},
|
||||
destinationSourceIds: [],
|
||||
migrateOptions: null,
|
||||
|
||||
@@ -111,7 +111,7 @@ export type MigrationProgress = { total: number; completed: number; success: num
|
||||
|
||||
export interface MigrationState {
|
||||
phase: MigrationPhase;
|
||||
sourceId: SourceIdInfo['id'] | null;
|
||||
sourceIds: SourceIdInfo['id'][] | null;
|
||||
entries: Record<MangaIdInfo['id'], TMigrationEntry>;
|
||||
destinationSourceIds: SourceIdInfo['id'][];
|
||||
migrateOptions: Omit<MigrateOptions, 'mangaIdToMigrateTo'> | null;
|
||||
@@ -123,3 +123,7 @@ export interface MigrationState {
|
||||
}
|
||||
|
||||
export interface SourceItem extends SourceIdInfo, SourceNameInfo, SourceLanguageInfo, SourceIconInfo, SourceMetaInfo {}
|
||||
|
||||
export type TMigratableSource = NonNullable<GetMigratableSourcesQuery['mangas']['nodes'][number]['source']> & {
|
||||
mangaCount: number;
|
||||
};
|
||||
|
||||
@@ -14,11 +14,11 @@ import { devtools, persist } from 'zustand/middleware';
|
||||
import {
|
||||
type MigratableEntry,
|
||||
type MigrateOptions,
|
||||
type TMigrationEntry,
|
||||
MigrationEntryStatus,
|
||||
MigrationPhase,
|
||||
type MigrationMatch,
|
||||
MigrationPhase,
|
||||
type MigrationState,
|
||||
type TMigrationEntry,
|
||||
} from '@/features/migration/Migration.types.ts';
|
||||
import {
|
||||
DEFAULT_MIGRATION_STATE,
|
||||
@@ -153,6 +153,14 @@ export class MigrationManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static ensureIsInValidPhase(phases: MigrationPhase[]): void {
|
||||
const { phase } = MigrationManager.getState();
|
||||
|
||||
if (!phases.includes(phase)) {
|
||||
throw new Error(`Illegal migration phase "${phase}". Expected: ${phases}`);
|
||||
}
|
||||
}
|
||||
|
||||
static async goToPreviousPhase(): Promise<boolean> {
|
||||
switch (MigrationManager.getState().phase) {
|
||||
case MigrationPhase.IDLE:
|
||||
@@ -265,16 +273,34 @@ export class MigrationManager {
|
||||
return updatedEntry;
|
||||
}
|
||||
|
||||
static selectSource(sourceId: SourceIdInfo['id']): void {
|
||||
static selectSources(sourceIds: SourceIdInfo['id'][]): void {
|
||||
MigrationManager.ensureIsInValidPhase([MigrationPhase.IDLE]);
|
||||
|
||||
MigrationManager.updateState((draft) => {
|
||||
draft.phase = MigrationPhase.SELECT_MANGAS;
|
||||
draft.sourceId = sourceId;
|
||||
draft.sourceIds = sourceIds;
|
||||
draft.entries = {};
|
||||
});
|
||||
ReactRouter.navigate(AppRoutes.migrate.path);
|
||||
}
|
||||
|
||||
static selectMangas(mangas: MangaMigrationFieldsFragment[]): void {
|
||||
MigrationManager.ensureIsInValidPhase([MigrationPhase.SELECT_MANGAS]);
|
||||
|
||||
const isSingleManga = mangas.length === 1;
|
||||
if (isSingleManga) {
|
||||
const [manga] = mangas;
|
||||
|
||||
ReactRouter.navigate(
|
||||
AppRoutes.migrate.childRoutes.singleMangaSearch.path(manga.sourceId, manga.id, manga.title),
|
||||
{
|
||||
state: { mangaTitle: manga.title },
|
||||
},
|
||||
);
|
||||
|
||||
MigrationManager.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
MigrationManager.updateState((draft) => {
|
||||
draft.phase = MigrationPhase.SELECTING_SOURCES;
|
||||
draft.entries = Object.fromEntries(
|
||||
@@ -305,6 +331,8 @@ export class MigrationManager {
|
||||
}
|
||||
|
||||
static async startSearch(destinationSourceIds: SourceIdInfo['id'][]): Promise<void> {
|
||||
MigrationManager.ensureIsInValidPhase([MigrationPhase.SELECTING_SOURCES]);
|
||||
|
||||
MigrationManager.updateState((draft) => {
|
||||
draft.destinationSourceIds = destinationSourceIds;
|
||||
});
|
||||
@@ -369,6 +397,8 @@ export class MigrationManager {
|
||||
}
|
||||
|
||||
static async startMigration(options: Omit<MigrateOptions, 'mangaIdToMigrateTo'>): Promise<void> {
|
||||
MigrationManager.ensureIsInValidPhase([MigrationPhase.SEARCHING]);
|
||||
|
||||
const migratableEntries = MigrationManager.getMigratableEntries();
|
||||
|
||||
await Confirmation.show({
|
||||
@@ -551,25 +581,49 @@ export class MigrationManager {
|
||||
return RESUMABLE_PHASES.includes(MigrationManager.getState().phase);
|
||||
}
|
||||
|
||||
private static getHigherPrioritySourceIds(sourceId: SourceIdInfo['id']): SourceIdInfo['id'][] {
|
||||
const { destinationSourceIds } = MigrationManager.getState();
|
||||
private static getDestinationSourceIds(mangaSourceId: SourceIdInfo['id']): MigrationState['destinationSourceIds'] {
|
||||
const { sourceIds, destinationSourceIds } = MigrationManager.getState();
|
||||
|
||||
const sourceIdPriority = destinationSourceIds.indexOf(sourceId);
|
||||
// Prevent (most likely) unintentionally matching a manga to itself.
|
||||
const isMultiSourceMigration = (sourceIds?.length ?? -1) > 1;
|
||||
if (isMultiSourceMigration) {
|
||||
const mangaSourceIdIndex = destinationSourceIds.indexOf(mangaSourceId);
|
||||
|
||||
return [
|
||||
...destinationSourceIds.slice(0, mangaSourceIdIndex),
|
||||
...destinationSourceIds.slice(mangaSourceIdIndex + 1),
|
||||
mangaSourceId,
|
||||
];
|
||||
}
|
||||
|
||||
return destinationSourceIds;
|
||||
}
|
||||
|
||||
private static getHigherPrioritySourceIds(
|
||||
mangaSourceId: SourceIdInfo['id'],
|
||||
destSourceId: SourceIdInfo['id'],
|
||||
): SourceIdInfo['id'][] {
|
||||
const destinationSourceIds = MigrationManager.getDestinationSourceIds(mangaSourceId);
|
||||
|
||||
const sourceIdPriority = destinationSourceIds.indexOf(destSourceId);
|
||||
return destinationSourceIds.slice(0, Math.max(0, sourceIdPriority - 1));
|
||||
}
|
||||
|
||||
private static isHigherPrioritySourceUnsettled(mangaId: MangaIdInfo['id'], sourceId: SourceIdInfo['id']): boolean {
|
||||
private static isHigherPrioritySourceUnsettled(
|
||||
mangaId: MangaIdInfo['id'],
|
||||
destSourceId: SourceIdInfo['id'],
|
||||
): boolean {
|
||||
const { entries } = MigrationManager.getState();
|
||||
const entry = entries[mangaId];
|
||||
|
||||
assertIsDefined(entry);
|
||||
|
||||
return MigrationManager.getHigherPrioritySourceIds(sourceId).some(
|
||||
return MigrationManager.getHigherPrioritySourceIds(entry.sourceId, destSourceId).some(
|
||||
(higherPrioritySourceId) => entry.destSourceIdToSearchState[higherPrioritySourceId] == null,
|
||||
);
|
||||
}
|
||||
|
||||
private static hasHigherSourcePriorityMatch(mangaId: MangaIdInfo['id'], sourceId: SourceIdInfo['id']): boolean {
|
||||
private static hasHigherSourcePriorityMatch(mangaId: MangaIdInfo['id'], destSourceId: SourceIdInfo['id']): boolean {
|
||||
const { entries } = MigrationManager.getState();
|
||||
const entry = entries[mangaId];
|
||||
|
||||
@@ -577,7 +631,7 @@ export class MigrationManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
return MigrationManager.getHigherPrioritySourceIds(sourceId).some(
|
||||
return MigrationManager.getHigherPrioritySourceIds(entry.sourceId, destSourceId).some(
|
||||
(higherPrioritySourceId) => entry.destSourceIdToSearchState[higherPrioritySourceId],
|
||||
);
|
||||
}
|
||||
@@ -658,7 +712,7 @@ export class MigrationManager {
|
||||
});
|
||||
|
||||
try {
|
||||
const searchPromises = state.destinationSourceIds.map((destSourceId) =>
|
||||
const searchPromises = MigrationManager.getDestinationSourceIds(entry.sourceId).map((destSourceId) =>
|
||||
MigrationManager.getParallelSourceQueue()(async () => {
|
||||
if (signal.aborted) {
|
||||
return null;
|
||||
@@ -893,8 +947,8 @@ export class MigrationManager {
|
||||
return useMigrationStore((state) => state.phase);
|
||||
}
|
||||
|
||||
static useSourceId(): SourceIdInfo['id'] | null {
|
||||
return useMigrationStore((state) => state.sourceId);
|
||||
static useSourceIds(): MigrationState['sourceIds'] {
|
||||
return useMigrationStore((state) => state.sourceIds);
|
||||
}
|
||||
|
||||
static useEntries(): Record<number, TMigrationEntry> {
|
||||
|
||||
@@ -13,26 +13,29 @@ import Chip from '@mui/material/Chip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import type { GetMigratableSourcesQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { translateExtensionLanguage } from '@/features/extension/Extensions.utils.ts';
|
||||
import { MigrationManager } from '@/features/migration/MigrationManager.ts';
|
||||
import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx';
|
||||
import { ListCardContent } from '@/base/components/lists/cards/ListCardContent.tsx';
|
||||
import { Sources } from '@/features/source/services/Sources';
|
||||
import type { TMigratableSource } from '@/features/migration/Migration.types.ts';
|
||||
import { ReactRouter } from '@/lib/react-router/ReactRouter.ts';
|
||||
import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
||||
|
||||
export type TMigratableSource = NonNullable<GetMigratableSourcesQuery['mangas']['nodes'][number]['source']> & {
|
||||
mangaCount: number;
|
||||
};
|
||||
|
||||
// TODO - cleanup source/extension components
|
||||
export const MigrationCard = ({ id, name, lang, iconUrl, mangaCount }: TMigratableSource) => {
|
||||
export const MigrationCard = (source: TMigratableSource) => {
|
||||
const { id, name, lang, iconUrl, mangaCount } = source;
|
||||
const { t } = useLingui();
|
||||
|
||||
const isLocalSource = Number(id) === 0;
|
||||
const sourceName = isLocalSource ? t`Local source` : name;
|
||||
const sourceName = Sources.isLocalSource(source) ? t`Local source` : name;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardActionArea onClick={() => MigrationManager.selectSource(id)}>
|
||||
<CardActionArea
|
||||
onClick={() => {
|
||||
MigrationManager.selectSources([id]);
|
||||
ReactRouter.navigate(AppRoutes.migrate.path);
|
||||
}}
|
||||
>
|
||||
<ListCardContent sx={{ justifyContent: 'space-between' }}>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<ListCardAvatar
|
||||
|
||||
@@ -20,7 +20,7 @@ import { DndSortableItem } from '@/lib/dnd-kit/DndSortableItem.tsx';
|
||||
import { DndKitUtil } from '@/lib/dnd-kit/DndKitUtil.ts';
|
||||
import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx';
|
||||
import { ListCardContent } from '@/base/components/lists/cards/ListCardContent.tsx';
|
||||
import type { SourceItem } from '@/features/migration/Migration.types.ts';
|
||||
import type { MigrationState, SourceItem } from '@/features/migration/Migration.types.ts';
|
||||
import type { SourceIdInfo } from '@/features/source/Source.types.ts';
|
||||
import type { SelectableCollectionReturnType } from '@/base/collection/hooks/useSelectableCollection.ts';
|
||||
import { assertIsDefined } from '@/base/Asserts.ts';
|
||||
@@ -91,13 +91,13 @@ export const MigrationSourceList = ({
|
||||
handleSelection,
|
||||
selectedSourceIds,
|
||||
handlePriorityChange,
|
||||
currentSourceId,
|
||||
currentSourceIds,
|
||||
}: {
|
||||
sources: SourceItem[];
|
||||
handleSelection: SelectableCollectionReturnType<SourceIdInfo['id'], 'default'>['handleSelection'];
|
||||
handlePriorityChange: (oldIndex: number, newIndex: number) => void;
|
||||
selectedSourceIds: SourceIdInfo['id'][];
|
||||
currentSourceId: SourceIdInfo['id'] | null;
|
||||
currentSourceIds: MigrationState['sourceIds'];
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
const dndSensors = DndKitUtil.useSensorsForDevice();
|
||||
@@ -207,7 +207,7 @@ export const MigrationSourceList = ({
|
||||
<SourceCard
|
||||
source={source}
|
||||
onToggle={handleToggle}
|
||||
isCurrentSource={source.id === currentSourceId}
|
||||
isCurrentSource={!!currentSourceIds?.includes(source.id)}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
);
|
||||
@@ -232,7 +232,7 @@ export const MigrationSourceList = ({
|
||||
<SourceCard
|
||||
source={dndActiveSource!}
|
||||
onToggle={noOp}
|
||||
isCurrentSource={dndActiveSource?.id === currentSourceId}
|
||||
isCurrentSource={!!dndActiveSource && !!currentSourceIds?.includes(dndActiveSource?.id)}
|
||||
isSelected
|
||||
isDragging
|
||||
/>
|
||||
|
||||
@@ -68,7 +68,7 @@ export const MigrationExecute = () => {
|
||||
})}
|
||||
entries={migratingEntries}
|
||||
isMigrating
|
||||
color="error"
|
||||
color="info"
|
||||
/>
|
||||
<MigrationEntryGroup
|
||||
status={MigrationEntryStatus.MIGRATION_FAILED}
|
||||
|
||||
@@ -71,7 +71,7 @@ export const MigrationSearch = () => {
|
||||
other: '# searching',
|
||||
})}
|
||||
entries={searchingEntries}
|
||||
color="error"
|
||||
color="info"
|
||||
/>
|
||||
<MigrationEntryGroup
|
||||
status={MigrationEntryStatus.SEARCH_FAILED}
|
||||
|
||||
@@ -31,7 +31,7 @@ import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
|
||||
export const MigrationSelectDestinationSources = () => {
|
||||
const { t } = useLingui();
|
||||
const currentSourceId = MigrationManager.useSourceId();
|
||||
const currentSourceIds = MigrationManager.useSourceIds();
|
||||
|
||||
const {
|
||||
settings: { browseLanguages, showNsfw },
|
||||
@@ -156,7 +156,7 @@ export const MigrationSelectDestinationSources = () => {
|
||||
selectedSourceIds={selectedItemIds}
|
||||
handleSelection={handleSelection}
|
||||
handlePriorityChange={handlePriorityChange}
|
||||
currentSourceId={currentSourceId}
|
||||
currentSourceIds={currentSourceIds}
|
||||
/>
|
||||
<Fab
|
||||
variant="extended"
|
||||
|
||||
@@ -26,8 +26,6 @@ import { SelectableCollectionSelectMode } from '@/base/collection/components/Sel
|
||||
import { Mangas } from '@/features/manga/services/Mangas.ts';
|
||||
import { STABLE_EMPTY_ARRAY } from '@/base/Base.constants.ts';
|
||||
import { MigrationContinueButton } from '@/features/migration/components/MigrationContinueButton.tsx';
|
||||
import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
||||
import { ReactRouter } from '@/lib/react-router/ReactRouter.ts';
|
||||
|
||||
const getSourceError = (error: unknown): unknown => {
|
||||
const message = getErrorMessage(error);
|
||||
@@ -45,9 +43,11 @@ const getSourceError = (error: unknown): unknown => {
|
||||
|
||||
export const MigrationSelectMangas = () => {
|
||||
const { t } = useLingui();
|
||||
const sourceId = MigrationManager.useSourceId();
|
||||
const sourceIds = MigrationManager.useSourceIds();
|
||||
const selectedMangas = MigrationManager.useEntries();
|
||||
|
||||
const sourceId = sourceIds?.[0];
|
||||
|
||||
const [gridLayout, setGridLayout] = useLocalStorage('migrateGridLayout', GridLayout.List);
|
||||
|
||||
const {
|
||||
@@ -58,7 +58,7 @@ export const MigrationSelectMangas = () => {
|
||||
} = requestManager.useGetSource<GetSourceMigratableQuery, GetSourceMigratableQueryVariables>(
|
||||
GET_SOURCE_MIGRATABLE,
|
||||
sourceId ?? '',
|
||||
{ skip: !sourceId, notifyOnNetworkStatusChange: true },
|
||||
{ skip: !sourceIds, notifyOnNetworkStatusChange: true },
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -67,7 +67,7 @@ export const MigrationSelectMangas = () => {
|
||||
error: mangasError,
|
||||
refetch: refetchMangas,
|
||||
} = requestManager.useGetMigratableSourceMangas(sourceId!, {
|
||||
skip: !sourceId,
|
||||
skip: !sourceIds,
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
@@ -86,7 +86,7 @@ export const MigrationSelectMangas = () => {
|
||||
),
|
||||
});
|
||||
|
||||
const sourceName = migratableSourceData?.source?.displayName ?? sourceId ?? t`Migrate`;
|
||||
const sourceName = migratableSourceData?.source?.displayName ?? sourceIds ?? t`Migrate`;
|
||||
useAppTitleAndAction(
|
||||
sourceName,
|
||||
<>
|
||||
@@ -108,22 +108,6 @@ export const MigrationSelectMangas = () => {
|
||||
const handleContinue = () => {
|
||||
const selected = mangas.filter((manga) => selectedItemIds.includes(manga.id));
|
||||
|
||||
const [entry] = selected;
|
||||
const isSingleManga = selected.length === 1;
|
||||
|
||||
if (isSingleManga && entry) {
|
||||
ReactRouter.navigate(
|
||||
AppRoutes.migrate.childRoutes.singleMangaSearch.path(entry.sourceId, entry.id, entry.title),
|
||||
{
|
||||
state: { mangaTitle: entry.title },
|
||||
},
|
||||
);
|
||||
|
||||
MigrationManager.reset();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
MigrationManager.selectMangas(selected);
|
||||
};
|
||||
|
||||
|
||||
@@ -19,11 +19,14 @@ import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
|
||||
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
|
||||
import type { TMigratableSource } from '@/features/migration/components/MigrationCard.tsx';
|
||||
import { MigrationCard } from '@/features/migration/components/MigrationCard.tsx';
|
||||
import { StyledGroupItemWrapper } from '@/base/components/virtuoso/StyledGroupItemWrapper.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import type { SortSettings, TMigratableSourcesResult } from '@/features/migration/Migration.types.ts';
|
||||
import type {
|
||||
SortSettings,
|
||||
TMigratableSource,
|
||||
TMigratableSourcesResult,
|
||||
} from '@/features/migration/Migration.types.ts';
|
||||
import { SortBy, SortOrder } from '@/features/migration/Migration.types.ts';
|
||||
import { sortByToTranslation, sortOrderToTranslation } from '@/features/migration/Migration.constants.ts';
|
||||
import {
|
||||
|
||||
Reference in New Issue
Block a user