diff --git a/public/locales/en.json b/public/locales/en.json index cd0d22ed..ce015c94 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -520,6 +520,11 @@ "label": { "description": "Show all duplicated entries in your library", "title": "Duplicated entries" + }, + "settings": { + "label": { + "check_description": "Check description" + } } } }, diff --git a/src/screens/settings/LibraryDuplicates.tsx b/src/screens/settings/LibraryDuplicates.tsx index 8bf65d2d..3657ac15 100644 --- a/src/screens/settings/LibraryDuplicates.tsx +++ b/src/screens/settings/LibraryDuplicates.tsx @@ -8,6 +8,11 @@ import { useTranslation } from 'react-i18next'; import { useContext, useEffect, useMemo } from 'react'; +import IconButton from '@mui/material/IconButton'; +import SettingsIcon from '@mui/icons-material/Settings'; +import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { MangaGrid } from '@/components/MangaGrid.tsx'; import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts'; @@ -16,35 +21,112 @@ import { NavBarContext } from '@/components/context/NavbarContext.tsx'; import { useLocalStorage } from '@/util/useStorage.tsx'; import { GridLayout } from '@/components/context/LibraryOptionsContext.tsx'; import { GridLayouts } from '@/components/source/GridLayouts.tsx'; +import { CheckboxInput } from '@/components/atoms/CheckboxInput.tsx'; + +const findDuplicatesByTitle = (libraryMangas: TManga[]): TManga[] => { + const titleToMangas = Object.groupBy(libraryMangas, ({ title }) => title.toLowerCase().trim()); + + return Object.entries(titleToMangas) + .filter(([, mangasWithTitle]) => (mangasWithTitle?.length ?? 0) > 1) + .map(([, mangasWithTitle]) => mangasWithTitle ?? []) + .flat(); +}; + +const findDuplicatesByTitleAndAlternativeTitles = (libraryMangas: TManga[]): TManga[] => { + const idToDuplicateStatus: Record = {}; + const duplicatedMangas: TManga[] = []; + + libraryMangas.forEach((mangaToCheck) => + libraryMangas.forEach((libraryManga) => { + const isDifferentManga = mangaToCheck.id !== libraryManga.id; + if (!isDifferentManga) { + return; + } + + const titleToCheck = mangaToCheck.title.toLowerCase().trim(); + + const doesTitleMatch = libraryManga.title.toLowerCase().trim() === titleToCheck; + const doesAlternativeTitleMatch = !!libraryManga.description?.toLowerCase().trim().includes(titleToCheck); + + const isDuplicate = doesTitleMatch || doesAlternativeTitleMatch; + if (!isDuplicate) { + return; + } + + const markMangaToCheckAsDuplicate = !idToDuplicateStatus[mangaToCheck.id]; + if (markMangaToCheckAsDuplicate) { + idToDuplicateStatus[mangaToCheck.id] = true; + duplicatedMangas.push(mangaToCheck); + } + + const wasAlreadyDetected = idToDuplicateStatus[libraryManga.id]; + if (wasAlreadyDetected) { + return; + } + + idToDuplicateStatus[libraryManga.id] = true; + duplicatedMangas.push(libraryManga); + }), + ); + + return duplicatedMangas; +}; export const LibraryDuplicates = () => { const { t } = useTranslation(); const [gridLayout, setGridLayout] = useLocalStorage('libraryDuplicatesGridLayout', GridLayout.List); + const [checkAlternativeTitles, setCheckAlternativeTitles] = useLocalStorage( + 'libraryDuplicatesCheckAlternativeTitles', + false, + ); const { setTitle, setAction } = useContext(NavBarContext); useEffect(() => { setTitle(t('library.settings.advanced.duplicates.label.title')); - setAction(); + setAction( + <> + + + {(popupState) => ( + <> + + + + + + setCheckAlternativeTitles(checked)} + /> + + + + )} + + , + ); return () => { setTitle(''); setAction(null); }; - }, [t, gridLayout]); + }, [t, gridLayout, checkAlternativeTitles]); 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.toLowerCase().trim()); + if (checkAlternativeTitles) { + return findDuplicatesByTitleAndAlternativeTitles(libraryMangas); + } - return Object.entries(titleToMangas) - .filter(([, mangasWithTitle]) => (mangasWithTitle?.length ?? 0) > 1) - .map(([, mangasWithTitle]) => mangasWithTitle ?? []) - .flat(); - }, [data?.mangas.nodes]); + return findDuplicatesByTitle(libraryMangas); + }, [data?.mangas.nodes, checkAlternativeTitles]); return (