diff --git a/public/locales/en.json b/public/locales/en.json index d4ac0b36..ce9088aa 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -505,6 +505,12 @@ "title": "Cleanup database" } } + }, + "duplicates": { + "label": { + "description": "Show all duplicated entries in your library", + "title": "Duplicated entries" + } } }, "general": { diff --git a/src/App.tsx b/src/App.tsx index 2f3b719b..a39e441e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -44,6 +44,7 @@ const { Migrate } = loadable(() => import('@/screens/Migrate.tsx'), lazyLoadFall const { DeviceSetting } = loadable(() => import('@/components/settings/DeviceSetting.tsx'), lazyLoadFallback); const { TrackingSettings } = loadable(() => import('@/screens/settings/TrackingSettings.tsx'), lazyLoadFallback); const { TrackerOAuthLogin } = loadable(() => import('@/screens/TrackerOAuthLogin.tsx'), lazyLoadFallback); +const { LibraryDuplicates } = loadable(() => import('@/screens/settings/LibraryDuplicates.tsx'), lazyLoadFallback); if (process.env.NODE_ENV !== 'production') { // Adds messages only in a dev environment @@ -105,7 +106,10 @@ export const App: React.FC = () => ( } /> } /> } /> - } /> + + } /> + } /> + } /> } /> } /> diff --git a/src/components/manga/MangaBadges.tsx b/src/components/manga/MangaBadges.tsx index 75dd65fa..d8fc308a 100644 --- a/src/components/manga/MangaBadges.tsx +++ b/src/components/manga/MangaBadges.tsx @@ -74,10 +74,10 @@ export const MangaBadges = ({ {t('manga.button.in_library')} )} - {showUnreadBadge && (unread ?? 0) > 0 && ( + {showUnreadBadge && mode === 'default' && (unread ?? 0) > 0 && ( {unread} )} - {showDownloadBadge && (downloadCount ?? 0) > 0 && ( + {showDownloadBadge && mode === 'default' && (downloadCount ?? 0) > 0 && ( { + const { t } = useTranslation(); + + const [gridLayout, setGridLayout] = useLocalStorage('migrateGridLayout', GridLayout.List); + + const { setTitle, setAction } = useContext(NavBarContext); + useEffect(() => { + setTitle(t('library.settings.advanced.duplicates.label.title')); + setAction(); + + return () => { + setTitle(''); + setAction(null); + }; + }, [t, gridLayout]); + + const { data, loading, error, refetch } = requestManager.useGetMangas({ condition: { inLibrary: true } }); + + const duplicatedMangas = useMemo(() => { + const libraryMangas: TManga[] = data?.mangas.nodes ?? []; + + const titleToMangas = Object.groupBy(libraryMangas, ({ title }) => title); + + return Object.entries(titleToMangas) + .filter(([, mangasWithTitle]) => (mangasWithTitle?.length ?? 0) > 1) + .map(([, mangasWithTitle]) => mangasWithTitle ?? []) + .flat(); + }, [data?.mangas.nodes]); + + return ( + {}} + message={error ? t('manga.error.label.request_failure') : t('library.error.label.empty')} + messageExtra={error?.message} + isLoading={loading} + retry={error ? () => refetch().catch(defaultPromiseErrorHandler('LibraryDuplicates::refetch')) : undefined} + gridLayout={gridLayout} + /> + ); +}; diff --git a/src/screens/settings/LibrarySettings.tsx b/src/screens/settings/LibrarySettings.tsx index 3571af9b..25d22db4 100644 --- a/src/screens/settings/LibrarySettings.tsx +++ b/src/screens/settings/LibrarySettings.tsx @@ -28,6 +28,7 @@ import { Mangas } from '@/lib/data/Mangas.ts'; import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx'; import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts'; import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx'; +import { ListItemLink } from '@/components/util/ListItemLink.tsx'; const removeNonLibraryMangasFromCategories = async (): Promise => { try { @@ -170,6 +171,12 @@ export function LibrarySettings() { secondary={t('library.settings.advanced.database.cleanup.label.description')} /> + + + );