Add bulk migration
This commit is contained in:
@@ -22,6 +22,7 @@ import type {
|
||||
import type { SingleModeProps } from '@/features/manga/components/MangaActionMenuItems.tsx';
|
||||
import type { GridLayout } from '@/base/Base.types.ts';
|
||||
import type { ChapterListOptions } from '@/features/chapter/Chapter.types.ts';
|
||||
import type { MigrationMatch } from '@/features/migration/Migration.types.ts';
|
||||
|
||||
export type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select' | 'duplicate';
|
||||
|
||||
@@ -33,6 +34,7 @@ type MangaCardBaseProps = Pick<MangaTypeGql, 'id' | 'title' | 'sourceId'> &
|
||||
|
||||
export type MangaIdInfo = Pick<MangaTypeGql, 'id'>;
|
||||
export type MangaChapterCountInfo = { chapters: Pick<MangaTypeGql['chapters'], 'totalCount'> };
|
||||
export type MangaHighestChapterNumberInfo = { highestNumberedChapter?: Pick<ChapterType, 'id' | 'chapterNumber'> };
|
||||
export type MangaInLibraryInfo = Pick<MangaTypeGql, 'inLibrary'>;
|
||||
export type MangaDownloadInfo = Pick<MangaTypeGql, 'downloadCount'> & MangaChapterCountInfo;
|
||||
export type MangaUnreadInfo = Pick<MangaTypeGql, 'unreadCount'> & MangaChapterCountInfo;
|
||||
@@ -42,7 +44,7 @@ export type MangaTrackRecordInfo = MangaIdInfo & {
|
||||
};
|
||||
export type MangaGenreInfo = Pick<MangaTypeGql, 'genre'>;
|
||||
export type MangaSourceIdInfo = Pick<MangaTypeGql, 'sourceId'>;
|
||||
export type MangaSourceNameInfo = { source?: Maybe<Pick<SourceType, 'name'>> };
|
||||
export type MangaSourceNameInfo = { source?: Maybe<Pick<SourceType, 'name' | 'displayName'>> };
|
||||
export type MangaSourceLngInfo = { source?: Maybe<Pick<SourceType, 'lang'>> };
|
||||
export type MangaArtistInfo = Pick<MangaTypeGql, 'artist'>;
|
||||
export type MangaAuthorInfo = Pick<MangaTypeGql, 'author'>;
|
||||
@@ -58,6 +60,7 @@ export interface MangaCardProps {
|
||||
inLibraryIndicator?: boolean;
|
||||
selected?: boolean | null;
|
||||
handleSelection?: SelectableCollectionReturnType<MangaTypeGql['id']>['handleSelection'];
|
||||
onMigrateSelect?: (manga: Omit<MigrationMatch, 'sourceTitle' | 'isManualMatch' | 'latestChapterNumber'>) => void;
|
||||
mode?: MangaCardMode;
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,11 @@ export const MangaActionMenuItems = ({
|
||||
)}
|
||||
{isSingleMode && (
|
||||
<Link
|
||||
to={AppRoutes.migrate.childRoutes.search.path(manga?.sourceId ?? -1, manga?.id ?? -1, manga?.title)}
|
||||
to={AppRoutes.migrate.childRoutes.singleMangaSearch.path(
|
||||
manga?.sourceId ?? -1,
|
||||
manga?.id ?? -1,
|
||||
manga?.title,
|
||||
)}
|
||||
state={{ mangaTitle: manga?.title }}
|
||||
style={{ textDecoration: 'none', color: 'inherit' }}
|
||||
>
|
||||
|
||||
@@ -25,7 +25,6 @@ import { useLingui } from '@lingui/react/macro';
|
||||
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
|
||||
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
|
||||
import { MangaCard } from '@/features/manga/components/cards/MangaCard.tsx';
|
||||
import type { SelectableCollectionReturnType } from '@/base/collection/hooks/useSelectableCollection.ts';
|
||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/base/components/buttons/StyledFab.tsx';
|
||||
import type { MangaCardProps } from '@/features/manga/Manga.types.ts';
|
||||
import type { MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
@@ -68,6 +67,7 @@ const createMangaCard = (
|
||||
selectedMangaIds?: MangaType['id'][],
|
||||
handleSelection?: DefaultGridProps['handleSelection'],
|
||||
mode?: MangaCardProps['mode'],
|
||||
onMigrateSelect?: DefaultGridProps['onMigrateSelect'],
|
||||
) => (
|
||||
<MangaCard
|
||||
manga={manga}
|
||||
@@ -76,10 +76,11 @@ const createMangaCard = (
|
||||
selected={isSelectModeActive ? selectedMangaIds?.includes(manga.id) : null}
|
||||
handleSelection={handleSelection}
|
||||
mode={mode}
|
||||
onMigrateSelect={onMigrateSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
type DefaultGridProps = Pick<MangaCardProps, 'mode'> & {
|
||||
type DefaultGridProps = Omit<MangaCardProps, 'manga' | 'selected'> & {
|
||||
isLoading: boolean;
|
||||
mangas: TManga[];
|
||||
inLibraryIndicator?: boolean;
|
||||
@@ -87,7 +88,6 @@ type DefaultGridProps = Pick<MangaCardProps, 'mode'> & {
|
||||
gridLayout?: GridLayout;
|
||||
isSelectModeActive?: boolean;
|
||||
selectedMangaIds?: Required<MangaType['id']>[];
|
||||
handleSelection?: SelectableCollectionReturnType<MangaType['id']>['handleSelection'];
|
||||
ref?: ForwardedRef<HTMLDivElement | null>;
|
||||
};
|
||||
|
||||
@@ -102,6 +102,7 @@ const HorizontalGrid = ({
|
||||
handleSelection,
|
||||
mode,
|
||||
ref,
|
||||
onMigrateSelect,
|
||||
}: DefaultGridProps) => (
|
||||
<Grid
|
||||
ref={ref}
|
||||
@@ -127,6 +128,7 @@ const HorizontalGrid = ({
|
||||
selectedMangaIds,
|
||||
handleSelection,
|
||||
mode,
|
||||
onMigrateSelect,
|
||||
)}
|
||||
</GridItemContainer>
|
||||
))
|
||||
@@ -149,6 +151,7 @@ const VerticalGrid = ({
|
||||
handleSelection,
|
||||
mode,
|
||||
ref,
|
||||
onMigrateSelect,
|
||||
}: DefaultGridProps & {
|
||||
hasNextPage: boolean;
|
||||
loadMore: () => void;
|
||||
@@ -175,6 +178,7 @@ const VerticalGrid = ({
|
||||
selectedMangaIds,
|
||||
handleSelection,
|
||||
mode,
|
||||
onMigrateSelect,
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -220,6 +224,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = ({
|
||||
mode,
|
||||
retry,
|
||||
gridWrapperProps,
|
||||
onMigrateSelect,
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -346,6 +351,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = ({
|
||||
selectedMangaIds={selectedMangaIds}
|
||||
handleSelection={handleSelection}
|
||||
mode={mode}
|
||||
onMigrateSelect={onMigrateSelect}
|
||||
/>
|
||||
) : (
|
||||
<VerticalGrid
|
||||
@@ -361,6 +367,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = ({
|
||||
selectedMangaIds={selectedMangaIds}
|
||||
handleSelection={handleSelection}
|
||||
mode={mode}
|
||||
onMigrateSelect={onMigrateSelect}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -84,7 +84,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
<>
|
||||
<CustomTooltip title={t`Migrate`}>
|
||||
<Link
|
||||
to={AppRoutes.migrate.childRoutes.search.path(
|
||||
to={AppRoutes.migrate.childRoutes.singleMangaSearch.path(
|
||||
manga.sourceId,
|
||||
manga.id,
|
||||
manga.title,
|
||||
@@ -156,7 +156,11 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
|
||||
<MenuItem
|
||||
key="migrate"
|
||||
component={Link}
|
||||
to={AppRoutes.migrate.childRoutes.search.path(manga.sourceId, manga.id, manga.title)}
|
||||
to={AppRoutes.migrate.childRoutes.singleMangaSearch.path(
|
||||
manga.sourceId,
|
||||
manga.id,
|
||||
manga.title,
|
||||
)}
|
||||
state={{ mangaTitle: manga.title }}
|
||||
style={{ textDecoration: 'none', color: 'inherit' }}
|
||||
>
|
||||
|
||||
@@ -7,12 +7,11 @@
|
||||
*/
|
||||
|
||||
import PopupState, { bindMenu } from 'material-ui-popup-state';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useLongPress } from 'use-long-press';
|
||||
import type { SingleModeProps } from '@/features/manga/components/MangaActionMenuItems.tsx';
|
||||
import { MangaActionMenuItems } from '@/features/manga/components/MangaActionMenuItems.tsx';
|
||||
import { Menu } from '@/base/components/menu/Menu.tsx';
|
||||
import { MigrateDialog } from '@/features/migration/components/MigrateDialog.tsx';
|
||||
import { useManageMangaLibraryState } from '@/features/manga/hooks/useManageMangaLibraryState.tsx';
|
||||
import { MangaGridCard } from '@/features/manga/components/cards/MangaGridCard.tsx';
|
||||
import { MangaListCard } from '@/features/manga/components/cards/MangaListCard.tsx';
|
||||
@@ -22,6 +21,14 @@ import { MangaBadges } from '@/features/manga/components/MangaBadges.tsx';
|
||||
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';
|
||||
|
||||
const getMangaLinkTo = (
|
||||
mode: MangaCardMode,
|
||||
@@ -35,7 +42,7 @@ const getMangaLinkTo = (
|
||||
case 'duplicate':
|
||||
return AppRoutes.manga.path(mangaId);
|
||||
case 'migrate.search':
|
||||
return AppRoutes.migrate.childRoutes.search.path(sourceId ?? '-1', mangaId, mangaTitle);
|
||||
return AppRoutes.migrate.childRoutes.singleMangaSearch.path(sourceId ?? '-1', mangaId, mangaTitle);
|
||||
case 'migrate.select':
|
||||
return '';
|
||||
default:
|
||||
@@ -44,8 +51,20 @@ const getMangaLinkTo = (
|
||||
};
|
||||
|
||||
export const MangaCard = memo((props: MangaCardProps) => {
|
||||
const { manga, gridLayout, inLibraryIndicator, selected, handleSelection, mode = 'default' } = props;
|
||||
const {
|
||||
manga,
|
||||
gridLayout,
|
||||
inLibraryIndicator,
|
||||
selected,
|
||||
handleSelection,
|
||||
mode = 'default',
|
||||
onMigrateSelect,
|
||||
} = props;
|
||||
const { id, firstUnreadChapter, downloadCount, unreadCount } = manga;
|
||||
|
||||
const { mangaId: mangaIdAsString } = useParams<{ mangaId: string }>();
|
||||
const migrationSourceMangaId = Number(mangaIdAsString);
|
||||
|
||||
const {
|
||||
settings: { showContinueReadingButton },
|
||||
} = useMetadataServerSettings();
|
||||
@@ -54,8 +73,6 @@ export const MangaCard = memo((props: MangaCardProps) => {
|
||||
|
||||
const mangaLinkTo = getMangaLinkTo(mode, manga.id, manga.sourceId, manga.title);
|
||||
|
||||
const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event: React.MouseEvent | React.TouchEvent, openMenu?: () => void) => {
|
||||
const isDefaultMode = mode === 'default';
|
||||
@@ -88,10 +105,42 @@ export const MangaCard = memo((props: MangaCardProps) => {
|
||||
}
|
||||
|
||||
if (isMigrateSelectMode) {
|
||||
setIsMigrateDialogOpen(true);
|
||||
const isBulkMigrationManualSearch = !!onMigrateSelect;
|
||||
if (isBulkMigrationManualSearch) {
|
||||
onMigrateSelect(manga);
|
||||
return;
|
||||
}
|
||||
|
||||
const migrate = () => {
|
||||
const optionsDialog = AwaitableComponent.showControlled(
|
||||
MigrationOptionsDialog,
|
||||
{
|
||||
mangaIdToMigrateTo: id,
|
||||
isMigrating: false,
|
||||
startMigration: async (options) => {
|
||||
makeToast(t`Migrating manga…`, 'info');
|
||||
|
||||
optionsDialog.update({ isMigrating: true });
|
||||
|
||||
try {
|
||||
await Mangas.migrate(migrationSourceMangaId, id, options);
|
||||
|
||||
optionsDialog.submit(options);
|
||||
|
||||
ReactRouter.navigate(AppRoutes.manga.path(id), { replace: true });
|
||||
} catch (e) {
|
||||
optionsDialog.update({ isMigrating: false, startMigration: () => migrate() });
|
||||
}
|
||||
},
|
||||
},
|
||||
{ id: `manga-migration-single-manga-${migrationSourceMangaId}-${id}` },
|
||||
);
|
||||
optionsDialog.promise.catch(defaultPromiseErrorHandler('MangaCard::migrate'));
|
||||
};
|
||||
migrate();
|
||||
}
|
||||
},
|
||||
[mode, selected, updateLibraryState, handleSelection],
|
||||
[mode, selected, updateLibraryState, handleSelection, migrationSourceMangaId],
|
||||
);
|
||||
|
||||
const longPressBind = useLongPress(
|
||||
@@ -111,54 +160,49 @@ export const MangaCard = memo((props: MangaCardProps) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isMigrateDialogOpen && (
|
||||
<MigrateDialog mangaIdToMigrateTo={manga.id} onClose={() => setIsMigrateDialogOpen(false)} />
|
||||
<PopupState variant="popover" popupId="manga-card-action-menu">
|
||||
{(popupState) => (
|
||||
<>
|
||||
<MangaCardComponent
|
||||
{...props}
|
||||
longPressBind={longPressBind}
|
||||
popupState={popupState}
|
||||
handleClick={handleClick}
|
||||
mangaLinkTo={mangaLinkTo}
|
||||
isInLibrary={isInLibrary}
|
||||
inLibraryIndicator={inLibraryIndicator}
|
||||
continueReadingButton={
|
||||
<ContinueReadingButton
|
||||
showContinueReadingButton={showContinueReadingButton && mode === 'default'}
|
||||
chapter={firstUnreadChapter}
|
||||
mangaLinkTo={mangaLinkTo}
|
||||
/>
|
||||
}
|
||||
mangaBadges={
|
||||
<MangaBadges
|
||||
inLibraryIndicator={inLibraryIndicator}
|
||||
isInLibrary={isInLibrary}
|
||||
unread={unreadCount}
|
||||
downloadCount={downloadCount}
|
||||
updateLibraryState={updateLibraryState}
|
||||
mode={mode}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{!!handleSelection && popupState.isOpen && (
|
||||
<Menu {...bindMenu(popupState)}>
|
||||
{(onClose, setHideMenu) => (
|
||||
<MangaActionMenuItems
|
||||
manga={manga as SingleModeProps['manga']}
|
||||
handleSelection={handleSelection}
|
||||
onClose={onClose}
|
||||
setHideMenu={setHideMenu}
|
||||
/>
|
||||
)}
|
||||
</Menu>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<PopupState variant="popover" popupId="manga-card-action-menu">
|
||||
{(popupState) => (
|
||||
<>
|
||||
<MangaCardComponent
|
||||
{...props}
|
||||
longPressBind={longPressBind}
|
||||
popupState={popupState}
|
||||
handleClick={handleClick}
|
||||
mangaLinkTo={mangaLinkTo}
|
||||
isInLibrary={isInLibrary}
|
||||
inLibraryIndicator={inLibraryIndicator}
|
||||
continueReadingButton={
|
||||
<ContinueReadingButton
|
||||
showContinueReadingButton={showContinueReadingButton && mode === 'default'}
|
||||
chapter={firstUnreadChapter}
|
||||
mangaLinkTo={mangaLinkTo}
|
||||
/>
|
||||
}
|
||||
mangaBadges={
|
||||
<MangaBadges
|
||||
inLibraryIndicator={inLibraryIndicator}
|
||||
isInLibrary={isInLibrary}
|
||||
unread={unreadCount}
|
||||
downloadCount={downloadCount}
|
||||
updateLibraryState={updateLibraryState}
|
||||
mode={mode}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{!!handleSelection && popupState.isOpen && (
|
||||
<Menu {...bindMenu(popupState)}>
|
||||
{(onClose, setHideMenu) => (
|
||||
<MangaActionMenuItems
|
||||
manga={manga as SingleModeProps['manga']}
|
||||
handleSelection={handleSelection}
|
||||
onClose={onClose}
|
||||
setHideMenu={setHideMenu}
|
||||
/>
|
||||
)}
|
||||
</Menu>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PopupState>
|
||||
</>
|
||||
</PopupState>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user