Add bulk migration

This commit is contained in:
schroda
2026-03-13 22:41:28 +01:00
parent c4c2a616f6
commit f6302fe3aa
75 changed files with 4207 additions and 669 deletions

View File

@@ -10,7 +10,7 @@ import Card from '@mui/material/Card';
import CardActionArea from '@mui/material/CardActionArea';
import Typography from '@mui/material/Typography';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
import { StringParam, useQueryParam } from 'use-query-params';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
@@ -28,7 +28,7 @@ import { AppbarSearch } from '@/base/components/AppbarSearch.tsx';
import { useDebounce } from '@/base/hooks/useDebounce.ts';
import type { MangaCardProps } from '@/features/manga/Manga.types.ts';
import { EmptyView } from '@/base/components/feedback/EmptyView.tsx';
import { STABLE_EMPTY_ARRAY } from '@/base/Base.constants.ts';
import { STABLE_EMPTY_ARRAY, STABLE_EMPTY_OBJECT } from '@/base/Base.constants.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
import { BaseMangaGrid } from '@/features/manga/components/BaseMangaGrid.tsx';
@@ -56,6 +56,9 @@ import { MUIUtil } from '@/lib/mui/MUI.util.ts';
import type { MetadataBrowseSettings } from '@/features/browse/Browse.types.ts';
import { SourceLanguageSelect } from '@/features/source/components/SourceLanguageSelect.tsx';
import { SearchParam } from '@/base/Base.types.ts';
import { MigrationManager } from '@/features/migration/MigrationManager.ts';
import { assertIsDefined } from '@/base/Asserts.ts';
import { useBackButton } from '@/base/hooks/useBackButton.ts';
type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean; error: any };
type SourceToLoadingStateMap = Map<string, SourceLoadingState>;
@@ -122,12 +125,13 @@ const SourceSearchPreview = React.memo(
emptyQuery,
mode,
shouldShowOnlySourcesWithResults,
onMigrateSelect,
}: {
source: SourceIdInfo & SourceDisplayNameInfo & SourceNameInfo & SourceLanguageInfo;
onSearchRequestFinished: (source: SourceIdInfo, state: SourceLoadingState) => void;
searchString: string | null | undefined;
emptyQuery: boolean;
} & Pick<MangaCardProps, 'mode'> &
} & Pick<MangaCardProps, 'mode' | 'onMigrateSelect'> &
Pick<MetadataBrowseSettings, 'shouldShowOnlySourcesWithResults'>) => {
const { t } = useLingui();
@@ -230,6 +234,7 @@ const SourceSearchPreview = React.memo(
message={errorMessage}
inLibraryIndicator
mode={mode}
onMigrateSelect={onMigrateSelect}
/>
)}
</Box>
@@ -237,15 +242,21 @@ const SourceSearchPreview = React.memo(
},
);
export const SearchAll: React.FC = () => {
export const SearchAll = ({
migrationDestinationSourceIds,
}: {
migrationDestinationSourceIds?: SourceIdInfo['id'][];
}) => {
const { t } = useLingui();
const navigate = useNavigate();
const handleBack = useBackButton();
const { pathname, state } = useLocation<{ mangaTitle?: string; shouldShowOnlyPinnedSources?: boolean }>();
const { ref: filterHeaderRef, height: filterHeaderHeight } = useElementSize();
const shouldShowOnlyPinnedSources = state?.shouldShowOnlyPinnedSources ?? true;
const isMigrateMode = pathname.startsWith('/migrate/source');
const isMigrateMode = pathname.startsWith('/migrate/source') || pathname.startsWith('/migrate/manual-search');
const { mangaId } = useParams<{ mangaId?: string }>() ?? STABLE_EMPTY_OBJECT;
const [query] = useQueryParam(SearchParam.QUERY, StringParam);
const searchString = useDebounce(query, TRIGGER_SEARCH_THRESHOLD);
@@ -255,7 +266,14 @@ export const SearchAll: React.FC = () => {
} = useMetadataServerSettings();
const { data, loading, error, refetch } = requestManager.useGetSourceList({ notifyOnNetworkStatusChange: true });
const sources = data?.sources.nodes ?? STABLE_EMPTY_ARRAY;
const tmpSources = data?.sources.nodes ?? STABLE_EMPTY_ARRAY;
const sources = useMemo(
() =>
tmpSources.filter(
(source) => !migrationDestinationSourceIds || migrationDestinationSourceIds.includes(source.id),
),
[tmpSources, migrationDestinationSourceIds],
);
const [sourceToLoadingStateMap, setSourceToLoadingStateMap] = useState<SourceToLoadingStateMap>(new Map());
const debouncedSourceToLoadingStateMap = useDebounce(sourceToLoadingStateMap, 500);
@@ -398,6 +416,19 @@ export const SearchAll: React.FC = () => {
emptyQuery={!query}
mode={isMigrateMode ? 'migrate.select' : 'source'}
shouldShowOnlySourcesWithResults={shouldShowOnlySourcesWithResults}
onMigrateSelect={
migrationDestinationSourceIds
? (match) => {
assertIsDefined(mangaId);
MigrationManager.selectManualMatch(Number(mangaId), {
...match,
sourceTitle: Sources.getFromCache(match.sourceId)?.displayName,
latestChapterNumber: undefined,
});
handleBack();
}
: undefined
}
/>
))}
</Box>