Add bulk migration in library indicator

closes #1109
This commit is contained in:
schroda
2026-05-28 01:31:23 +02:00
parent 278ebb312f
commit 0b278ff525
9 changed files with 94 additions and 42 deletions

View File

@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- (**Migration**) Add a search option to ignore matches with missing chapters - (**Migration**) Add a search option to ignore matches with missing chapters
- (**Migration**) Add "local source" as a possible destination source - (**Migration**) Add "local source" as a possible destination source
- (**Migration**) Add option to abort entries that are searching or are waiting to get migrated - (**Migration**) Add option to abort entries that are searching or are waiting to get migrated
- (**Migration**) Add "in library" indicator
### Changed ### Changed

View File

@@ -32,9 +32,9 @@ import type {
export type MangaCardMode = 'default' | 'source' | 'migrate.select.bulk' | 'migrate.select.single' | 'duplicate'; export type MangaCardMode = 'default' | 'source' | 'migrate.select.bulk' | 'migrate.select.single' | 'duplicate';
type MangaCardBaseProps = Pick<MangaTypeGql, 'id' | 'title' | 'sourceId'> & type MangaCardBaseProps = Pick<MangaTypeGql, 'id' | 'title' | 'sourceId' | 'inLibrary'> &
Omit<SingleModeProps['manga'], 'downloadCount' | 'unreadCount' | 'chapters'> & Omit<SingleModeProps['manga'], 'downloadCount' | 'unreadCount' | 'chapters'> &
Partial<Pick<MangaTypeGql, 'inLibrary' | 'downloadCount' | 'unreadCount'>> & { Partial<Pick<MangaTypeGql, 'downloadCount' | 'unreadCount'>> & {
firstUnreadChapter?: firstUnreadChapter?:
| (ChapterIdInfo & ChapterSourceOrderInfo & ChapterReadInfo & ChapterNumberInfo & ChapterNameInfo) | (ChapterIdInfo & ChapterSourceOrderInfo & ChapterReadInfo & ChapterNumberInfo & ChapterNameInfo)
| null; | null;

View File

@@ -36,6 +36,7 @@ import type {
MangaAction, MangaAction,
MangaDownloadInfo, MangaDownloadInfo,
MangaIdInfo, MangaIdInfo,
MangaInLibraryInfo,
MangaSourceIdInfo, MangaSourceIdInfo,
MangaTitleInfo, MangaTitleInfo,
MangaUnreadInfo, MangaUnreadInfo,
@@ -47,7 +48,12 @@ import { STABLE_EMPTY_ARRAY } from '@/base/Base.constants.ts';
type BaseProps = { onClose: () => void; setHideMenu: (hide: boolean) => void }; type BaseProps = { onClose: () => void; setHideMenu: (hide: boolean) => void };
export type SingleModeProps = { export type SingleModeProps = {
manga: MangaIdInfo & MangaTitleInfo & MangaSourceIdInfo & MangaDownloadInfo & MangaUnreadInfo; manga: MangaIdInfo &
MangaTitleInfo &
MangaSourceIdInfo &
MangaDownloadInfo &
MangaUnreadInfo &
Pick<MangaInLibraryInfo, 'inLibrary'>;
handleSelection?: SelectableCollectionReturnType<MangaIdInfo['id']>['handleSelection']; handleSelection?: SelectableCollectionReturnType<MangaIdInfo['id']>['handleSelection'];
}; };

View File

@@ -23,6 +23,7 @@ import type {
MangaArtistInfo, MangaArtistInfo,
MangaAuthorInfo, MangaAuthorInfo,
MangaIdInfo, MangaIdInfo,
MangaInLibraryInfo,
MangaSourceIdInfo, MangaSourceIdInfo,
MangaThumbnailInfo, MangaThumbnailInfo,
MangaTitleInfo, MangaTitleInfo,
@@ -102,7 +103,14 @@ export enum MigrationEntryStatus {
} }
export interface MigrationMatch export interface MigrationMatch
extends MangaIdInfo, MangaTitleInfo, MangaThumbnailInfo, MangaSourceIdInfo, MangaArtistInfo, MangaAuthorInfo { extends
MangaIdInfo,
MangaTitleInfo,
MangaThumbnailInfo,
MangaSourceIdInfo,
MangaArtistInfo,
MangaAuthorInfo,
Pick<MangaInLibraryInfo, 'inLibrary'> {
sourceTitle: SourceDisplayNameInfo['displayName'] | undefined; sourceTitle: SourceDisplayNameInfo['displayName'] | undefined;
latestChapterNumber: ChapterNumberInfo['chapterNumber'] | undefined; latestChapterNumber: ChapterNumberInfo['chapterNumber'] | undefined;
missingChapters: number | undefined; missingChapters: number | undefined;

View File

@@ -249,6 +249,7 @@ export class MigrationManager {
sourceTitle: cachedEntry?.source?.displayName ?? searchMatch.sourceTitle, sourceTitle: cachedEntry?.source?.displayName ?? searchMatch.sourceTitle,
latestChapterNumber: cachedEntry?.highestNumberedChapter?.chapterNumber ?? searchMatch.latestChapterNumber, latestChapterNumber: cachedEntry?.highestNumberedChapter?.chapterNumber ?? searchMatch.latestChapterNumber,
missingChapters: searchMatch.missingChapters, missingChapters: searchMatch.missingChapters,
inLibrary: cachedEntry?.inLibrary ?? searchMatch.inLibrary,
}; };
} }
@@ -955,6 +956,7 @@ export class MigrationManager {
sourceId: manga.sourceId, sourceId: manga.sourceId,
sourceTitle: manga.source?.displayName, sourceTitle: manga.source?.displayName,
missingChapters: chapters ? Chapters.getMissingCount(chapters) : undefined, missingChapters: chapters ? Chapters.getMissingCount(chapters) : undefined,
inLibrary: manga.inLibrary,
})); }));
draftEntry.destSourceIdToSearchState[destSourceId] = true; draftEntry.destSourceIdToSearchState[destSourceId] = true;

View File

@@ -18,8 +18,6 @@ import { MigrationManager } from '@/features/migration/MigrationManager.ts';
import { extractGraphqlExceptionInfo } from '@/lib/HelperFunctions.ts'; import { extractGraphqlExceptionInfo } from '@/lib/HelperFunctions.ts';
import { Confirmation } from '@/base/AppAwaitableComponent.ts'; import { Confirmation } from '@/base/AppAwaitableComponent.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx';
import { Mangas } from '@/features/manga/services/Mangas.ts';
import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx'; import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx';
import { MigrationEntryMetadataText } from '@/features/migration/components/migration-entry/MigrationEntryMetadataText.tsx'; import { MigrationEntryMetadataText } from '@/features/migration/components/migration-entry/MigrationEntryMetadataText.tsx';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
@@ -36,6 +34,7 @@ import Link from '@mui/material/Link';
import { Link as RouterLink } from 'react-router-dom'; import { Link as RouterLink } from 'react-router-dom';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { applyStyles } from '@/base/utils/ApplyStyles.ts'; import { applyStyles } from '@/base/utils/ApplyStyles.ts';
import { MigrationEntryThumbnail } from '@/features/migration/components/migration-entry/MigrationEntryThumbnail.tsx';
const EntryStatus = ({ const EntryStatus = ({
sourceMangaId, sourceMangaId,
@@ -153,21 +152,7 @@ const EntryData = (entry: MigrationMatch) => {
return ( return (
<> <>
<Link component={RouterLink} to={AppRoutes.manga.path(id)}> <MigrationEntryThumbnail entry={entry} height={112} />
<ListCardAvatar
iconUrl={Mangas.getThumbnailUrl(entry)}
alt={title}
slots={{
avatarProps: {
sx: {
width: 'unset',
height: 112,
aspectRatio: '3 / 4',
},
},
}}
/>
</Link>
<Stack sx={{ minWidth: 0, flex: 1 }}> <Stack sx={{ minWidth: 0, flex: 1 }}>
<Typography <Typography
variant="overline" variant="overline"

View File

@@ -0,0 +1,68 @@
/*
* 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 FavoriteIcon from '@mui/icons-material/Favorite';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx';
import { Mangas } from '@/features/manga/services/Mangas.ts';
import Stack from '@mui/material/Stack';
import Link from '@mui/material/Link';
import { Link as RouterLink } from 'react-router-dom';
import type { MigrationMatch } from '@/features/migration/Migration.types.ts';
import type { Theme } from '@mui/material/styles';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
export const MigrationEntryThumbnail = ({
entry,
height,
}: {
entry: MigrationMatch;
height: ReturnType<Theme['applyStyles']>['height'];
}) => {
const { id, title } = entry;
const { t } = useLingui();
return (
<Stack sx={{ position: 'relative' }}>
<Link component={RouterLink} to={AppRoutes.manga.path(id)} onClick={(e) => e.stopPropagation()}>
<ListCardAvatar
iconUrl={Mangas.getThumbnailUrl(entry)}
alt={title}
slots={{
avatarProps: {
sx: {
width: 'unset',
height,
aspectRatio: '3 / 4',
filter: entry.inLibrary ? 'brightness(0.4)' : undefined,
},
},
}}
/>
</Link>
{entry.inLibrary && (
<CustomTooltip title={t`In Library`}>
<Stack
sx={{
position: 'absolute',
top: 2.5,
left: 2.5,
p: 0.5,
backgroundColor: 'background.paper',
borderRadius: 1,
}}
>
<FavoriteIcon color="primary" sx={{ fontSize: 15 }} />
</Stack>
</CustomTooltip>
)}
</Stack>
);
};

View File

@@ -12,8 +12,6 @@ import { MigrationEntryCard } from '@/features/migration/components/migration-en
import { MigrationManager } from '@/features/migration/MigrationManager.ts'; import { MigrationManager } from '@/features/migration/MigrationManager.ts';
import { MigrationEntryCardContent } from '@/features/migration/components/migration-entry/MigrationEntryCardContent.tsx'; import { MigrationEntryCardContent } from '@/features/migration/components/migration-entry/MigrationEntryCardContent.tsx';
import { AppRoutes } from '@/base/AppRoute.constants.ts'; import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx';
import { Mangas } from '@/features/manga/services/Mangas.ts';
import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx'; import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx';
import { MigrationEntryMetadataText } from '@/features/migration/components/migration-entry/MigrationEntryMetadataText.tsx'; import { MigrationEntryMetadataText } from '@/features/migration/components/migration-entry/MigrationEntryMetadataText.tsx';
import { MUIUtil } from '@/lib/mui/MUI.util.ts'; import { MUIUtil } from '@/lib/mui/MUI.util.ts';
@@ -25,6 +23,7 @@ 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'; import { memo } from 'react';
import { MigrationEntryThumbnail } from '@/features/migration/components/migration-entry/MigrationEntryThumbnail.tsx';
export const MigrationMatchedEntry = memo( export const MigrationMatchedEntry = memo(
({ sourceMangaId, entry }: { sourceMangaId: MangaIdInfo['id']; entry: MigrationMatch }) => { ({ sourceMangaId, entry }: { sourceMangaId: MangaIdInfo['id']; entry: MigrationMatch }) => {
@@ -36,25 +35,7 @@ export const MigrationMatchedEntry = memo(
<MigrationEntryCardContent> <MigrationEntryCardContent>
{(() => ( {(() => (
<> <>
<Link <MigrationEntryThumbnail entry={entry} height={80} />
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 }}> <Stack sx={{ minWidth: 0, flex: 1 }}>
<Typography variant="overline" color="textSecondary"> <Typography variant="overline" color="textSecondary">
{entry.sourceTitle} {entry.sourceTitle}

View File

@@ -1915,6 +1915,7 @@ msgstr ""
#: src/features/manga/components/details/MangaDetails.tsx #: src/features/manga/components/details/MangaDetails.tsx
#: src/features/manga/components/MangaBadges.tsx #: src/features/manga/components/MangaBadges.tsx
#: src/features/migration/components/migration-entry/MigrationEntryThumbnail.tsx
msgid "In Library" msgid "In Library"
msgstr "In Library" msgstr "In Library"