Improve bulk migration render performance

This commit is contained in:
schroda
2026-05-13 01:22:04 +02:00
parent 72d213c43e
commit 06cc0724df
6 changed files with 377 additions and 360 deletions

View File

@@ -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>
); );
}; },
);

View File

@@ -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>
); );
}; });

View File

@@ -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,

View File

@@ -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>
);
};

View File

@@ -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>
);
},
);

View File

@@ -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>
); );
}; });