Improve bulk migration render performance
This commit is contained in:
@@ -11,58 +11,60 @@ import type { ButtonProps } from '@mui/material/Button';
|
|||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Collapse from '@mui/material/Collapse';
|
import Collapse from '@mui/material/Collapse';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import {} from 'react';
|
import { memo } from 'react';
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||||
import { MigrationEntry } from '@/features/migration/components/migration-entry/MigrationEntry.tsx';
|
import { MigrationEntry } from '@/features/migration/components/migration-entry/MigrationEntry.tsx';
|
||||||
import { MigrationManager } from '@/features/migration/MigrationManager.ts';
|
import { MigrationManager } from '@/features/migration/MigrationManager.ts';
|
||||||
|
|
||||||
export const MigrationEntryGroup = ({
|
export const MigrationEntryGroup = memo(
|
||||||
status,
|
({
|
||||||
title,
|
status,
|
||||||
entries,
|
title,
|
||||||
color,
|
entries,
|
||||||
isMigrating = false,
|
color,
|
||||||
}: {
|
isMigrating = false,
|
||||||
status: MigrationEntryStatus;
|
}: {
|
||||||
title: string;
|
status: MigrationEntryStatus;
|
||||||
entries: TMigrationEntry[];
|
title: string;
|
||||||
color: ButtonProps['color'];
|
entries: TMigrationEntry[];
|
||||||
isMigrating?: boolean;
|
color: ButtonProps['color'];
|
||||||
}) => {
|
isMigrating?: boolean;
|
||||||
const isExpanded = MigrationManager.useGroupExpandState(status);
|
}) => {
|
||||||
|
const isExpanded = MigrationManager.useGroupExpandState(status);
|
||||||
|
|
||||||
if (!entries.length) {
|
if (!entries.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack sx={{ width: '100%', gap: 2 }}>
|
<Stack sx={{ width: '100%', gap: 2 }}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => MigrationManager.setGroupExpandState(status, !isExpanded)}
|
onClick={() => MigrationManager.setGroupExpandState(status, !isExpanded)}
|
||||||
color={color}
|
color={color}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
startIcon={isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||||
size="large"
|
size="large"
|
||||||
sx={{
|
sx={{
|
||||||
py: 2,
|
py: 2,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
'& .MuiButton-startIcon': {
|
'& .MuiButton-startIcon': {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: (theme) => theme.spacing(4),
|
left: (theme) => theme.spacing(4),
|
||||||
margin: 0,
|
margin: 0,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Button>
|
</Button>
|
||||||
<Collapse in={isExpanded} unmountOnExit>
|
<Collapse in={isExpanded} unmountOnExit>
|
||||||
<Stack sx={{ gap: 1 }}>
|
<Stack sx={{ gap: 1 }}>
|
||||||
{entries.map((entry) => (
|
{entries.map((entry) => (
|
||||||
<MigrationEntry key={entry.mangaId} entry={entry} isMigrating={isMigrating} />
|
<MigrationEntry key={entry.mangaId} entry={entry} isMigrating={isMigrating} />
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ import { Sources } from '@/features/source/services/Sources';
|
|||||||
import type { TMigratableSource } from '@/features/migration/Migration.types.ts';
|
import type { TMigratableSource } from '@/features/migration/Migration.types.ts';
|
||||||
import { ReactRouter } from '@/lib/react-router/ReactRouter.ts';
|
import { ReactRouter } from '@/lib/react-router/ReactRouter.ts';
|
||||||
import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
import { AppRoutes } from '@/base/AppRoute.constants.ts';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
export const MigrationCard = (source: TMigratableSource) => {
|
export const MigrationCard = memo((source: TMigratableSource) => {
|
||||||
const { id, name, lang, iconUrl, mangaCount } = source;
|
const { id, name, lang, iconUrl, mangaCount } = source;
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
@@ -66,4 +67,4 @@ export const MigrationCard = (source: TMigratableSource) => {
|
|||||||
</CardActionArea>
|
</CardActionArea>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Card from '@mui/material/Card';
|
import Card from '@mui/material/Card';
|
||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
@@ -36,55 +36,57 @@ import { languageCodeToName } from '@/base/utils/Languages.ts';
|
|||||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/base/components/buttons/StyledFab.tsx';
|
import { DEFAULT_FULL_FAB_HEIGHT } from '@/base/components/buttons/StyledFab.tsx';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
|
|
||||||
const SourceCard = ({
|
const SourceCard = memo(
|
||||||
source,
|
({
|
||||||
onToggle,
|
source,
|
||||||
isCurrentSource,
|
onToggle,
|
||||||
isSelected,
|
isCurrentSource,
|
||||||
isDragging,
|
isSelected,
|
||||||
}: {
|
isDragging,
|
||||||
source: SourceItem;
|
}: {
|
||||||
onToggle: (id: SourceIdInfo['id']) => void;
|
source: SourceItem;
|
||||||
isCurrentSource: boolean;
|
onToggle: (id: SourceIdInfo['id']) => void;
|
||||||
isSelected: boolean;
|
isCurrentSource: boolean;
|
||||||
isDragging?: boolean;
|
isSelected: boolean;
|
||||||
}) => {
|
isDragging?: boolean;
|
||||||
const { t } = useLingui();
|
}) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledGroupItemWrapper>
|
<StyledGroupItemWrapper>
|
||||||
<Card>
|
<Card>
|
||||||
<CardActionArea onClick={() => onToggle(source.id)}>
|
<CardActionArea onClick={() => onToggle(source.id)}>
|
||||||
<ListCardContent sx={{ justifyContent: 'space-between' }}>
|
<ListCardContent sx={{ justifyContent: 'space-between' }}>
|
||||||
<Stack sx={{ flexFlow: 'row', gap: 1, alignItems: 'center' }}>
|
<Stack sx={{ flexFlow: 'row', gap: 1, alignItems: 'center' }}>
|
||||||
<ListCardAvatar
|
<ListCardAvatar
|
||||||
iconUrl={requestManager.getValidImgUrlFor(source.iconUrl)}
|
iconUrl={requestManager.getValidImgUrlFor(source.iconUrl)}
|
||||||
alt={source.name}
|
alt={source.name}
|
||||||
slots={{ spinnerImageProps: { ignoreQueue: true } }}
|
slots={{ spinnerImageProps: { ignoreQueue: true } }}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||||
<Typography variant="h6" component="h3">
|
<Typography variant="h6" component="h3">
|
||||||
{source.name}
|
{source.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption">{languageCodeToName(source.lang)}</Typography>
|
<Typography variant="caption">{languageCodeToName(source.lang)}</Typography>
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
<Stack sx={{ flexDirection: 'row', gap: 4 }}>
|
|
||||||
{isCurrentSource && (
|
|
||||||
<Chip size="small" label={t`Current source`} color="primary" variant="outlined" />
|
|
||||||
)}
|
|
||||||
{isSelected && (
|
|
||||||
<Box>
|
|
||||||
<DragHandle sx={{ mr: 2, cursor: isDragging ? 'grabbing' : 'grab' }} />
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
</Stack>
|
||||||
</Stack>
|
<Stack sx={{ flexDirection: 'row', gap: 4 }}>
|
||||||
</ListCardContent>
|
{isCurrentSource && (
|
||||||
</CardActionArea>
|
<Chip size="small" label={t`Current source`} color="primary" variant="outlined" />
|
||||||
</Card>
|
)}
|
||||||
</StyledGroupItemWrapper>
|
{isSelected && (
|
||||||
);
|
<Box>
|
||||||
};
|
<DragHandle sx={{ mr: 2, cursor: isDragging ? 'grabbing' : 'grab' }} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</ListCardContent>
|
||||||
|
</CardActionArea>
|
||||||
|
</Card>
|
||||||
|
</StyledGroupItemWrapper>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const MigrationSourceList = ({
|
export const MigrationSourceList = ({
|
||||||
sources,
|
sources,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import type { MigrationMatch, TMigrationEntry } from '@/features/migration/Migration.types.ts';
|
import type { MigrationMatch, TMigrationEntry } from '@/features/migration/Migration.types.ts';
|
||||||
import { MigrationManager } from '@/features/migration/MigrationManager.ts';
|
import { MigrationManager } from '@/features/migration/MigrationManager.ts';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import { useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
|
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
|
||||||
import { applyStyles } from '@/base/utils/ApplyStyles.ts';
|
import { applyStyles } from '@/base/utils/ApplyStyles.ts';
|
||||||
import { MigrationSourceEntry } from '@/features/migration/components/migration-entry/MigrationSourceEntry.tsx';
|
import { MigrationSourceEntry } from '@/features/migration/components/migration-entry/MigrationSourceEntry.tsx';
|
||||||
@@ -28,214 +28,227 @@ import Divider from '@mui/material/Divider';
|
|||||||
import { MigrationEntryStatusIndicator } from '@/features/migration/components/migration-entry/MigrationEntryStatusIndicator.tsx';
|
import { MigrationEntryStatusIndicator } from '@/features/migration/components/migration-entry/MigrationEntryStatusIndicator.tsx';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
const MigrationEntryMobile = ({
|
const MigrationEntryMobile = memo(
|
||||||
entry,
|
({
|
||||||
entry: { mangaId, mangaTitle, status, error, isExcluded },
|
entry,
|
||||||
destinationEntry,
|
entry: { mangaId, mangaTitle, status, error, isExcluded },
|
||||||
otherSearchMatches,
|
destinationEntry,
|
||||||
isExpanded,
|
otherSearchMatches,
|
||||||
setIsExpanded,
|
isExpanded,
|
||||||
isMigrating,
|
setIsExpanded,
|
||||||
}: {
|
isMigrating,
|
||||||
entry: TMigrationEntry;
|
}: {
|
||||||
destinationEntry: MigrationMatch | undefined;
|
entry: TMigrationEntry;
|
||||||
otherSearchMatches: MigrationMatch[];
|
destinationEntry: MigrationMatch | undefined;
|
||||||
isExpanded: boolean;
|
otherSearchMatches: MigrationMatch[];
|
||||||
setIsExpanded: (expanded: boolean) => void;
|
isExpanded: boolean;
|
||||||
isMigrating: boolean;
|
setIsExpanded: (expanded: boolean) => void;
|
||||||
}) => {
|
isMigrating: boolean;
|
||||||
const { t } = useLingui();
|
}) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MigrationSourceEntry {...entry} />
|
<MigrationSourceEntry {...entry} />
|
||||||
<MigrationDestinationEntry
|
<MigrationDestinationEntry
|
||||||
sourceMangaId={mangaId}
|
sourceMangaId={mangaId}
|
||||||
sourceMangaTitle={mangaTitle}
|
sourceMangaTitle={mangaTitle}
|
||||||
entry={destinationEntry}
|
entry={destinationEntry}
|
||||||
status={status}
|
status={status}
|
||||||
otherResultsCount={otherSearchMatches.length}
|
|
||||||
isExpanded={isExpanded}
|
|
||||||
setIsExpanded={setIsExpanded}
|
|
||||||
isMigrating={isMigrating}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Collapse in={isExpanded} unmountOnExit>
|
|
||||||
<Divider sx={{ mb: 1 }} />
|
|
||||||
<Stack sx={{ flexDirection: 'row', gap: 1, alignItems: 'center', justifyContent: 'space-between' }}>
|
|
||||||
<Typography variant="h6" component="h2">{t`Matches`}</Typography>
|
|
||||||
<Button
|
|
||||||
startIcon={<SearchIcon />}
|
|
||||||
variant="text"
|
|
||||||
onClick={() => {
|
|
||||||
ReactRouter.navigate(AppRoutes.migrate.childRoutes.manualSearch.path(mangaId, mangaTitle), {
|
|
||||||
state: { mangaTitle: mangaTitle },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>{t`Manual search`}</Button>
|
|
||||||
</Stack>
|
|
||||||
<Stack sx={{ pt: 2 }}>
|
|
||||||
{otherSearchMatches.map((searchMatch) => {
|
|
||||||
if (destinationEntry?.id === searchMatch.id) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MigrationMatchedEntry key={searchMatch.id} sourceMangaId={mangaId} entry={searchMatch} />
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
|
||||||
</Collapse>
|
|
||||||
|
|
||||||
{!isMigrating && (
|
|
||||||
<MigrationEntrySearchExcludeActions
|
|
||||||
hasResults={!!destinationEntry}
|
|
||||||
otherResultsCount={otherSearchMatches.length}
|
otherResultsCount={otherSearchMatches.length}
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
setIsExpanded={setIsExpanded}
|
setIsExpanded={setIsExpanded}
|
||||||
isExcluded={isExcluded}
|
isMigrating={isMigrating}
|
||||||
mangaId={mangaId}
|
error={error}
|
||||||
mangaTitle={mangaTitle}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MigrationEntryDesktop = ({
|
<Collapse in={isExpanded} unmountOnExit>
|
||||||
entry,
|
<Divider sx={{ mb: 1 }} />
|
||||||
entry: { mangaId, mangaTitle, status, error },
|
<Stack sx={{ flexDirection: 'row', gap: 1, alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
destinationEntry,
|
<Typography variant="h6" component="h2">{t`Matches`}</Typography>
|
||||||
otherSearchMatches,
|
<Button
|
||||||
isExpanded,
|
startIcon={<SearchIcon />}
|
||||||
setIsExpanded,
|
variant="text"
|
||||||
isMigrating,
|
onClick={() => {
|
||||||
}: {
|
ReactRouter.navigate(
|
||||||
entry: TMigrationEntry;
|
AppRoutes.migrate.childRoutes.manualSearch.path(mangaId, mangaTitle),
|
||||||
destinationEntry: MigrationMatch | undefined;
|
{
|
||||||
otherSearchMatches: MigrationMatch[];
|
state: { mangaTitle: mangaTitle },
|
||||||
isExpanded: boolean;
|
},
|
||||||
setIsExpanded: (expanded: boolean) => void;
|
);
|
||||||
isMigrating: boolean;
|
}}
|
||||||
}) => {
|
>{t`Manual search`}</Button>
|
||||||
const { t } = useLingui();
|
</Stack>
|
||||||
|
<Stack sx={{ pt: 2 }}>
|
||||||
|
{otherSearchMatches.map((searchMatch) => {
|
||||||
|
if (destinationEntry?.id === searchMatch.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<MigrationMatchedEntry
|
||||||
<Box
|
key={searchMatch.id}
|
||||||
|
sourceMangaId={mangaId}
|
||||||
|
entry={searchMatch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
{!isMigrating && (
|
||||||
|
<MigrationEntrySearchExcludeActions
|
||||||
|
hasResults={!!destinationEntry}
|
||||||
|
otherResultsCount={otherSearchMatches.length}
|
||||||
|
isExpanded={isExpanded}
|
||||||
|
setIsExpanded={setIsExpanded}
|
||||||
|
isExcluded={isExcluded}
|
||||||
|
mangaId={mangaId}
|
||||||
|
mangaTitle={mangaTitle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MigrationEntryDesktop = memo(
|
||||||
|
({
|
||||||
|
entry,
|
||||||
|
entry: { mangaId, mangaTitle, status, error },
|
||||||
|
destinationEntry,
|
||||||
|
otherSearchMatches,
|
||||||
|
isExpanded,
|
||||||
|
setIsExpanded,
|
||||||
|
isMigrating,
|
||||||
|
}: {
|
||||||
|
entry: TMigrationEntry;
|
||||||
|
destinationEntry: MigrationMatch | undefined;
|
||||||
|
otherSearchMatches: MigrationMatch[];
|
||||||
|
isExpanded: boolean;
|
||||||
|
setIsExpanded: (expanded: boolean) => void;
|
||||||
|
isMigrating: boolean;
|
||||||
|
}) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MigrationSourceEntry {...entry} />
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'stretch', gap: 2 }}>
|
||||||
|
<Box sx={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||||
|
<MigrationEntryStatusIndicator status={status} hasResults={!!destinationEntry} />
|
||||||
|
<MigrationDestinationEntry
|
||||||
|
sourceMangaId={mangaId}
|
||||||
|
sourceMangaTitle={mangaTitle}
|
||||||
|
entry={destinationEntry}
|
||||||
|
status={status}
|
||||||
|
otherResultsCount={otherSearchMatches.length}
|
||||||
|
isExpanded={isExpanded}
|
||||||
|
setIsExpanded={setIsExpanded}
|
||||||
|
isMigrating={isMigrating}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{!isMigrating && (
|
||||||
|
<MigrationEntrySearchExcludeActions
|
||||||
|
hasResults={!!destinationEntry}
|
||||||
|
otherResultsCount={otherSearchMatches.length}
|
||||||
|
isExpanded={isExpanded}
|
||||||
|
setIsExpanded={setIsExpanded}
|
||||||
|
isExcluded={entry.isExcluded}
|
||||||
|
mangaId={entry.mangaId}
|
||||||
|
mangaTitle={entry.mangaTitle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Collapse in={isExpanded && !isMigrating} unmountOnExit>
|
||||||
|
<Divider sx={{ my: 4 }} />
|
||||||
|
<Typography variant="h6" component="h2">{t`Matches`}</Typography>
|
||||||
|
<Stack sx={{ pt: 2 }}>
|
||||||
|
{otherSearchMatches.map((searchMatch) => {
|
||||||
|
if (destinationEntry?.id === searchMatch.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MigrationMatchedEntry
|
||||||
|
key={searchMatch.id}
|
||||||
|
sourceMangaId={entry.mangaId}
|
||||||
|
entry={searchMatch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Collapse>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MigrationEntry = memo(
|
||||||
|
({ entry: propEntry, isMigrating }: { entry: TMigrationEntry; isMigrating: boolean }) => {
|
||||||
|
const isTabletWidth = MediaQuery.useIsTabletWidth();
|
||||||
|
|
||||||
|
const entry = useMemo(() => MigrationManager.getUpToDateMigrationEntry(propEntry), [propEntry]);
|
||||||
|
|
||||||
|
const destinationEntry = useMemo(() => {
|
||||||
|
const match = entry.searchMatches.find((matchEntry) => matchEntry.id === entry.selectedMatchMangaId);
|
||||||
|
const manualMatch = entry.manualMatches.find((matchEntry) => matchEntry.id === entry.selectedMatchMangaId);
|
||||||
|
|
||||||
|
return match ?? manualMatch;
|
||||||
|
}, [entry.searchMatches, entry.selectedMatchMangaId]);
|
||||||
|
const otherMatches = useMemo(
|
||||||
|
() =>
|
||||||
|
entry.searchMatches
|
||||||
|
.filter((searchMatch) => searchMatch.id !== entry.selectedMatchMangaId)
|
||||||
|
.sort((a, b) => (b.latestChapterNumber ?? 0) - (a.latestChapterNumber ?? 0)),
|
||||||
|
[entry.searchMatches, entry.selectedMatchMangaId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const MigrationComponent = useMemo(
|
||||||
|
() => (isTabletWidth ? MigrationEntryMobile : MigrationEntryDesktop),
|
||||||
|
[isTabletWidth],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
opacity: !isMigrating && entry.isExcluded ? 0.5 : 1,
|
||||||
alignItems: 'center',
|
...applyStyles(isTabletWidth, {
|
||||||
justifyContent: 'space-between',
|
flexDirection: 'column',
|
||||||
|
display: 'flex',
|
||||||
|
p: 2,
|
||||||
|
gap: 2,
|
||||||
|
}),
|
||||||
|
...applyStyles(!isTabletWidth, {
|
||||||
|
px: 2,
|
||||||
|
py: 2,
|
||||||
|
pr: 6,
|
||||||
|
}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MigrationSourceEntry {...entry} />
|
<MigrationComponent
|
||||||
|
entry={entry}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'stretch', gap: 2 }}>
|
destinationEntry={destinationEntry}
|
||||||
<Box sx={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
isExpanded={entry.areMatchesExpanded}
|
||||||
<MigrationEntryStatusIndicator status={status} hasResults={!!destinationEntry} />
|
setIsExpanded={() =>
|
||||||
<MigrationDestinationEntry
|
MigrationManager.setEntryMatchesExpandState(entry.mangaId, !entry.areMatchesExpanded)
|
||||||
sourceMangaId={mangaId}
|
}
|
||||||
sourceMangaTitle={mangaTitle}
|
otherSearchMatches={otherMatches}
|
||||||
entry={destinationEntry}
|
isMigrating={isMigrating}
|
||||||
status={status}
|
/>
|
||||||
otherResultsCount={otherSearchMatches.length}
|
</Paper>
|
||||||
isExpanded={isExpanded}
|
);
|
||||||
setIsExpanded={setIsExpanded}
|
},
|
||||||
isMigrating={isMigrating}
|
);
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{!isMigrating && (
|
|
||||||
<MigrationEntrySearchExcludeActions
|
|
||||||
hasResults={!!destinationEntry}
|
|
||||||
otherResultsCount={otherSearchMatches.length}
|
|
||||||
isExpanded={isExpanded}
|
|
||||||
setIsExpanded={setIsExpanded}
|
|
||||||
isExcluded={entry.isExcluded}
|
|
||||||
mangaId={entry.mangaId}
|
|
||||||
mangaTitle={entry.mangaTitle}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Collapse in={isExpanded && !isMigrating} unmountOnExit>
|
|
||||||
<Divider sx={{ my: 4 }} />
|
|
||||||
<Typography variant="h6" component="h2">{t`Matches`}</Typography>
|
|
||||||
<Stack sx={{ pt: 2 }}>
|
|
||||||
{otherSearchMatches.map((searchMatch) => {
|
|
||||||
if (destinationEntry?.id === searchMatch.id) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MigrationMatchedEntry
|
|
||||||
key={searchMatch.id}
|
|
||||||
sourceMangaId={entry.mangaId}
|
|
||||||
entry={searchMatch}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
|
||||||
</Collapse>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MigrationEntry = ({ entry: propEntry, isMigrating }: { entry: TMigrationEntry; isMigrating: boolean }) => {
|
|
||||||
const isTabletWidth = MediaQuery.useIsTabletWidth();
|
|
||||||
|
|
||||||
const entry = useMemo(() => MigrationManager.getUpToDateMigrationEntry(propEntry), [propEntry]);
|
|
||||||
|
|
||||||
const destinationEntry = useMemo(() => {
|
|
||||||
const match = entry.searchMatches.find((matchEntry) => matchEntry.id === entry.selectedMatchMangaId);
|
|
||||||
const manualMatch = entry.manualMatches.find((matchEntry) => matchEntry.id === entry.selectedMatchMangaId);
|
|
||||||
|
|
||||||
return match ?? manualMatch;
|
|
||||||
}, [entry.searchMatches, entry.selectedMatchMangaId]);
|
|
||||||
const otherMatches = useMemo(
|
|
||||||
() =>
|
|
||||||
entry.searchMatches
|
|
||||||
.filter((searchMatch) => searchMatch.id !== entry.selectedMatchMangaId)
|
|
||||||
.sort((a, b) => (b.latestChapterNumber ?? 0) - (a.latestChapterNumber ?? 0)),
|
|
||||||
[entry.searchMatches, entry.selectedMatchMangaId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const MigrationComponent = useMemo(
|
|
||||||
() => (isTabletWidth ? MigrationEntryMobile : MigrationEntryDesktop),
|
|
||||||
[isTabletWidth],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper
|
|
||||||
sx={{
|
|
||||||
opacity: !isMigrating && entry.isExcluded ? 0.5 : 1,
|
|
||||||
...applyStyles(isTabletWidth, {
|
|
||||||
flexDirection: 'column',
|
|
||||||
display: 'flex',
|
|
||||||
p: 2,
|
|
||||||
gap: 2,
|
|
||||||
}),
|
|
||||||
...applyStyles(!isTabletWidth, {
|
|
||||||
px: 2,
|
|
||||||
py: 2,
|
|
||||||
pr: 6,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MigrationComponent
|
|
||||||
entry={entry}
|
|
||||||
destinationEntry={destinationEntry}
|
|
||||||
isExpanded={entry.areMatchesExpanded}
|
|
||||||
setIsExpanded={() =>
|
|
||||||
MigrationManager.setEntryMatchesExpandState(entry.mangaId, !entry.areMatchesExpanded)
|
|
||||||
}
|
|
||||||
otherSearchMatches={otherMatches}
|
|
||||||
isMigrating={isMigrating}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -24,69 +24,67 @@ import Button from '@mui/material/Button';
|
|||||||
import CardActionArea from '@mui/material/CardActionArea';
|
import CardActionArea from '@mui/material/CardActionArea';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
export const MigrationMatchedEntry = ({
|
export const MigrationMatchedEntry = memo(
|
||||||
sourceMangaId,
|
({ sourceMangaId, entry }: { sourceMangaId: MangaIdInfo['id']; entry: MigrationMatch }) => {
|
||||||
entry,
|
const { t } = useLingui();
|
||||||
}: {
|
|
||||||
sourceMangaId: MangaIdInfo['id'];
|
|
||||||
entry: MigrationMatch;
|
|
||||||
}) => {
|
|
||||||
const { t } = useLingui();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MigrationEntryCard sx={{ mb: 1 }}>
|
<MigrationEntryCard sx={{ mb: 1 }}>
|
||||||
<CardActionArea onClick={() => MigrationManager.selectMatch(sourceMangaId, entry.id, entry.sourceId)}>
|
<CardActionArea onClick={() => MigrationManager.selectMatch(sourceMangaId, entry.id, entry.sourceId)}>
|
||||||
<MigrationEntryCardContent>
|
<MigrationEntryCardContent>
|
||||||
{(() => (
|
{(() => (
|
||||||
<>
|
<>
|
||||||
<Link
|
|
||||||
component={RouterLink}
|
|
||||||
to={AppRoutes.manga.path(entry.id)}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<ListCardAvatar
|
|
||||||
iconUrl={Mangas.getThumbnailUrl(entry)}
|
|
||||||
alt={entry.title}
|
|
||||||
slots={{
|
|
||||||
avatarProps: {
|
|
||||||
sx: {
|
|
||||||
width: 'unset',
|
|
||||||
height: 80,
|
|
||||||
aspectRatio: '3 / 4',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
<Stack sx={{ minWidth: 0, flex: 1 }}>
|
|
||||||
<Typography variant="overline" color="textSecondary">
|
|
||||||
{entry.sourceTitle}
|
|
||||||
</Typography>
|
|
||||||
<Link
|
<Link
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
to={AppRoutes.manga.path(entry.id)}
|
to={AppRoutes.manga.path(entry.id)}
|
||||||
sx={{ textDecoration: 'none', color: 'inherit', width: 'max-content' }}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<TypographyMaxLines variant="h6" component="h3" title={entry.title}>
|
<ListCardAvatar
|
||||||
{entry.title}
|
iconUrl={Mangas.getThumbnailUrl(entry)}
|
||||||
</TypographyMaxLines>
|
alt={entry.title}
|
||||||
|
slots={{
|
||||||
|
avatarProps: {
|
||||||
|
sx: {
|
||||||
|
width: 'unset',
|
||||||
|
height: 80,
|
||||||
|
aspectRatio: '3 / 4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
<MigrationEntryMetadataText {...entry} />
|
<Stack sx={{ minWidth: 0, flex: 1 }}>
|
||||||
</Stack>
|
<Typography variant="overline" color="textSecondary">
|
||||||
</>
|
{entry.sourceTitle}
|
||||||
))()}
|
</Typography>
|
||||||
{(() => (
|
<Link
|
||||||
<Button
|
component={RouterLink}
|
||||||
variant="outlined"
|
to={AppRoutes.manga.path(entry.id)}
|
||||||
{...MUIUtil.preventRippleProp({
|
sx={{ textDecoration: 'none', color: 'inherit', width: 'max-content' }}
|
||||||
onClick: () => MigrationManager.selectMatch(sourceMangaId, entry.id, entry.sourceId),
|
onClick={(e) => e.stopPropagation()}
|
||||||
})}
|
>
|
||||||
>{t`Select`}</Button>
|
<TypographyMaxLines variant="h6" component="h3" title={entry.title}>
|
||||||
))()}
|
{entry.title}
|
||||||
</MigrationEntryCardContent>
|
</TypographyMaxLines>
|
||||||
</CardActionArea>
|
</Link>
|
||||||
</MigrationEntryCard>
|
<MigrationEntryMetadataText {...entry} />
|
||||||
);
|
</Stack>
|
||||||
};
|
</>
|
||||||
|
))()}
|
||||||
|
{(() => (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
{...MUIUtil.preventRippleProp({
|
||||||
|
onClick: () =>
|
||||||
|
MigrationManager.selectMatch(sourceMangaId, entry.id, entry.sourceId),
|
||||||
|
})}
|
||||||
|
>{t`Select`}</Button>
|
||||||
|
))()}
|
||||||
|
</MigrationEntryCardContent>
|
||||||
|
</CardActionArea>
|
||||||
|
</MigrationEntryCard>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ import { useLingui } from '@lingui/react/macro';
|
|||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
export const MigrationSourceEntry = (entry: TMigrationEntry) => {
|
export const MigrationSourceEntry = memo((entry: TMigrationEntry) => {
|
||||||
const {
|
const {
|
||||||
mangaId,
|
mangaId,
|
||||||
mangaThumbnailUrl,
|
mangaThumbnailUrl,
|
||||||
@@ -94,4 +95,4 @@ export const MigrationSourceEntry = (entry: TMigrationEntry) => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user