Replace old impl

This commit is contained in:
Manav Rathi
2024-12-27 11:08:58 +05:30
parent 28ab3c321c
commit eca0137426
12 changed files with 17 additions and 929 deletions

View File

@@ -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<void>;
favItemIds?: Set<number>;
setSelected: (
@@ -96,8 +88,6 @@ export interface PhotoFrameProps {
}
const PhotoFrame = ({
page,
duplicates,
mode,
modePlus,
files,
@@ -476,30 +466,19 @@ const PhotoFrame = ({
return (
<Container>
<AutoSizer>
{({ height, width }) =>
page === PHOTOS_PAGES.DEDUPLICATE ? (
<DedupePhotoList
width={width}
height={height}
getThumbnail={getThumbnail}
duplicates={duplicates}
activeCollectionID={activeCollectionID}
showAppDownloadBanner={showAppDownloadBanner}
/>
) : (
<PhotoList
width={width}
height={height}
getThumbnail={getThumbnail}
mode={mode}
modePlus={modePlus}
displayFiles={displayFiles}
activeCollectionID={activeCollectionID}
activePersonID={activePersonID}
showAppDownloadBanner={showAppDownloadBanner}
/>
)
}
{({ height, width }) => (
<PhotoList
width={width}
height={height}
getThumbnail={getThumbnail}
mode={mode}
modePlus={modePlus}
displayFiles={displayFiles}
activeCollectionID={activeCollectionID}
activePersonID={activePersonID}
showAppDownloadBanner={showAppDownloadBanner}
/>
)}
</AutoSizer>
<PhotoViewer
isOpen={open}

View File

@@ -1,373 +0,0 @@
import { EnteFile } from "@/media/file";
import {
GAP_BTW_TILES,
IMAGE_CONTAINER_MAX_HEIGHT,
IMAGE_CONTAINER_MAX_WIDTH,
MIN_COLUMNS,
} from "@/new/photos/components/PhotoList";
import { formattedByteSize } from "@/new/photos/utils/units";
import { FlexWrapper } from "@ente/shared/components/Container";
import { Box, styled } from "@mui/material";
import { t } from "i18next";
import memoize from "memoize-one";
import React, { useEffect, useMemo, useRef, useState } from "react";
import {
VariableSizeList as List,
ListChildComponentProps,
areEqual,
} from "react-window";
import { Duplicate } from "services/deduplicationService";
import {
DATE_CONTAINER_HEIGHT,
SIZE_AND_COUNT_CONTAINER_HEIGHT,
SPACE_BTW_DATES,
} from "components/PhotoList";
export enum ITEM_TYPE {
TIME = "TIME",
FILE = "FILE",
SIZE_AND_COUNT = "SIZE_AND_COUNT",
HEADER = "HEADER",
FOOTER = "FOOTER",
MARKETING_FOOTER = "MARKETING_FOOTER",
OTHER = "OTHER",
}
export interface TimeStampListItem {
itemType: ITEM_TYPE;
items?: EnteFile[];
itemStartIndex?: number;
date?: string;
dates?: {
date: string;
span: number;
}[];
groups?: number[];
item?: any;
id?: string;
height?: number;
fileSize?: number;
fileCount?: number;
}
const ListItem = styled("div")`
display: flex;
justify-content: center;
`;
const getTemplateColumns = (
columns: number,
shrinkRatio: number,
groups?: number[],
): string => {
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<ItemData>) => {
const { timeStampList, columns, shrinkRatio, renderListItem } = data;
return (
<ListItem style={style}>
<ListContainer
columns={columns}
shrinkRatio={shrinkRatio}
groups={timeStampList[index].groups}
>
{renderListItem(timeStampList[index], isScrolling)}
</ListContainer>
</ListItem>
);
},
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<TimeStampListItem[]>([]);
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
*/
<SizeAndCountContainer span={columns}>
{listItem.fileCount} {t("FILES")},{" "}
{formattedByteSize(listItem.fileSize || 0)} {t("EACH")}
</SizeAndCountContainer>
);
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,
<div key={`${listItem.items[0].id}-gap-${i}`} />,
);
sum += 1;
}
}
return ret;
}
default:
return listItem.item;
}
};
if (!timeStampList?.length) {
return <></>;
}
const itemData = createItemData(
timeStampList,
columns,
shrinkRatio,
renderListItem,
);
return (
<List
key={`${activeCollectionID}`}
itemData={itemData}
ref={listRef}
itemSize={getItemSize(timeStampList)}
height={height}
width={width}
itemCount={timeStampList.length}
itemKey={generateKey}
overscanCount={3}
useIsScrolling
>
{PhotoListRow}
</List>
);
}

View File

@@ -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}
</DateContainer>
);
case ITEM_TYPE.SIZE_AND_COUNT:
return (
<SizeAndCountContainer span={columns}>
{listItem.fileCount} {t("FILES")},{" "}
{formattedByteSize(listItem.fileSize || 0)} {t("EACH")}
</SizeAndCountContainer>
);
case ITEM_TYPE.FILE: {
const ret = listItem.items.map((item, idx) =>
getThumbnail(

View File

@@ -514,7 +514,7 @@ const UtilitySection: React.FC<UtilitySectionProps> = ({ 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<UtilitySectionProps> = ({ closeSidebar }) => {
/>
<EnteMenuItem
variant="secondary"
onClick={redirectToDeduplicatePage}
onClick={handleDeduplicate}
label={t("DEDUPLICATE_FILES")}
/>
<EnteMenuItem

View File

@@ -1,58 +0,0 @@
import { SelectionBar } from "@/base/components/Navbar";
import { AppContext } from "@/new/photos/types/context";
import { FluidContainer } from "@ente/shared/components/Container";
import BackButton from "@mui/icons-material/ArrowBackOutlined";
import CloseIcon from "@mui/icons-material/Close";
import DeleteIcon from "@mui/icons-material/Delete";
import { Box, IconButton, Tooltip } from "@mui/material";
import { t } from "i18next";
import { useContext } from "react";
interface IProps {
deleteFileHelper: () => 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 (
<SelectionBar>
<FluidContainer>
{count ? (
<IconButton onClick={clearSelection}>
<CloseIcon />
</IconButton>
) : (
<IconButton onClick={close}>
<BackButton />
</IconButton>
)}
<Box ml={1.5}>{t("selected_count", { selected: count })}</Box>
</FluidContainer>
<Tooltip title={t("delete")}>
<IconButton onClick={trashHandler}>
<DeleteIcon />
</IconButton>
</Tooltip>
</SelectionBar>
);
}

View File

@@ -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 (
<Cont
key={`thumb-${file.id}}`}
onClick={handleClick}
@@ -372,16 +370,6 @@ export default function PreviewCard(props: IProps) {
<InSelectRangeOverLay
$active={isRangeSelectActive && isInsSelectRange}
/>
{deduplicateContext.isOnDeduplicatePage && (
<FileAndCollectionNameOverlay>
<p>{file.metadata.title}</p>
<p>
{deduplicateContext.collectionNameMap.get(
file.collectionID,
)}
</p>
</FileAndCollectionNameOverlay>
)}
{props?.activeCollectionID === TRASH_SECTION && file.isTrashed && (
<FileAndCollectionNameOverlay>
<p>{formatDateRelative(file.deleteBy / 1000)}</p>
@@ -389,25 +377,6 @@ export default function PreviewCard(props: IProps) {
)}
</Cont>
);
if (deduplicateContext.isOnDeduplicatePage) {
return (
<Tooltip
placement="bottom-start"
enterDelay={300}
enterNextDelay={100}
title={`${
file.metadata.title
} - ${deduplicateContext.collectionNameMap.get(
file.collectionID,
)}`}
>
{renderFn()}
</Tooltip>
);
} else {
return renderFn();
}
}
function formatDateRelative(date: number) {

View File

@@ -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<number, string>;
}
export const DeduplicateContext = createContext<DeduplicateContextType>({
isOnDeduplicatePage: false,
collectionNameMap: new Map<number, string>(),
});
export const Info = styled("div")`
padding: 24px;
font-size: 18px;
`;
export default function Deduplicate() {
const { showNavBar, showLoadingBar, hideLoadingBar, showMiniDialog } =
useAppContext();
const [duplicates, setDuplicates] = useState<Duplicate[]>(null);
const [collectionNameMap, setCollectionNameMap] = useState(
new Map<number, string>(),
);
const [selected, setSelected] = useState<SelectedState>({
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<number, string>();
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<number, number>();
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 (
<VerticallyCentered>
<ActivityIndicator />
</VerticallyCentered>
);
}
return (
<DeduplicateContext.Provider
value={{
collectionNameMap,
isOnDeduplicatePage: true,
}}
>
{duplicateFiles.length > 0 && (
<Info>{t("DEDUPLICATE_BASED_ON_SIZE")}</Info>
)}
{duplicateFiles.length === 0 ? (
<VerticallyCentered>
<Typography variant="large" color="text.muted">
{t("NO_DUPLICATES_FOUND")}
</Typography>
</VerticallyCentered>
) : (
<PhotoFrame
page={PAGES.DEDUPLICATE}
files={duplicateFiles}
duplicates={duplicates}
syncWithRemote={syncWithRemote}
setSelected={setSelected}
selected={selected}
activeCollectionID={ALL_SECTION}
fileToCollectionsMap={fileToCollectionsMap}
collectionNameMap={collectionNameMap}
selectable={true}
/>
)}
<DeduplicateOptions
deleteFileHelper={deleteFileHelper}
count={selected.count}
close={closeDeduplication}
clearSelection={clearSelection}
/>
</DeduplicateContext.Provider>
);
}

View File

@@ -1029,7 +1029,6 @@ export default function Gallery() {
<PeopleEmptyState />
) : (
<PhotoFrame
page={PAGES.GALLERY}
mode={barMode}
modePlus={isInSearchMode ? "search" : barMode}
files={filteredFiles}

View File

@@ -497,7 +497,6 @@ export default function PublicCollectionGallery() {
/>
<SharedAlbumNavbar onAddPhotos={onAddPhotos} />
<PhotoFrame
page={PAGES.SHARED_ALBUMS}
files={publicFiles}
syncWithRemote={syncWithRemote}
setSelected={setSelected}

View File

@@ -1,176 +0,0 @@
import log from "@/base/log";
import { apiURL } from "@/base/origins";
import { EnteFile } from "@/media/file";
import { metadataHash, type Metadata } from "@/media/file-metadata";
import HTTPService from "@ente/shared/network/HTTPService";
import { getToken } from "@ente/shared/storage/localStorage/helpers";
interface DuplicatesResponse {
duplicates: {
fileIDs: number[];
size: number;
}[];
}
export interface Duplicate {
files: EnteFile[];
size: number;
}
export async function getDuplicates(
files: EnteFile[],
collectionNameMap: Map<number, string>,
) {
try {
const ascDupes = await fetchDuplicateFileIDs();
const descSortedDupes = ascDupes.sort((firstDupe, secondDupe) => {
return secondDupe.size - firstDupe.size;
});
const fileMap = new Map<number, EnteFile>();
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<number, string>,
) {
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);
}

View File

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

View File

@@ -13,7 +13,6 @@ export enum PHOTOS_PAGES {
VERIFY = "/verify",
ROOT = "/",
SHARED_ALBUMS = "/shared-albums",
DEDUPLICATE = "/deduplicate",
}
export enum AUTH_PAGES {