From eca01374261cd28f6cb50b942e096243a877a995 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 27 Dec 2024 11:08:58 +0530 Subject: [PATCH] Replace old impl --- web/apps/photos/src/components/PhotoFrame.tsx | 47 +-- .../src/components/PhotoList/dedupe.tsx | 373 ------------------ .../photos/src/components/PhotoList/index.tsx | 17 - .../photos/src/components/Sidebar/index.tsx | 4 +- .../pages/dedupe/SelectedFileOptions.tsx | 58 --- .../components/pages/gallery/PreviewCard.tsx | 35 +- web/apps/photos/src/pages/deduplicate.tsx | 230 ----------- web/apps/photos/src/pages/gallery.tsx | 1 - web/apps/photos/src/pages/shared-albums.tsx | 1 - .../src/services/deduplicationService.ts | 176 --------- .../base/locales/en-US/translation.json | 3 - web/packages/shared/constants/pages.tsx | 1 - 12 files changed, 17 insertions(+), 929 deletions(-) delete mode 100644 web/apps/photos/src/components/PhotoList/dedupe.tsx delete mode 100644 web/apps/photos/src/components/pages/dedupe/SelectedFileOptions.tsx delete mode 100644 web/apps/photos/src/pages/deduplicate.tsx delete mode 100644 web/apps/photos/src/services/deduplicationService.ts diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index ed2d85d81a..2f529248ea 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -8,7 +8,6 @@ import { EnteFile } from "@/media/file"; import { FileType } from "@/media/file-type"; import type { GalleryBarMode } from "@/new/photos/components/gallery/reducer"; import { TRASH_SECTION } from "@/new/photos/services/collection"; -import { PHOTOS_PAGES } from "@ente/shared/constants/pages"; import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded"; import { styled } from "@mui/material"; import PhotoViewer, { type PhotoViewerProps } from "components/PhotoViewer"; @@ -17,14 +16,12 @@ import { GalleryContext } from "pages/gallery"; import PhotoSwipe from "photoswipe"; import { useContext, useEffect, useState } from "react"; import AutoSizer from "react-virtualized-auto-sizer"; -import { Duplicate } from "services/deduplicationService"; import { SelectedState, SetFilesDownloadProgressAttributesCreator, } from "types/gallery"; import { handleSelectCreator } from "utils/photoFrame"; import { PhotoList } from "./PhotoList"; -import { DedupePhotoList } from "./PhotoList/dedupe"; import PreviewCard from "./pages/gallery/PreviewCard"; const Container = styled("div")` @@ -60,10 +57,6 @@ export type DisplayFile = EnteFile & { }; export interface PhotoFrameProps { - page: - | PHOTOS_PAGES.GALLERY - | PHOTOS_PAGES.DEDUPLICATE - | PHOTOS_PAGES.SHARED_ALBUMS; mode?: GalleryBarMode; /** * This is an experimental prop, to see if we can merge the separate @@ -72,7 +65,6 @@ export interface PhotoFrameProps { */ modePlus?: GalleryBarMode | "search"; files: EnteFile[]; - duplicates?: Duplicate[]; syncWithRemote: () => Promise; favItemIds?: Set; setSelected: ( @@ -96,8 +88,6 @@ export interface PhotoFrameProps { } const PhotoFrame = ({ - page, - duplicates, mode, modePlus, files, @@ -476,30 +466,19 @@ const PhotoFrame = ({ return ( - {({ height, width }) => - page === PHOTOS_PAGES.DEDUPLICATE ? ( - - ) : ( - - ) - } + {({ height, width }) => ( + + )} { - if (groups) { - // need to confirm why this was there - // const sum = groups.reduce((acc, item) => acc + item, 0); - // if (sum < columns) { - // groups[groups.length - 1] += columns - sum; - // } - return groups - .map( - (x) => - `repeat(${x}, ${IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio}px)`, - ) - .join(` ${SPACE_BTW_DATES}px `); - } else { - return `repeat(${columns},${ - IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio - }px)`; - } -}; - -function getFractionFittableColumns(width: number): number { - return ( - (width - 2 * getGapFromScreenEdge(width) + GAP_BTW_TILES) / - (IMAGE_CONTAINER_MAX_WIDTH + GAP_BTW_TILES) - ); -} - -function getGapFromScreenEdge(width: number) { - if (width > MIN_COLUMNS * IMAGE_CONTAINER_MAX_WIDTH) { - return 24; - } else { - return 4; - } -} - -function getShrinkRatio(width: number, columns: number) { - return ( - (width - - 2 * getGapFromScreenEdge(width) - - (columns - 1) * GAP_BTW_TILES) / - (columns * IMAGE_CONTAINER_MAX_WIDTH) - ); -} - -const ListContainer = styled(Box)<{ - columns: number; - shrinkRatio: number; - groups?: number[]; -}>` - display: grid; - grid-template-columns: ${({ columns, shrinkRatio, groups }) => - getTemplateColumns(columns, shrinkRatio, groups)}; - grid-column-gap: ${GAP_BTW_TILES}px; - width: 100%; - color: #fff; - padding: 0 24px; - @media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * MIN_COLUMNS}px) { - padding: 0 4px; - } -`; - -const ListItemContainer = styled(FlexWrapper)<{ span: number }>` - grid-column: span ${(props) => props.span}; -`; - -const DateContainer = styled(ListItemContainer)` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - height: ${DATE_CONTAINER_HEIGHT}px; - color: ${({ theme }) => theme.colors.text.muted}; -`; - -const SizeAndCountContainer = styled(DateContainer)` - margin-top: 1rem; - height: ${SIZE_AND_COUNT_CONTAINER_HEIGHT}px; -`; - -interface Props { - height: number; - width: number; - duplicates: Duplicate[]; - showAppDownloadBanner: boolean; - getThumbnail: ( - file: EnteFile, - index: number, - isScrolling?: boolean, - ) => React.JSX.Element; - activeCollectionID: number; -} - -interface ItemData { - timeStampList: TimeStampListItem[]; - columns: number; - shrinkRatio: number; - renderListItem: ( - timeStampListItem: TimeStampListItem, - isScrolling?: boolean, - ) => React.JSX.Element; -} - -const createItemData = memoize( - ( - timeStampList: TimeStampListItem[], - columns: number, - shrinkRatio: number, - renderListItem: ( - timeStampListItem: TimeStampListItem, - isScrolling?: boolean, - ) => React.JSX.Element, - ): ItemData => ({ - timeStampList, - columns, - shrinkRatio, - renderListItem, - }), -); -const PhotoListRow = React.memo( - ({ - index, - style, - isScrolling, - data, - }: ListChildComponentProps) => { - const { timeStampList, columns, shrinkRatio, renderListItem } = data; - return ( - - - {renderListItem(timeStampList[index], isScrolling)} - - - ); - }, - areEqual, -); - -const getTimeStampListFromDuplicates = (duplicates: Duplicate[], columns) => { - const timeStampList: TimeStampListItem[] = []; - for (let index = 0; index < duplicates.length; index++) { - const dupes = duplicates[index]; - timeStampList.push({ - itemType: ITEM_TYPE.SIZE_AND_COUNT, - fileSize: dupes.size, - fileCount: dupes.files.length, - }); - let lastIndex = 0; - while (lastIndex < dupes.files.length) { - timeStampList.push({ - itemType: ITEM_TYPE.FILE, - items: dupes.files.slice(lastIndex, lastIndex + columns), - itemStartIndex: index, - }); - lastIndex += columns; - } - } - return timeStampList; -}; - -export function DedupePhotoList({ - height, - width, - duplicates, - getThumbnail, - activeCollectionID, -}: Props) { - const [timeStampList, setTimeStampList] = useState([]); - const refreshInProgress = useRef(false); - const shouldRefresh = useRef(false); - const listRef = useRef(null); - - const columns = useMemo(() => { - const fittableColumns = getFractionFittableColumns(width); - let columns = Math.floor(fittableColumns); - if (columns < MIN_COLUMNS) { - columns = MIN_COLUMNS; - } - return columns; - }, [width]); - - const shrinkRatio = getShrinkRatio(width, columns); - const listItemHeight = - IMAGE_CONTAINER_MAX_HEIGHT * shrinkRatio + GAP_BTW_TILES; - - const refreshList = () => { - listRef.current?.resetAfterIndex(0); - }; - - useEffect(() => { - const main = () => { - if (refreshInProgress.current) { - shouldRefresh.current = true; - return; - } - refreshInProgress.current = true; - const timeStampList = getTimeStampListFromDuplicates( - duplicates, - columns, - ); - setTimeStampList(timeStampList); - refreshInProgress.current = false; - if (shouldRefresh.current) { - shouldRefresh.current = false; - setTimeout(main, 0); - } - }; - main(); - }, [columns, duplicates]); - - useEffect(() => { - refreshList(); - }, [timeStampList]); - - const getItemSize = (timeStampList) => (index) => { - switch (timeStampList[index].itemType) { - case ITEM_TYPE.TIME: - return DATE_CONTAINER_HEIGHT; - case ITEM_TYPE.SIZE_AND_COUNT: - return SIZE_AND_COUNT_CONTAINER_HEIGHT; - case ITEM_TYPE.FILE: - return listItemHeight; - default: - return timeStampList[index].height; - } - }; - - const generateKey = (index) => { - switch (timeStampList[index].itemType) { - case ITEM_TYPE.FILE: - return `${timeStampList[index].items[0].id}-${ - timeStampList[index].items.slice(-1)[0].id - }`; - default: - return `${timeStampList[index].id}-${index}`; - } - }; - - const renderListItem = ( - listItem: TimeStampListItem, - isScrolling: boolean, - ) => { - switch (listItem.itemType) { - case ITEM_TYPE.SIZE_AND_COUNT: - return ( - /*TODO: Translate the full phrase instead of piecing - together parts like this See: - https://crowdin.com/editor/ente-photos-web/9/enus-de?view=comfortable&filter=basic&value=0#8104 - */ - - {listItem.fileCount} {t("FILES")},{" "} - {formattedByteSize(listItem.fileSize || 0)} {t("EACH")} - - ); - case ITEM_TYPE.FILE: { - const ret = listItem.items.map((item, idx) => - getThumbnail( - item, - listItem.itemStartIndex + idx, - isScrolling, - ), - ); - if (listItem.groups) { - let sum = 0; - for (let i = 0; i < listItem.groups.length - 1; i++) { - sum = sum + listItem.groups[i]; - ret.splice( - sum, - 0, -
, - ); - sum += 1; - } - } - return ret; - } - default: - return listItem.item; - } - }; - - if (!timeStampList?.length) { - return <>; - } - - const itemData = createItemData( - timeStampList, - columns, - shrinkRatio, - renderListItem, - ); - - return ( - - {PhotoListRow} - - ); -} diff --git a/web/apps/photos/src/components/PhotoList/index.tsx b/web/apps/photos/src/components/PhotoList/index.tsx index f6a3aeee1f..e8448725b4 100644 --- a/web/apps/photos/src/components/PhotoList/index.tsx +++ b/web/apps/photos/src/components/PhotoList/index.tsx @@ -1,5 +1,4 @@ import { EnteFile } from "@/media/file"; -import { formattedByteSize } from "@/new/photos/utils/units"; import { FlexWrapper } from "@ente/shared/components/Container"; import { formatDate } from "@ente/shared/time/format"; import { Box, Checkbox, Link, Typography, styled } from "@mui/material"; @@ -26,7 +25,6 @@ import { import type { PhotoFrameProps } from "components/PhotoFrame"; export const DATE_CONTAINER_HEIGHT = 48; -export const SIZE_AND_COUNT_CONTAINER_HEIGHT = 72; export const SPACE_BTW_DATES = 44; const SPACE_BTW_DATES_TO_IMAGE_CONTAINER_WIDTH_RATIO = 0.244; @@ -38,7 +36,6 @@ const ALBUM_FOOTER_HEIGHT_WITH_REFERRAL = 113; export enum ITEM_TYPE { TIME = "TIME", FILE = "FILE", - SIZE_AND_COUNT = "SIZE_AND_COUNT", HEADER = "HEADER", FOOTER = "FOOTER", MARKETING_FOOTER = "MARKETING_FOOTER", @@ -143,11 +140,6 @@ const DateContainer = styled(ListItemContainer)` color: ${({ theme }) => theme.colors.text.muted}; `; -const SizeAndCountContainer = styled(DateContainer)` - margin-top: 1rem; - height: ${SIZE_AND_COUNT_CONTAINER_HEIGHT}px; -`; - const FooterContainer = styled(ListItemContainer)` margin-bottom: 0.75rem; @media (max-width: 540px) { @@ -712,8 +704,6 @@ export function PhotoList({ switch (timeStampList[index].itemType) { case ITEM_TYPE.TIME: return DATE_CONTAINER_HEIGHT; - case ITEM_TYPE.SIZE_AND_COUNT: - return SIZE_AND_COUNT_CONTAINER_HEIGHT; case ITEM_TYPE.FILE: return listItemHeight; default: @@ -842,13 +832,6 @@ export function PhotoList({ {listItem.date} ); - case ITEM_TYPE.SIZE_AND_COUNT: - return ( - - {listItem.fileCount} {t("FILES")},{" "} - {formattedByteSize(listItem.fileSize || 0)} {t("EACH")} - - ); case ITEM_TYPE.FILE: { const ret = listItem.items.map((item, idx) => getThumbnail( diff --git a/web/apps/photos/src/components/Sidebar/index.tsx b/web/apps/photos/src/components/Sidebar/index.tsx index 2f09ccc578..362bc89251 100644 --- a/web/apps/photos/src/components/Sidebar/index.tsx +++ b/web/apps/photos/src/components/Sidebar/index.tsx @@ -514,7 +514,7 @@ const UtilitySection: React.FC = ({ closeSidebar }) => { await openAccountsManagePasskeysPage(); }; - const redirectToDeduplicatePage = () => router.push(PAGES.DEDUPLICATE); + const handleDeduplicate = () => router.push("/duplicates"); const toggleTheme = () => setThemeColor( @@ -573,7 +573,7 @@ const UtilitySection: React.FC = ({ closeSidebar }) => { /> void; - close: () => void; - count: number; - clearSelection: () => void; -} - -export default function DeduplicateOptions({ - deleteFileHelper, - close, - count, - clearSelection, -}: IProps) { - const { showMiniDialog } = useContext(AppContext); - - const trashHandler = () => - showMiniDialog({ - title: t("trash_files_title"), - message: t("trash_files_message"), - continue: { - text: t("move_to_trash"), - color: "critical", - action: deleteFileHelper, - }, - }); - - return ( - - - {count ? ( - - - - ) : ( - - - - )} - {t("selected_count", { selected: count })} - - - - - - - - ); -} diff --git a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx b/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx index bd0200bc2c..d5d598c3ee 100644 --- a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx +++ b/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx @@ -15,10 +15,9 @@ import useLongPress from "@ente/shared/hooks/useLongPress"; import AlbumOutlined from "@mui/icons-material/AlbumOutlined"; import Favorite from "@mui/icons-material/FavoriteRounded"; import PlayCircleOutlineOutlinedIcon from "@mui/icons-material/PlayCircleOutlineOutlined"; -import { Tooltip, styled } from "@mui/material"; +import { styled } from "@mui/material"; import type { DisplayFile } from "components/PhotoFrame"; import i18n from "i18next"; -import { DeduplicateContext } from "pages/deduplicate"; import { GalleryContext } from "pages/gallery"; import React, { useContext, useEffect, useRef, useState } from "react"; import { shouldShowAvatar } from "utils/file"; @@ -235,7 +234,6 @@ const Cont = styled("div")<{ disabled: boolean }>` export default function PreviewCard(props: IProps) { const galleryContext = useContext(GalleryContext); - const deduplicateContext = useContext(DeduplicateContext); const longPressCallback = () => { onSelect(!selected); @@ -318,7 +316,7 @@ export default function PreviewCard(props: IProps) { } }; - const renderFn = () => ( + return ( - {deduplicateContext.isOnDeduplicatePage && ( - -

{file.metadata.title}

-

- {deduplicateContext.collectionNameMap.get( - file.collectionID, - )} -

-
- )} {props?.activeCollectionID === TRASH_SECTION && file.isTrashed && (

{formatDateRelative(file.deleteBy / 1000)}

@@ -389,25 +377,6 @@ export default function PreviewCard(props: IProps) { )}
); - - if (deduplicateContext.isOnDeduplicatePage) { - return ( - - {renderFn()} - - ); - } else { - return renderFn(); - } } function formatDateRelative(date: number) { diff --git a/web/apps/photos/src/pages/deduplicate.tsx b/web/apps/photos/src/pages/deduplicate.tsx deleted file mode 100644 index fbeb5bcbd1..0000000000 --- a/web/apps/photos/src/pages/deduplicate.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import { stashRedirect } from "@/accounts/services/redirect"; -import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; -import { errorDialogAttributes } from "@/base/components/utils/dialog"; -import log from "@/base/log"; -import { ALL_SECTION, moveToTrash } from "@/new/photos/services/collection"; -import { - getAllLatestCollections, - getLocalCollections, - syncTrash, -} from "@/new/photos/services/collections"; -import { - createFileCollectionIDs, - getLocalFiles, - syncFiles, -} from "@/new/photos/services/files"; -import { useAppContext } from "@/new/photos/types/context"; -import { VerticallyCentered } from "@ente/shared/components/Container"; -import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages"; -import { ApiError } from "@ente/shared/error"; -import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded"; -import { SESSION_KEYS, getKey } from "@ente/shared/storage/sessionStorage"; -import { styled } from "@mui/material"; -import Typography from "@mui/material/Typography"; -import { HttpStatusCode } from "axios"; -import DeduplicateOptions from "components/pages/dedupe/SelectedFileOptions"; -import PhotoFrame from "components/PhotoFrame"; -import { t } from "i18next"; -import { default as Router, default as router } from "next/router"; -import { createContext, useEffect, useState } from "react"; -import { Duplicate, getDuplicates } from "services/deduplicationService"; -import { SelectedState } from "types/gallery"; -import { getSelectedFiles } from "utils/file"; - -export interface DeduplicateContextType { - isOnDeduplicatePage: boolean; - collectionNameMap: Map; -} - -export const DeduplicateContext = createContext({ - isOnDeduplicatePage: false, - collectionNameMap: new Map(), -}); - -export const Info = styled("div")` - padding: 24px; - font-size: 18px; -`; - -export default function Deduplicate() { - const { showNavBar, showLoadingBar, hideLoadingBar, showMiniDialog } = - useAppContext(); - const [duplicates, setDuplicates] = useState(null); - const [collectionNameMap, setCollectionNameMap] = useState( - new Map(), - ); - const [selected, setSelected] = useState({ - count: 0, - collectionID: 0, - ownCount: 0, - context: undefined, - }); - const closeDeduplication = function () { - Router.push(PAGES.GALLERY); - }; - useEffect(() => { - const key = getKey(SESSION_KEYS.ENCRYPTION_KEY); - if (!key) { - stashRedirect(PAGES.DEDUPLICATE); - router.push("/"); - return; - } - showNavBar(true); - }, []); - - useEffect(() => { - syncWithRemote(); - }, []); - - const syncWithRemote = async () => { - showLoadingBar(); - try { - const collections = await getLocalCollections(); - const collectionNameMap = new Map(); - for (const collection of collections) { - collectionNameMap.set(collection.id, collection.name); - } - setCollectionNameMap(collectionNameMap); - const files = await getLocalFiles(); - const duplicateFiles = await getDuplicates( - files, - collectionNameMap, - ); - const currFileSizeMap = new Map(); - let toSelectFileIDs: number[] = []; - let count = 0; - for (const dupe of duplicateFiles) { - // select all except first file - toSelectFileIDs = [ - ...toSelectFileIDs, - ...dupe.files.slice(1).map((f) => f.id), - ]; - count += dupe.files.length - 1; - - for (const file of dupe.files) { - currFileSizeMap.set(file.id, dupe.size); - } - } - setDuplicates(duplicateFiles); - const selectedFiles = { - count: count, - ownCount: count, - collectionID: ALL_SECTION, - context: undefined, - }; - for (const fileID of toSelectFileIDs) { - selectedFiles[fileID] = true; - } - setSelected(selectedFiles); - } finally { - hideLoadingBar(); - } - }; - - const duplicateFiles = useMemoSingleThreaded(() => { - return (duplicates ?? []).reduce((acc, dupe) => { - return [...acc, ...dupe.files]; - }, []); - }, [duplicates]); - - const fileToCollectionsMap = useMemoSingleThreaded(() => { - return createFileCollectionIDs(duplicateFiles ?? []); - }, [duplicateFiles]); - - const deleteFileHelper = async () => { - try { - showLoadingBar(); - const selectedFiles = getSelectedFiles(selected, duplicateFiles); - await moveToTrash(selectedFiles); - - // moveToTrash above does an API request, we still need to update - // our local state. - // - // Enhancement: This can be done in a more granular manner. Also, it - // is better to funnel these syncs instead of adding these here and - // there in an ad-hoc manner. For now, this fixes the issue with the - // UI not updating if the user deletes only some of the duplicates. - const collections = await getAllLatestCollections(); - await syncFiles( - "normal", - collections, - () => {}, - () => {}, - ); - await syncTrash(collections, () => {}); - await syncWithRemote(); - } catch (e) { - log.error("Dedup delete failed", e); - await syncWithRemote(); - // See: [Note: Chained MiniDialogs] - setTimeout(() => { - showMiniDialog( - errorDialogAttributes( - e instanceof ApiError && - e.httpStatusCode == HttpStatusCode.Forbidden - ? t("not_file_owner_delete_error") - : t("generic_error"), - ), - ); - }, 0); - } finally { - hideLoadingBar(); - } - }; - - const clearSelection = function () { - setSelected({ - count: 0, - collectionID: 0, - ownCount: 0, - context: undefined, - }); - }; - - if (!duplicates) { - return ( - - - - ); - } - - return ( - - {duplicateFiles.length > 0 && ( - {t("DEDUPLICATE_BASED_ON_SIZE")} - )} - {duplicateFiles.length === 0 ? ( - - - {t("NO_DUPLICATES_FOUND")} - - - ) : ( - - )} - - - ); -} diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 7b8d799930..4c2550a32a 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -1029,7 +1029,6 @@ export default function Gallery() { ) : ( , -) { - try { - const ascDupes = await fetchDuplicateFileIDs(); - - const descSortedDupes = ascDupes.sort((firstDupe, secondDupe) => { - return secondDupe.size - firstDupe.size; - }); - - const fileMap = new Map(); - for (const file of files) { - fileMap.set(file.id, file); - } - - let result: Duplicate[] = []; - - for (const dupe of descSortedDupes) { - let duplicateFiles: EnteFile[] = []; - for (const fileID of dupe.fileIDs) { - if (fileMap.has(fileID)) { - duplicateFiles.push(fileMap.get(fileID)); - } - } - duplicateFiles = await sortDuplicateFiles( - duplicateFiles, - collectionNameMap, - ); - - if (duplicateFiles.length > 1) { - result = [ - ...result, - ...getDupesGroupedBySameFileHashes({ - files: duplicateFiles, - size: dupe.size, - }), - ]; - } - } - - return result; - } catch (e) { - log.error("failed to get duplicate files", e); - } -} - -const hasFileHash = (file: Metadata) => !!metadataHash(file); - -function getDupesGroupedBySameFileHashes(dupe: Duplicate) { - const result: Duplicate[] = []; - - const fileWithHashes: EnteFile[] = []; - const fileWithoutHashes: EnteFile[] = []; - for (const file of dupe.files) { - if (hasFileHash(file.metadata)) { - fileWithHashes.push(file); - } else { - fileWithoutHashes.push(file); - } - } - - if (fileWithHashes.length > 1) { - result.push( - ...groupDupesByFileHashes({ - files: fileWithHashes, - size: dupe.size, - }), - ); - } - - if (fileWithoutHashes.length > 1) { - result.push({ - files: fileWithoutHashes, - size: dupe.size, - }); - } - return result; -} - -function groupDupesByFileHashes(dupe: Duplicate) { - const result: Duplicate[] = []; - - const filesSortedByFileHash = dupe.files - .map((file) => { - return { - file, - hash: metadataHash(file.metadata), - }; - }) - .sort((firstFile, secondFile) => { - return firstFile.hash.localeCompare(secondFile.hash); - }); - - let sameHashFiles: EnteFile[] = []; - sameHashFiles.push(filesSortedByFileHash[0].file); - for (let i = 1; i < filesSortedByFileHash.length; i++) { - if ( - areFileHashesSame( - filesSortedByFileHash[i - 1].file.metadata, - filesSortedByFileHash[i].file.metadata, - ) - ) { - sameHashFiles.push(filesSortedByFileHash[i].file); - } else { - if (sameHashFiles.length > 1) { - result.push({ - files: [...sameHashFiles], - size: dupe.size, - }); - } - sameHashFiles = [filesSortedByFileHash[i].file]; - } - } - if (sameHashFiles.length > 1) { - result.push({ - files: sameHashFiles, - size: dupe.size, - }); - } - - return result; -} - -async function fetchDuplicateFileIDs() { - try { - const response = await HTTPService.get( - await apiURL("/files/duplicates"), - null, - { - "X-Auth-Token": getToken(), - }, - ); - return (response.data as DuplicatesResponse).duplicates; - } catch (e) { - log.error("failed to fetch duplicate file IDs", e); - } -} - -async function sortDuplicateFiles( - files: EnteFile[], - collectionNameMap: Map, -) { - return files.sort((firstFile, secondFile) => { - const firstCollectionName = collectionNameMap - .get(firstFile.collectionID) - .toLocaleLowerCase(); - const secondCollectionName = collectionNameMap - .get(secondFile.collectionID) - .toLocaleLowerCase(); - return firstCollectionName.localeCompare(secondCollectionName); - }); -} - -function areFileHashesSame(firstFile: Metadata, secondFile: Metadata) { - return metadataHash(firstFile) === metadataHash(secondFile); -} diff --git a/web/packages/base/locales/en-US/translation.json b/web/packages/base/locales/en-US/translation.json index 3b7a70ae0f..a235afdd45 100644 --- a/web/packages/base/locales/en-US/translation.json +++ b/web/packages/base/locales/en-US/translation.json @@ -425,10 +425,7 @@ "no_duplicates": "No duplicates", "duplicate_group_description": "{{count}} items, {{itemSize}} each", "remove_duplicates_button_count": "Delete {{count, number}} items", - "NO_DUPLICATES_FOUND": "You have no duplicate files that can be cleared", "FILES": "files", - "EACH": "each", - "DEDUPLICATE_BASED_ON_SIZE": "The following files were clubbed based on their sizes, please review and delete items you believe are duplicates", "stop_uploads_title": "Stop uploads?", "stop_uploads_message": "Are you sure that you want to stop all the uploads in progress?", "yes_stop_uploads": "Yes, stop uploads", diff --git a/web/packages/shared/constants/pages.tsx b/web/packages/shared/constants/pages.tsx index 45dd108a2a..1348c0f4ae 100644 --- a/web/packages/shared/constants/pages.tsx +++ b/web/packages/shared/constants/pages.tsx @@ -13,7 +13,6 @@ export enum PHOTOS_PAGES { VERIFY = "/verify", ROOT = "/", SHARED_ALBUMS = "/shared-albums", - DEDUPLICATE = "/deduplicate", } export enum AUTH_PAGES {