Optionally select bulk migration match based on highest chapter number
This commit is contained in:
@@ -37,6 +37,7 @@ export const DEFAULT_MIGRATION_STATE: MigrationState = {
|
|||||||
sourceIds: null,
|
sourceIds: null,
|
||||||
entries: {},
|
entries: {},
|
||||||
destinationSourceIds: [],
|
destinationSourceIds: [],
|
||||||
|
searchOptions: null,
|
||||||
migrateOptions: null,
|
migrateOptions: null,
|
||||||
searchProgress: { total: 0, completed: 0, success: 0, failed: 0 },
|
searchProgress: { total: 0, completed: 0, success: 0, failed: 0 },
|
||||||
migrationProgress: { total: 0, completed: 0, success: 0, failed: 0 },
|
migrationProgress: { total: 0, completed: 0, success: 0, failed: 0 },
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ export type MetadataMigrationSettings = {
|
|||||||
migrateSortSettings: SortSettings;
|
migrateSortSettings: SortSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface MigrationBulkSearchSettings {
|
||||||
|
selectHighestChapterNumberSource: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export enum MigrationPhase {
|
export enum MigrationPhase {
|
||||||
IDLE = 'idle',
|
IDLE = 'idle',
|
||||||
SELECT_SOURCE = 'select_source',
|
SELECT_SOURCE = 'select_source',
|
||||||
@@ -114,6 +118,7 @@ export interface MigrationState {
|
|||||||
sourceIds: SourceIdInfo['id'][] | null;
|
sourceIds: SourceIdInfo['id'][] | null;
|
||||||
entries: Record<MangaIdInfo['id'], TMigrationEntry>;
|
entries: Record<MangaIdInfo['id'], TMigrationEntry>;
|
||||||
destinationSourceIds: SourceIdInfo['id'][];
|
destinationSourceIds: SourceIdInfo['id'][];
|
||||||
|
searchOptions: MigrationBulkSearchSettings | null;
|
||||||
migrateOptions: Omit<MigrateOptions, 'mangaIdToMigrateTo'> | null;
|
migrateOptions: Omit<MigrateOptions, 'mangaIdToMigrateTo'> | null;
|
||||||
searchProgress: MigrationProgress;
|
searchProgress: MigrationProgress;
|
||||||
migrationProgress: MigrationProgress;
|
migrationProgress: MigrationProgress;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { devtools, persist } from 'zustand/middleware';
|
|||||||
import {
|
import {
|
||||||
type MigratableEntry,
|
type MigratableEntry,
|
||||||
type MigrateOptions,
|
type MigrateOptions,
|
||||||
|
type MigrationBulkSearchSettings,
|
||||||
MigrationEntryStatus,
|
MigrationEntryStatus,
|
||||||
type MigrationMatch,
|
type MigrationMatch,
|
||||||
MigrationPhase,
|
MigrationPhase,
|
||||||
@@ -330,27 +331,28 @@ export class MigrationManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static async startSearch(destinationSourceIds: SourceIdInfo['id'][]): Promise<void> {
|
static async startSearch(
|
||||||
|
destinationSourceIds: SourceIdInfo['id'][],
|
||||||
|
options: MigrationBulkSearchSettings,
|
||||||
|
): Promise<void> {
|
||||||
MigrationManager.ensureIsInValidPhase([MigrationPhase.SELECTING_SOURCES]);
|
MigrationManager.ensureIsInValidPhase([MigrationPhase.SELECTING_SOURCES]);
|
||||||
|
|
||||||
MigrationManager.updateState((draft) => {
|
|
||||||
draft.destinationSourceIds = destinationSourceIds;
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = MigrationManager.getState();
|
const state = MigrationManager.getState();
|
||||||
const entryIds = Object.keys(state.entries).map(Number);
|
const entryIds = Object.keys(state.entries).map(Number);
|
||||||
|
|
||||||
MigrationManager.updateState((draft) => {
|
MigrationManager.updateState((draft) => {
|
||||||
draft.phase = MigrationPhase.SEARCHING;
|
draft.phase = MigrationPhase.SEARCHING;
|
||||||
|
draft.destinationSourceIds = destinationSourceIds;
|
||||||
|
draft.searchOptions = options;
|
||||||
draft.searchProgress = { total: entryIds.length, completed: 0, success: 0, failed: 0 };
|
draft.searchProgress = { total: entryIds.length, completed: 0, success: 0, failed: 0 };
|
||||||
draft.startedAt = Date.now();
|
draft.startedAt = Date.now();
|
||||||
draft.groupExpandState = MIGRATE_SEARCH_ENTRY_GROUP_EXPAND_DEFAULT_STATE;
|
draft.groupExpandState = MIGRATE_SEARCH_ENTRY_GROUP_EXPAND_DEFAULT_STATE;
|
||||||
});
|
});
|
||||||
|
|
||||||
await MigrationManager.search(Object.values(state.entries));
|
await MigrationManager.search(Object.values(state.entries), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async search(entries: TMigrationEntry[]): Promise<void> {
|
private static async search(entries: TMigrationEntry[], options: MigrationBulkSearchSettings): Promise<void> {
|
||||||
const { signal } = MigrationManager.abortAndCreateAbortController('search');
|
const { signal } = MigrationManager.abortAndCreateAbortController('search');
|
||||||
|
|
||||||
const searchPromises = entries.map((entry) =>
|
const searchPromises = entries.map((entry) =>
|
||||||
@@ -359,7 +361,7 @@ export class MigrationManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await MigrationManager.searchForManga(entry.mangaId, entry.mangaTitle, signal);
|
await MigrationManager.searchForManga(entry.mangaId, entry.mangaTitle, signal, options);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -481,16 +483,16 @@ export class MigrationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async resume(): Promise<void> {
|
static async resume(): Promise<void> {
|
||||||
const state = MigrationManager.getState();
|
const { phase, migrateOptions, searchOptions, entries } = MigrationManager.getState();
|
||||||
|
|
||||||
const isResumeablePhase = RESUMABLE_PHASES.includes(state.phase);
|
const isResumeablePhase = RESUMABLE_PHASES.includes(phase);
|
||||||
if (!isResumeablePhase) {
|
if (!isResumeablePhase) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resumeMigrationPhase = state.phase === MigrationPhase.MIGRATING && state.migrateOptions;
|
const resumeMigrationPhase = phase === MigrationPhase.MIGRATING && migrateOptions;
|
||||||
if (resumeMigrationPhase) {
|
if (resumeMigrationPhase) {
|
||||||
assertIsDefined(state.migrateOptions);
|
assertIsDefined(migrateOptions);
|
||||||
|
|
||||||
const migratableEntries = MigrationManager.getMigratableEntries();
|
const migratableEntries = MigrationManager.getMigratableEntries();
|
||||||
|
|
||||||
@@ -500,12 +502,14 @@ export class MigrationManager {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await MigrationManager.migrate(migratableEntries, state.migrateOptions);
|
await MigrationManager.migrate(migratableEntries, migrateOptions);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingEntries = Object.values(state.entries).filter((entry) =>
|
assertIsDefined(searchOptions);
|
||||||
|
|
||||||
|
const pendingEntries = Object.values(entries).filter((entry) =>
|
||||||
[MigrationEntryStatus.PENDING, MigrationEntryStatus.SEARCHING].includes(entry.status),
|
[MigrationEntryStatus.PENDING, MigrationEntryStatus.SEARCHING].includes(entry.status),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -515,7 +519,7 @@ export class MigrationManager {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await MigrationManager.search(pendingEntries);
|
await MigrationManager.search(pendingEntries, searchOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
static reset(): void {
|
static reset(): void {
|
||||||
@@ -658,6 +662,7 @@ export class MigrationManager {
|
|||||||
mangaTitle: string,
|
mangaTitle: string,
|
||||||
sourceId: SourceIdInfo['id'],
|
sourceId: SourceIdInfo['id'],
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
|
{ selectHighestChapterNumberSource }: MigrationBulkSearchSettings,
|
||||||
): Promise<MangaMigrationFieldsFragment[]> {
|
): Promise<MangaMigrationFieldsFragment[]> {
|
||||||
if (signal.aborted) {
|
if (signal.aborted) {
|
||||||
throw new Error(signal.reason);
|
throw new Error(signal.reason);
|
||||||
@@ -668,7 +673,7 @@ export class MigrationManager {
|
|||||||
throw new Error(signal.reason);
|
throw new Error(signal.reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MigrationManager.hasHigherSourcePriorityMatch(mangaId, sourceId)) {
|
if (!selectHighestChapterNumberSource && MigrationManager.hasHigherSourcePriorityMatch(mangaId, sourceId)) {
|
||||||
throw new Error('Entry already has a selected match from a higher priority source');
|
throw new Error('Entry already has a selected match from a higher priority source');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -723,7 +728,10 @@ export class MigrationManager {
|
|||||||
mangaId: MangaIdInfo['id'],
|
mangaId: MangaIdInfo['id'],
|
||||||
mangaTitle: string,
|
mangaTitle: string,
|
||||||
mainSignal: AbortSignal,
|
mainSignal: AbortSignal,
|
||||||
|
options: MigrationBulkSearchSettings,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const { selectHighestChapterNumberSource } = options;
|
||||||
|
|
||||||
const state = MigrationManager.getState();
|
const state = MigrationManager.getState();
|
||||||
const entry = state.entries[mangaId];
|
const entry = state.entries[mangaId];
|
||||||
|
|
||||||
@@ -745,7 +753,10 @@ export class MigrationManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MigrationManager.hasHigherSourcePriorityMatch(mangaId, destSourceId)) {
|
if (
|
||||||
|
!selectHighestChapterNumberSource &&
|
||||||
|
MigrationManager.hasHigherSourcePriorityMatch(mangaId, destSourceId)
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -756,6 +767,7 @@ export class MigrationManager {
|
|||||||
mangaTitle,
|
mangaTitle,
|
||||||
destSourceId,
|
destSourceId,
|
||||||
signal,
|
signal,
|
||||||
|
options,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
MigrationManager.updateState((draft) => {
|
MigrationManager.updateState((draft) => {
|
||||||
@@ -780,6 +792,9 @@ export class MigrationManager {
|
|||||||
|
|
||||||
MigrationManager.updateState((draft) => {
|
MigrationManager.updateState((draft) => {
|
||||||
const draftEntry = draft.entries[mangaId];
|
const draftEntry = draft.entries[mangaId];
|
||||||
|
const draftMatchEntry = draftEntry.selectedMatchMangaId
|
||||||
|
? draft.entries[draftEntry.selectedMatchMangaId]
|
||||||
|
: null;
|
||||||
|
|
||||||
const matches = foundMatches.map((manga) => ({
|
const matches = foundMatches.map((manga) => ({
|
||||||
id: manga.id,
|
id: manga.id,
|
||||||
@@ -793,26 +808,53 @@ export class MigrationManager {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
draftEntry.destSourceIdToSearchState[destSourceId] = true;
|
draftEntry.destSourceIdToSearchState[destSourceId] = true;
|
||||||
|
|
||||||
draftEntry.searchMatches = [...draftEntry.searchMatches, ...matches];
|
draftEntry.searchMatches = [...draftEntry.searchMatches, ...matches];
|
||||||
|
|
||||||
if (!MigrationManager.hasHigherSourcePriorityMatch(mangaId, destSourceId)) {
|
const matchesByChapterNumber = Object.groupBy(
|
||||||
const matchesByChapterNumber = Object.groupBy(
|
matches,
|
||||||
matches,
|
(match) => match.latestChapterNumber ?? -1,
|
||||||
(match) => match.latestChapterNumber ?? -1,
|
);
|
||||||
);
|
const latestChapterNumber = Math.max(
|
||||||
const latestChapterNumber = Math.max(
|
...Object.keys(matchesByChapterNumber).map((chapterNumber) => Number(chapterNumber)),
|
||||||
...Object.keys(matchesByChapterNumber).map((chapterNumber) => Number(chapterNumber)),
|
);
|
||||||
);
|
const bestMatch = matchesByChapterNumber[latestChapterNumber]?.[0];
|
||||||
const bestMatch = matchesByChapterNumber[latestChapterNumber]?.[0];
|
|
||||||
|
|
||||||
assertIsDefined(bestMatch);
|
assertIsDefined(bestMatch);
|
||||||
|
|
||||||
|
const entryLatestChapterNumber = draftEntry.latestChapterNumber ?? Number.MIN_SAFE_INTEGER;
|
||||||
|
const selectedMatchLatestChapterNumber =
|
||||||
|
draftMatchEntry?.latestChapterNumber ?? Number.MIN_SAFE_INTEGER;
|
||||||
|
|
||||||
|
const hasNewerChapterVsEntry = latestChapterNumber > entryLatestChapterNumber;
|
||||||
|
const hasNewerChapterVsSelectedMatch = latestChapterNumber > selectedMatchLatestChapterNumber;
|
||||||
|
const hasNewerChapter = hasNewerChapterVsEntry && hasNewerChapterVsSelectedMatch;
|
||||||
|
|
||||||
|
const hasSameLatestChapterAsSelectedMatch =
|
||||||
|
!!draftMatchEntry && selectedMatchLatestChapterNumber === latestChapterNumber;
|
||||||
|
|
||||||
|
const hasHigherSourcePriority = !MigrationManager.hasHigherSourcePriorityMatch(
|
||||||
|
mangaId,
|
||||||
|
destSourceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isPreferredSourcePriorityMatch =
|
||||||
|
hasHigherSourcePriority && !selectHighestChapterNumberSource;
|
||||||
|
const isPreferredChapterNumberMatch =
|
||||||
|
selectHighestChapterNumberSource &&
|
||||||
|
(hasNewerChapter ||
|
||||||
|
(hasHigherSourcePriority && hasSameLatestChapterAsSelectedMatch) ||
|
||||||
|
!draftMatchEntry);
|
||||||
|
|
||||||
|
const isPreferredMatch = isPreferredSourcePriorityMatch || isPreferredChapterNumberMatch;
|
||||||
|
if (isPreferredMatch) {
|
||||||
draftEntry.selectedMatchMangaId = bestMatch.id;
|
draftEntry.selectedMatchMangaId = bestMatch.id;
|
||||||
draftEntry.selectedMatchSourceId = destSourceId;
|
draftEntry.selectedMatchSourceId = destSourceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!MigrationManager.isHigherPrioritySourceUnsettled(mangaId, destSourceId)) {
|
if (
|
||||||
|
!selectHighestChapterNumberSource &&
|
||||||
|
!MigrationManager.isHigherPrioritySourceUnsettled(mangaId, destSourceId)
|
||||||
|
) {
|
||||||
searchController.abort(`Found best match in source "${destSourceId}"`);
|
searchController.abort(`Found best match in source "${destSourceId}"`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -908,7 +950,8 @@ export class MigrationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async retryEntry(id: MangaIdInfo['id']): Promise<void> {
|
static async retryEntry(id: MangaIdInfo['id']): Promise<void> {
|
||||||
const entry = MigrationManager.getState().entries[id];
|
const { entries, migrateOptions, searchOptions } = MigrationManager.getState();
|
||||||
|
const entry = entries[id];
|
||||||
|
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
return;
|
return;
|
||||||
@@ -923,8 +966,10 @@ export class MigrationManager {
|
|||||||
draft.searchProgress.failed -= 1;
|
draft.searchProgress.failed -= 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assertIsDefined(searchOptions);
|
||||||
|
|
||||||
await MigrationManager.mangaProcessQueue(() =>
|
await MigrationManager.mangaProcessQueue(() =>
|
||||||
MigrationManager.searchForManga(id, entry.mangaTitle, signal),
|
MigrationManager.searchForManga(id, entry.mangaTitle, signal, searchOptions),
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -936,11 +981,10 @@ export class MigrationManager {
|
|||||||
draft.migrationProgress.failed -= 1;
|
draft.migrationProgress.failed -= 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
const migrationOptions = MigrationManager.getState().migrateOptions;
|
assertIsDefined(migrateOptions);
|
||||||
assertIsDefined(migrationOptions);
|
|
||||||
|
|
||||||
await MigrationManager.mangaProcessQueue(() =>
|
await MigrationManager.mangaProcessQueue(() =>
|
||||||
MigrationManager.migrateSingleEntry(id, migrationOptions, signal),
|
MigrationManager.migrateSingleEntry(id, migrateOptions, signal),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
|
||||||
|
import type { AwaitableComponentProps } from 'awaitable-component';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import type { MigrationBulkSearchSettings } from '@/features/migration/Migration.types.ts';
|
||||||
|
|
||||||
|
export const MigrationBulkSearchOptionsDialog = ({
|
||||||
|
isVisible,
|
||||||
|
onDismiss,
|
||||||
|
onSubmit,
|
||||||
|
onExitComplete,
|
||||||
|
}: AwaitableComponentProps<MigrationBulkSearchSettings>) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
const [selectHighestChapterNumberSource, setSelectHighestChapterNumberSource] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isVisible} fullWidth onClose={onDismiss} onTransitionExited={onExitComplete}>
|
||||||
|
<DialogTitle>{t`Search options`}</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
sx={{
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WarningIcon color="warning" />
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
marginLeft: '10px',
|
||||||
|
marginTop: '5px',
|
||||||
|
whiteSpace: 'pre-line',
|
||||||
|
}}
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{t`These options are slow and dangerous and may lead to restrictions from sources`}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<CheckboxInput
|
||||||
|
label={
|
||||||
|
<Stack
|
||||||
|
sx={{
|
||||||
|
// Padding comes from the MUI Checkbox component
|
||||||
|
pt: '9px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography>{t`Match based on chapter number`}</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="textSecondary"
|
||||||
|
>{t`If enabled, chooses the match furthest ahead.\nOtherwise, picks the first match by source priority.`}</Typography>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
alignItems: 'start',
|
||||||
|
}}
|
||||||
|
checked={selectHighestChapterNumberSource}
|
||||||
|
onChange={(_, checked) => setSelectHighestChapterNumberSource(checked)}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onDismiss}>{t`Cancel`}</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() =>
|
||||||
|
onSubmit({
|
||||||
|
selectHighestChapterNumberSource,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t`Search`}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -28,6 +28,8 @@ import { Sources } from '@/features/source/services/Sources.ts';
|
|||||||
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
|
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
|
||||||
import ToggleOnIcon from '@mui/icons-material/ToggleOn';
|
import ToggleOnIcon from '@mui/icons-material/ToggleOn';
|
||||||
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
||||||
|
import { MigrationBulkSearchOptionsDialog } from '@/features/migration/components/MigrationBulkSearchOptionsDialog.tsx';
|
||||||
|
import { AwaitableComponent } from 'awaitable-component';
|
||||||
|
|
||||||
export const MigrationSelectDestinationSources = () => {
|
export const MigrationSelectDestinationSources = () => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
@@ -168,7 +170,15 @@ export const MigrationSelectDestinationSources = () => {
|
|||||||
bottom: (theme) => theme.spacing(2),
|
bottom: (theme) => theme.spacing(2),
|
||||||
right: (theme) => theme.spacing(2),
|
right: (theme) => theme.spacing(2),
|
||||||
}}
|
}}
|
||||||
onClick={() => MigrationManager.startSearch(selectedItemIds)}
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const searchOptions = await AwaitableComponent.show(MigrationBulkSearchOptionsDialog);
|
||||||
|
|
||||||
|
await MigrationManager.startSearch(selectedItemIds, searchOptions);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t`Start Search`}
|
{t`Start Search`}
|
||||||
</Fab>
|
</Fab>
|
||||||
|
|||||||
@@ -670,6 +670,7 @@ msgstr "Call timeout"
|
|||||||
#: src/features/category/components/CategoriesInclusionSetting.tsx
|
#: src/features/category/components/CategoriesInclusionSetting.tsx
|
||||||
#: src/features/category/components/CategorySelect.tsx
|
#: src/features/category/components/CategorySelect.tsx
|
||||||
#: src/features/category/components/CreateOrEditCategoryDialog.tsx
|
#: src/features/category/components/CreateOrEditCategoryDialog.tsx
|
||||||
|
#: src/features/migration/components/MigrationBulkSearchOptionsDialog.tsx
|
||||||
#: src/features/migration/components/MigrationOptionsDialog.tsx
|
#: src/features/migration/components/MigrationOptionsDialog.tsx
|
||||||
#: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx
|
#: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx
|
||||||
#: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx
|
#: src/features/settings/components/globalUpdate/GlobalUpdateSettingsEntries.tsx
|
||||||
@@ -1802,7 +1803,7 @@ msgstr "How many chapters should get downloaded while reading."
|
|||||||
msgid "How much time FlareSolverr has to handle the request"
|
msgid "How much time FlareSolverr has to handle the request"
|
||||||
msgstr "How much time FlareSolverr has to handle the request"
|
msgstr "How much time FlareSolverr has to handle the request"
|
||||||
|
|
||||||
#: src/features/migration/components/MigrationOptionsDialog.tsx
|
#: src/features/migration/components/MigrationBulkSearchOptionsDialog.tsx
|
||||||
msgid ""
|
msgid ""
|
||||||
"If enabled, chooses the match furthest ahead.\n"
|
"If enabled, chooses the match furthest ahead.\n"
|
||||||
"Otherwise, picks the first match by source priority."
|
"Otherwise, picks the first match by source priority."
|
||||||
@@ -2250,7 +2251,7 @@ msgstr "Mark selected as read"
|
|||||||
msgid "Mark selected as unread"
|
msgid "Mark selected as unread"
|
||||||
msgstr "Mark selected as unread"
|
msgstr "Mark selected as unread"
|
||||||
|
|
||||||
#: src/features/migration/components/MigrationOptionsDialog.tsx
|
#: src/features/migration/components/MigrationBulkSearchOptionsDialog.tsx
|
||||||
msgid "Match based on chapter number"
|
msgid "Match based on chapter number"
|
||||||
msgstr "Match based on chapter number"
|
msgstr "Match based on chapter number"
|
||||||
|
|
||||||
@@ -2930,6 +2931,7 @@ msgid "Scroll forward"
|
|||||||
msgstr "Scroll forward"
|
msgstr "Scroll forward"
|
||||||
|
|
||||||
#: src/base/components/AppbarSearch.tsx
|
#: src/base/components/AppbarSearch.tsx
|
||||||
|
#: src/features/migration/components/MigrationBulkSearchOptionsDialog.tsx
|
||||||
msgid "Search"
|
msgid "Search"
|
||||||
msgstr "Search"
|
msgstr "Search"
|
||||||
|
|
||||||
@@ -2954,6 +2956,10 @@ msgstr ""
|
|||||||
"Search for a ID via \"id:<ID>\" (e.g. \"id:13\")\n"
|
"Search for a ID via \"id:<ID>\" (e.g. \"id:13\")\n"
|
||||||
"Limit search to your lists via \"my:<Title>\" (e.g. \"my:One Piece\")"
|
"Limit search to your lists via \"my:<Title>\" (e.g. \"my:One Piece\")"
|
||||||
|
|
||||||
|
#: src/features/migration/components/MigrationBulkSearchOptionsDialog.tsx
|
||||||
|
msgid "Search options"
|
||||||
|
msgstr "Search options"
|
||||||
|
|
||||||
#: src/features/settings/components/images/Processing.tsx
|
#: src/features/settings/components/images/Processing.tsx
|
||||||
msgid "Search parameters"
|
msgid "Search parameters"
|
||||||
msgstr "Search parameters"
|
msgstr "Search parameters"
|
||||||
@@ -3483,7 +3489,7 @@ msgstr "There is no next chapter"
|
|||||||
msgid "There is no previous chapter"
|
msgid "There is no previous chapter"
|
||||||
msgstr "There is no previous chapter"
|
msgstr "There is no previous chapter"
|
||||||
|
|
||||||
#: src/features/migration/components/MigrationOptionsDialog.tsx
|
#: src/features/migration/components/MigrationBulkSearchOptionsDialog.tsx
|
||||||
msgid "These options are slow and dangerous and may lead to restrictions from sources"
|
msgid "These options are slow and dangerous and may lead to restrictions from sources"
|
||||||
msgstr "These options are slow and dangerous and may lead to restrictions from sources"
|
msgstr "These options are slow and dangerous and may lead to restrictions from sources"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user