From 1d02732719e8a544539db6ef411994045b36f1ff Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 11:43:07 +0530 Subject: [PATCH 01/21] Impl --- web/packages/media/file.ts | 3 ++ web/packages/new/photos/services/dedup.ts | 45 ++++++++++++++++++----- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/web/packages/media/file.ts b/web/packages/media/file.ts index ff6231fde1..e44846ab54 100644 --- a/web/packages/media/file.ts +++ b/web/packages/media/file.ts @@ -22,6 +22,9 @@ export interface S3FileAttributes { } export interface FileInfo { + /** + * The size of the file, in bytes. + */ fileSize: number; thumbSize: number; } diff --git a/web/packages/new/photos/services/dedup.ts b/web/packages/new/photos/services/dedup.ts index 163fb71520..49f5132246 100644 --- a/web/packages/new/photos/services/dedup.ts +++ b/web/packages/new/photos/services/dedup.ts @@ -1,5 +1,5 @@ import type { EnteFile } from "@/media/file"; -import { wait } from "@/utils/promise"; +import { getLocalFiles, uniqueFilesByID } from "./files"; /** * A group of duplicates as shown in the UI. @@ -65,14 +65,39 @@ export interface DuplicateGroup { * 4. Delete the remaining files. */ export const deduceDuplicates = async () => { - await wait(1000); - return [ - { - items: [], - itemSize: 0, - prunableCount: 0, - prunableSize: 0, + const collectionFiles = await getLocalFiles(); + const files = uniqueFilesByID(collectionFiles); + + const filesByHash = new Map(); + for (const file of files) { + let hash = file.metadata.hash; + if (!hash && file.metadata.imageHash && file.metadata.videoHash) + hash = `${file.metadata.imageHash}_${file.metadata.hash}`; + if (!hash) { + // Some very old files uploaded by ancient versions of Ente might + // not have hashes. Ignore these. + continue; + } + + filesByHash.set(hash, [...(filesByHash.get(hash) ?? []), file]); + } + + const duplicateGroups: DuplicateGroup[] = []; + + for (const potentialDuplicates of filesByHash.values()) { + if (potentialDuplicates.length < 2) continue; + const size = potentialDuplicates[0]?.info?.fileSize; + if (!size) continue; + const duplicates = potentialDuplicates.filter( + (file) => file.info?.fileSize == size, + ); + if (duplicates.length < 2) continue; + duplicateGroups.push({ + items: duplicates.map((file) => ({ file, collectionName: "TODO" })), + itemSize: size, + prunableCount: duplicates.length - 1, + prunableSize: size * (duplicates.length - 1), isSelected: true, - }, - ]; + }); + } }; From 76308cc9d0887d32b361f689fab96d0b8101408b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 11:56:18 +0530 Subject: [PATCH 02/21] Name --- .../PhotoViewer/ImageEditorOverlay.tsx | 2 +- web/apps/photos/src/pages/deduplicate.tsx | 10 +++---- web/apps/photos/src/pages/gallery.tsx | 2 +- .../photos/src/services/collectionService.ts | 27 +++++-------------- web/apps/photos/src/services/export/index.ts | 2 +- .../photos/src/services/export/migration.ts | 2 +- web/apps/photos/src/utils/collection.ts | 14 ++-------- web/apps/photos/tests/upload.test.ts | 2 +- .../new/photos/services/collection/index.ts | 18 +++++++++++++ web/packages/new/photos/services/dedup.ts | 6 +++++ 10 files changed, 43 insertions(+), 42 deletions(-) diff --git a/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay.tsx b/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay.tsx index f3000d9fc4..5a829e3998 100644 --- a/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay.tsx +++ b/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay.tsx @@ -11,6 +11,7 @@ import { downloadAndRevokeObjectURL } from "@/base/utils/web"; import { downloadManager } from "@/gallery/services/download"; import { EnteFile } from "@/media/file"; import { photosDialogZIndex } from "@/new/photos/components/utils/z-index"; +import { getLocalCollections } from "@/new/photos/services/collection"; import { AppContext } from "@/new/photos/types/context"; import { CenteredFlex, @@ -52,7 +53,6 @@ import React, { type MutableRefObject, type Ref, } from "react"; -import { getLocalCollections } from "services/collectionService"; import uploadManager from "services/upload/uploadManager"; interface ImageEditorOverlayProps { diff --git a/web/apps/photos/src/pages/deduplicate.tsx b/web/apps/photos/src/pages/deduplicate.tsx index ce4b3b045e..e5744910ac 100644 --- a/web/apps/photos/src/pages/deduplicate.tsx +++ b/web/apps/photos/src/pages/deduplicate.tsx @@ -2,7 +2,10 @@ 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 } from "@/new/photos/services/collection"; +import { + ALL_SECTION, + getLocalCollections, +} from "@/new/photos/services/collection"; import { createFileCollectionIDs } from "@/new/photos/services/file"; import { getLocalFiles } from "@/new/photos/services/files"; import { useAppContext } from "@/new/photos/types/context"; @@ -19,10 +22,7 @@ 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 { - getAllLatestCollections, - getLocalCollections, -} from "services/collectionService"; +import { getAllLatestCollections } from "services/collectionService"; import { Duplicate, getDuplicates } from "services/deduplicationService"; import { syncFiles, trashFiles } from "services/fileService"; import { syncTrash } from "services/trashService"; diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 456b678a7f..2aa7d6308e 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -113,8 +113,8 @@ import { createAlbum, createUnCategorizedCollection, getAllLatestCollections, - getAllLocalCollections, } from "services/collectionService"; +import { getAllLocalCollections } from "@/new/photos/services/collection"; import { syncFiles } from "services/fileService"; import { preFileInfoSync, sync } from "services/sync"; import { syncTrash } from "services/trashService"; diff --git a/web/apps/photos/src/services/collectionService.ts b/web/apps/photos/src/services/collectionService.ts index a4eda9232c..d24195f02f 100644 --- a/web/apps/photos/src/services/collectionService.ts +++ b/web/apps/photos/src/services/collectionService.ts @@ -21,6 +21,8 @@ import { import { EncryptedMagicMetadata, EnteFile } from "@/media/file"; import { ItemVisibility } from "@/media/file-metadata"; import { + getAllLocalCollections, + getLocalCollections, isDefaultHiddenCollection, isHiddenCollection, } from "@/new/photos/services/collection"; @@ -42,8 +44,6 @@ import { getActualKey } from "@ente/shared/user"; import type { User } from "@ente/shared/user/types"; import { changeCollectionSubType, - getHiddenCollections, - getNonHiddenCollections, isQuickLinkCollection, isValidMoveTarget, } from "utils/collection"; @@ -51,6 +51,7 @@ import { UpdateMagicMetadataRequest } from "./fileService"; import { getPublicKey } from "./userService"; const COLLECTION_TABLE = "collections"; + const COLLECTION_UPDATION_TIME = "collection-updation-time"; const HIDDEN_COLLECTION_IDS = "hidden-collection-ids"; @@ -192,21 +193,6 @@ const getCollections = async ( } }; -export const getLocalCollections = async ( - type: "normal" | "hidden" = "normal", -): Promise => { - const collections = await getAllLocalCollections(); - return type === "normal" - ? getNonHiddenCollections(collections) - : getHiddenCollections(collections); -}; - -export const getAllLocalCollections = async (): Promise => { - const collections: Collection[] = - (await localForage.getItem(COLLECTION_TABLE)) ?? []; - return collections; -}; - export const getCollectionUpdationTime = async (): Promise => (await localForage.getItem(COLLECTION_UPDATION_TIME)) ?? 0; @@ -217,10 +203,11 @@ export const getLatestCollections = async ( type: "normal" | "hidden" = "normal", ): Promise => { const collections = await getAllLatestCollections(); - return type === "normal" - ? getNonHiddenCollections(collections) - : getHiddenCollections(collections); + return type == "normal" + ? collections.filter((c) => !isHiddenCollection(c)) + : collections.filter((c) => isHiddenCollection(c)); }; + export const getAllLatestCollections = async (): Promise => { const collections = await syncCollections(); return collections; diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index 9254989709..83526c7eea 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -26,7 +26,7 @@ import { PromiseQueue } from "@/utils/promise"; import { CustomError } from "@ente/shared/error"; import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage"; import i18n from "i18next"; -import { getAllLocalCollections } from "../collectionService"; +import { getAllLocalCollections } from "@/new/photos/services/collection"; import { migrateExport, type ExportRecord } from "./migration"; /** Name of the JSON file in which we keep the state of the export. */ diff --git a/web/apps/photos/src/services/export/migration.ts b/web/apps/photos/src/services/export/migration.ts index 32e96e012c..9f9b035c4b 100644 --- a/web/apps/photos/src/services/export/migration.ts +++ b/web/apps/photos/src/services/export/migration.ts @@ -6,6 +6,7 @@ import type { Collection } from "@/media/collection"; import { mergeMetadata, type EnteFile } from "@/media/file"; import { FileType } from "@/media/file-type"; import { decodeLivePhoto } from "@/media/live-photo"; +import { getLocalCollections } from "@/new/photos/services/collection"; import { exportMetadataDirectoryName } from "@/new/photos/services/export"; import { getAllLocalFiles } from "@/new/photos/services/files"; import { @@ -16,7 +17,6 @@ import { import { wait } from "@/utils/promise"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import type { User } from "@ente/shared/user/types"; -import { getLocalCollections } from "services/collectionService"; import { getIDBasedSortedFiles, getPersonalFiles } from "utils/file"; import { getCollectionIDFromFileUID, diff --git a/web/apps/photos/src/utils/collection.ts b/web/apps/photos/src/utils/collection.ts index 04f64d2345..eb727e3b5c 100644 --- a/web/apps/photos/src/utils/collection.ts +++ b/web/apps/photos/src/utils/collection.ts @@ -14,6 +14,8 @@ import { ItemVisibility } from "@/media/file-metadata"; import { DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME, findDefaultHiddenCollectionIDs, + getAllLocalCollections, + getLocalCollections, isHiddenCollection, isIncomingShare, } from "@/new/photos/services/collection"; @@ -26,8 +28,6 @@ import { t } from "i18next"; import { addToCollection, createAlbum, - getAllLocalCollections, - getLocalCollections, moveToCollection, removeFromCollection, restoreToCollection, @@ -343,16 +343,6 @@ export function getCollectionNameMap( ); } -export function getNonHiddenCollections( - collections: Collection[], -): Collection[] { - return collections.filter((collection) => !isHiddenCollection(collection)); -} - -export function getHiddenCollections(collections: Collection[]): Collection[] { - return collections.filter((collection) => isHiddenCollection(collection)); -} - export const getOrCreateAlbum = async ( albumName: string, existingCollections: Collection[], diff --git a/web/apps/photos/tests/upload.test.ts b/web/apps/photos/tests/upload.test.ts index 6ef2e4e6c2..6e5cfead6d 100644 --- a/web/apps/photos/tests/upload.test.ts +++ b/web/apps/photos/tests/upload.test.ts @@ -2,7 +2,7 @@ import { FileType } from "@/media/file-type"; import { groupFilesByCollectionID } from "@/new/photos/services/file"; import { getLocalFiles } from "@/new/photos/services/files"; -import { getLocalCollections } from "services/collectionService"; +import { getLocalCollections } from "@/new/photos/services/collection"; import { parseDateFromDigitGroups } from "services/upload/date"; import { MAX_FILE_NAME_LENGTH_GOOGLE_EXPORT, diff --git a/web/packages/new/photos/services/collection/index.ts b/web/packages/new/photos/services/collection/index.ts index 2f87512f6f..5e21366d1c 100644 --- a/web/packages/new/photos/services/collection/index.ts +++ b/web/packages/new/photos/services/collection/index.ts @@ -1,5 +1,6 @@ import { SUB_TYPE, type Collection } from "@/media/collection"; import { ItemVisibility } from "@/media/file-metadata"; +import localForage from "@ente/shared/storage/localForage"; import type { User } from "@ente/shared/user/types"; export const ARCHIVE_SECTION = -1; @@ -8,6 +9,23 @@ export const DUMMY_UNCATEGORIZED_COLLECTION = -3; export const HIDDEN_ITEMS_SECTION = -4; export const ALL_SECTION = 0; +const COLLECTION_TABLE = "collections"; + +export const getLocalCollections = async ( + type: "normal" | "hidden" = "normal", +): Promise => { + const collections = await getAllLocalCollections(); + return type == "normal" + ? collections.filter((c) => !isHiddenCollection(c)) + : collections.filter((c) => isHiddenCollection(c)); +}; + +export const getAllLocalCollections = async (): Promise => { + const collections: Collection[] = + (await localForage.getItem(COLLECTION_TABLE)) ?? []; + return collections; +}; + /** * Return true if this is a default hidden collection. * diff --git a/web/packages/new/photos/services/dedup.ts b/web/packages/new/photos/services/dedup.ts index 49f5132246..27f807ecf7 100644 --- a/web/packages/new/photos/services/dedup.ts +++ b/web/packages/new/photos/services/dedup.ts @@ -1,4 +1,5 @@ import type { EnteFile } from "@/media/file"; +import { getLocalCollections } from "./collection"; import { getLocalFiles, uniqueFilesByID } from "./files"; /** @@ -82,6 +83,11 @@ export const deduceDuplicates = async () => { filesByHash.set(hash, [...(filesByHash.get(hash) ?? []), file]); } + const collections = await getLocalCollections(); + const collectionNameByID = new Map(); + for (const collection of collections) + collectionNameByID.set(collection.id, collection.name); + const duplicateGroups: DuplicateGroup[] = []; for (const potentialDuplicates of filesByHash.values()) { From 69ccf7d3c90a96c14290026e903c0e8e01b5aa42 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 12:00:16 +0530 Subject: [PATCH 03/21] Shorten --- web/packages/new/photos/services/collection/index.ts | 10 ++-------- web/packages/new/photos/services/dedup.ts | 7 ++----- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/web/packages/new/photos/services/collection/index.ts b/web/packages/new/photos/services/collection/index.ts index 5e21366d1c..d51e90ca56 100644 --- a/web/packages/new/photos/services/collection/index.ts +++ b/web/packages/new/photos/services/collection/index.ts @@ -60,7 +60,6 @@ export const isHiddenCollection = (collection: Collection) => // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition collection.magicMetadata?.data.visibility === ItemVisibility.hidden; -// TODO: Does this need localizations? export const DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME = "Hidden"; /** @@ -79,10 +78,5 @@ export const getCollectionUserFacingName = (collection: Collection) => { /** * Return a map of the (user-facing) collection name, indexed by collection ID. */ -export const createCollectionNameByID = (allCollections: Collection[]) => - new Map( - allCollections.map((collection) => [ - collection.id, - getCollectionUserFacingName(collection), - ]), - ); +export const createCollectionNameByID = (collections: Collection[]) => + new Map(collections.map((c) => [c.id, getCollectionUserFacingName(c)])); diff --git a/web/packages/new/photos/services/dedup.ts b/web/packages/new/photos/services/dedup.ts index 27f807ecf7..6bede82f46 100644 --- a/web/packages/new/photos/services/dedup.ts +++ b/web/packages/new/photos/services/dedup.ts @@ -1,5 +1,5 @@ import type { EnteFile } from "@/media/file"; -import { getLocalCollections } from "./collection"; +import { createCollectionNameByID, getLocalCollections } from "./collection"; import { getLocalFiles, uniqueFilesByID } from "./files"; /** @@ -83,10 +83,7 @@ export const deduceDuplicates = async () => { filesByHash.set(hash, [...(filesByHash.get(hash) ?? []), file]); } - const collections = await getLocalCollections(); - const collectionNameByID = new Map(); - for (const collection of collections) - collectionNameByID.set(collection.id, collection.name); + const collectionNameByID = createCollectionNameByID(await getLocalCollections()); const duplicateGroups: DuplicateGroup[] = []; From a302f986d7c95ed254420f766919a26bff8550fc Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 12:04:32 +0530 Subject: [PATCH 04/21] Mirror files --- .../PhotoViewer/ImageEditorOverlay.tsx | 2 +- web/apps/photos/src/pages/deduplicate.tsx | 6 ++---- .../photos/src/services/collectionService.ts | 6 ++++-- .../photos/src/services/export/migration.ts | 2 +- web/apps/photos/src/utils/collection.ts | 6 ++++-- web/apps/photos/tests/upload.test.ts | 2 +- .../new/photos/services/collections.ts | 20 +++++++++++++++++++ web/packages/new/photos/services/dedup.ts | 7 +++++-- 8 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 web/packages/new/photos/services/collections.ts diff --git a/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay.tsx b/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay.tsx index 5a829e3998..c232bc698a 100644 --- a/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay.tsx +++ b/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay.tsx @@ -11,7 +11,7 @@ import { downloadAndRevokeObjectURL } from "@/base/utils/web"; import { downloadManager } from "@/gallery/services/download"; import { EnteFile } from "@/media/file"; import { photosDialogZIndex } from "@/new/photos/components/utils/z-index"; -import { getLocalCollections } from "@/new/photos/services/collection"; +import { getLocalCollections } from "@/new/photos/services/collections"; import { AppContext } from "@/new/photos/types/context"; import { CenteredFlex, diff --git a/web/apps/photos/src/pages/deduplicate.tsx b/web/apps/photos/src/pages/deduplicate.tsx index e5744910ac..9b3cb7722d 100644 --- a/web/apps/photos/src/pages/deduplicate.tsx +++ b/web/apps/photos/src/pages/deduplicate.tsx @@ -2,10 +2,8 @@ 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, - getLocalCollections, -} from "@/new/photos/services/collection"; +import { ALL_SECTION } from "@/new/photos/services/collection"; +import { getLocalCollections } from "@/new/photos/services/collections"; import { createFileCollectionIDs } from "@/new/photos/services/file"; import { getLocalFiles } from "@/new/photos/services/files"; import { useAppContext } from "@/new/photos/types/context"; diff --git a/web/apps/photos/src/services/collectionService.ts b/web/apps/photos/src/services/collectionService.ts index d24195f02f..e9ef53afd7 100644 --- a/web/apps/photos/src/services/collectionService.ts +++ b/web/apps/photos/src/services/collectionService.ts @@ -21,8 +21,6 @@ import { import { EncryptedMagicMetadata, EnteFile } from "@/media/file"; import { ItemVisibility } from "@/media/file-metadata"; import { - getAllLocalCollections, - getLocalCollections, isDefaultHiddenCollection, isHiddenCollection, } from "@/new/photos/services/collection"; @@ -31,6 +29,10 @@ import { CollectionSummaryOrder, CollectionsSortBy, } from "@/new/photos/services/collection/ui"; +import { + getAllLocalCollections, + getLocalCollections, +} from "@/new/photos/services/collections"; import { groupFilesByCollectionID } from "@/new/photos/services/file"; import { getLocalFiles, sortFiles } from "@/new/photos/services/files"; import { updateMagicMetadata } from "@/new/photos/services/magic-metadata"; diff --git a/web/apps/photos/src/services/export/migration.ts b/web/apps/photos/src/services/export/migration.ts index 9f9b035c4b..b5113ea226 100644 --- a/web/apps/photos/src/services/export/migration.ts +++ b/web/apps/photos/src/services/export/migration.ts @@ -6,7 +6,7 @@ import type { Collection } from "@/media/collection"; import { mergeMetadata, type EnteFile } from "@/media/file"; import { FileType } from "@/media/file-type"; import { decodeLivePhoto } from "@/media/live-photo"; -import { getLocalCollections } from "@/new/photos/services/collection"; +import { getLocalCollections } from "@/new/photos/services/collections"; import { exportMetadataDirectoryName } from "@/new/photos/services/export"; import { getAllLocalFiles } from "@/new/photos/services/files"; import { diff --git a/web/apps/photos/src/utils/collection.ts b/web/apps/photos/src/utils/collection.ts index eb727e3b5c..90b663565c 100644 --- a/web/apps/photos/src/utils/collection.ts +++ b/web/apps/photos/src/utils/collection.ts @@ -14,11 +14,13 @@ import { ItemVisibility } from "@/media/file-metadata"; import { DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME, findDefaultHiddenCollectionIDs, - getAllLocalCollections, - getLocalCollections, isHiddenCollection, isIncomingShare, } from "@/new/photos/services/collection"; +import { + getAllLocalCollections, + getLocalCollections, +} from "@/new/photos/services/collections"; import { getAllLocalFiles, getLocalFiles } from "@/new/photos/services/files"; import { updateMagicMetadata } from "@/new/photos/services/magic-metadata"; import { safeDirectoryName } from "@/new/photos/utils/native-fs"; diff --git a/web/apps/photos/tests/upload.test.ts b/web/apps/photos/tests/upload.test.ts index 6e5cfead6d..b8a9a904f3 100644 --- a/web/apps/photos/tests/upload.test.ts +++ b/web/apps/photos/tests/upload.test.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/dot-notation */ import { FileType } from "@/media/file-type"; +import { getLocalCollections } from "@/new/photos/services/collections"; import { groupFilesByCollectionID } from "@/new/photos/services/file"; import { getLocalFiles } from "@/new/photos/services/files"; -import { getLocalCollections } from "@/new/photos/services/collection"; import { parseDateFromDigitGroups } from "services/upload/date"; import { MAX_FILE_NAME_LENGTH_GOOGLE_EXPORT, diff --git a/web/packages/new/photos/services/collections.ts b/web/packages/new/photos/services/collections.ts new file mode 100644 index 0000000000..44f6b9f868 --- /dev/null +++ b/web/packages/new/photos/services/collections.ts @@ -0,0 +1,20 @@ +import { type Collection } from "@/media/collection"; +import localForage from "@ente/shared/storage/localForage"; +import { isHiddenCollection } from "./collection"; + +const COLLECTION_TABLE = "collections"; + +export const getLocalCollections = async ( + type: "normal" | "hidden" = "normal", +): Promise => { + const collections = await getAllLocalCollections(); + return type == "normal" + ? collections.filter((c) => !isHiddenCollection(c)) + : collections.filter((c) => isHiddenCollection(c)); +}; + +export const getAllLocalCollections = async (): Promise => { + const collections: Collection[] = + (await localForage.getItem(COLLECTION_TABLE)) ?? []; + return collections; +}; diff --git a/web/packages/new/photos/services/dedup.ts b/web/packages/new/photos/services/dedup.ts index 6bede82f46..baf84f79d9 100644 --- a/web/packages/new/photos/services/dedup.ts +++ b/web/packages/new/photos/services/dedup.ts @@ -1,5 +1,6 @@ import type { EnteFile } from "@/media/file"; -import { createCollectionNameByID, getLocalCollections } from "./collection"; +import { createCollectionNameByID } from "./collection"; +import { getLocalCollections } from "./collections"; import { getLocalFiles, uniqueFilesByID } from "./files"; /** @@ -83,7 +84,9 @@ export const deduceDuplicates = async () => { filesByHash.set(hash, [...(filesByHash.get(hash) ?? []), file]); } - const collectionNameByID = createCollectionNameByID(await getLocalCollections()); + const collectionNameByID = createCollectionNameByID( + await getLocalCollections(), + ); const duplicateGroups: DuplicateGroup[] = []; From 3263542f5ee31a802ae3a74fe3b46b7e91c8cf51 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 12:26:55 +0530 Subject: [PATCH 05/21] fin 1 --- web/packages/new/photos/services/dedup.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/web/packages/new/photos/services/dedup.ts b/web/packages/new/photos/services/dedup.ts index baf84f79d9..b5bd842a62 100644 --- a/web/packages/new/photos/services/dedup.ts +++ b/web/packages/new/photos/services/dedup.ts @@ -13,9 +13,13 @@ export interface DuplicateGroup { * These are sorted by the collectionName. */ items: { - /** The underlying collection file. */ + /** + * The underlying collection file. + */ file: EnteFile; - /** The name of the collection to which this file belongs. */ + /** + * The name of the collection to which this file belongs. + */ collectionName: string; }[]; /** @@ -97,13 +101,23 @@ export const deduceDuplicates = async () => { const duplicates = potentialDuplicates.filter( (file) => file.info?.fileSize == size, ); - if (duplicates.length < 2) continue; + const items = duplicates + .map((file) => { + const collectionName = collectionNameByID.get( + file.collectionID, + ); + return collectionName ? { file, collectionName } : undefined; + }) + .filter((item) => !!item); + if (items.length < 2) continue; duplicateGroups.push({ - items: duplicates.map((file) => ({ file, collectionName: "TODO" })), + items, itemSize: size, prunableCount: duplicates.length - 1, prunableSize: size * (duplicates.length - 1), isSelected: true, }); } + + return duplicateGroups; }; From a31803e3f576776872dfea9c6aefd027b3664e99 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 12:41:39 +0530 Subject: [PATCH 06/21] Appear --- web/packages/new/photos/pages/duplicates.tsx | 33 +++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/web/packages/new/photos/pages/duplicates.tsx b/web/packages/new/photos/pages/duplicates.tsx index fb6791b21a..f9644434e7 100644 --- a/web/packages/new/photos/pages/duplicates.tsx +++ b/web/packages/new/photos/pages/duplicates.tsx @@ -45,7 +45,9 @@ const Page: React.FC = () => { if (state.duplicateGroups.length == 0) { return ; } else { - return ; + return ( + + ); } default: return ; @@ -128,6 +130,7 @@ const dedupReducer: React.Reducer = ( return { ...state, status: "analysisFailed" }; case "analysisCompleted": { const duplicateGroups = action.duplicateGroups; + sortDuplicateGroups(duplicateGroups, state.sortOrder); const prunableCount = duplicateGroups.reduce( (sum, { prunableCount }) => sum + prunableCount, 0, @@ -150,6 +153,16 @@ const dedupReducer: React.Reducer = ( } }; +const sortDuplicateGroups = ( + duplicateGroups: DuplicateGroup[], + sortOrder: DedupState["sortOrder"], +) => + duplicateGroups.sort((a, b) => + sortOrder == "prunableSize" + ? b.prunableSize - a.prunableSize + : b.prunableCount - a.prunableCount, + ); + const Navbar: React.FC = () => { const router = useRouter(); @@ -201,7 +214,14 @@ const NoDuplicatesFound: React.FC = () => ( ); -const Duplicates: React.FC = () => { +interface DuplicatesProps { + /** + * Groups of duplicates. Guaranteed to be non-empty. + */ + duplicateGroups: DuplicateGroup[]; +} + +const Duplicates: React.FC = ({ duplicateGroups }) => { return ( @@ -215,12 +235,9 @@ const Duplicates: React.FC = () => { fontSize: "4rem", }} > -
1
-
1
-
1
-
1
-
1
-
1
+ {duplicateGroups.map((dup, i) => ( +
{dup.items.length}
+ ))}
)} From 1068b6811f12dd65046fb1185df8ee16337559d7 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 12:51:45 +0530 Subject: [PATCH 07/21] Dup --- web/packages/base/components/OverflowMenu.tsx | 182 ++++++++++++++++++ web/packages/new/photos/pages/duplicates.tsx | 8 + 2 files changed, 190 insertions(+) create mode 100644 web/packages/base/components/OverflowMenu.tsx diff --git a/web/packages/base/components/OverflowMenu.tsx b/web/packages/base/components/OverflowMenu.tsx new file mode 100644 index 0000000000..12db95409b --- /dev/null +++ b/web/packages/base/components/OverflowMenu.tsx @@ -0,0 +1,182 @@ +import { FluidContainer } from "@ente/shared/components/Container"; +import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; +import { + Box, + IconButton, + MenuItem, + styled, + Typography, + type ButtonProps, + type IconButtonProps, + type PaperProps, +} from "@mui/material"; +import Menu, { type MenuProps } from "@mui/material/Menu"; +import React, { createContext, useContext, useMemo, useState } from "react"; + +interface OverflowMenuContextT { + close: () => void; +} + +const OverflowMenuContext = createContext( + undefined, +); + +interface OverflowMenuProps { + /** + * An ARIA identifier for the overflow menu when it is displayed. + */ + ariaID: string; + /** + * The icon for the trigger button. + * + * If not provided, then by default the MoreHoriz icon from MUI is used. + */ + triggerButtonIcon?: React.ReactNode; + /** + * Optional additional properties for the trigger icon button. + */ + triggerButtonProps?: Partial; + /** + * Optional additional properties for the MUI {@link Paper} that underlies + * the {@link Menu}. + */ + menuPaperProps?: Partial; +} + +/** + * A custom MUI {@link Menu} with some Ente specific styling applied to it. + */ +export const StyledMenu = styled(Menu)` + & .MuiPaper-root { + margin: 16px auto; + box-shadow: + 0px 0px 6px rgba(0, 0, 0, 0.16), + 0px 3px 6px rgba(0, 0, 0, 0.12); + } + & .MuiList-root { + padding: 0; + border: none; + } +`; + +/** + * An overflow menu showing {@link OverflowMenuOptions}, alongwith a button to + * trigger the visibility of the menu. + */ +export const OverflowMenu: React.FC< + React.PropsWithChildren +> = ({ + ariaID, + triggerButtonIcon, + triggerButtonProps, + menuPaperProps, + children, +}) => { + const [anchorEl, setAnchorEl] = useState(); + const context = useMemo( + () => ({ close: () => setAnchorEl(undefined) }), + [], + ); + return ( + + setAnchorEl(event.currentTarget)} + aria-controls={anchorEl ? ariaID : undefined} + aria-haspopup="true" + aria-expanded={anchorEl ? "true" : undefined} + {...triggerButtonProps} + > + {triggerButtonIcon ?? } + + setAnchorEl(undefined)} + MenuListProps={{ + disablePadding: true, + "aria-labelledby": ariaID, + }} + slotProps={{ + paper: menuPaperProps, + }} + anchorOrigin={{ + vertical: "bottom", + horizontal: "right", + }} + transformOrigin={{ + vertical: "top", + horizontal: "right", + }} + > + {children} + + + ); +}; + +interface OverflowMenuOptionProps { + color?: ButtonProps["color"]; + /** + * An optional icon to show at the leading edge of the menu option. + */ + startIcon?: React.ReactNode; + /** + * An optional icon to show at the trailing edge of the menu option. + */ + endIcon?: React.ReactNode; + /** + * Called when the menu option is clicked. + */ + onClick: () => void; +} + +export const OverflowMenuOption: React.FC< + React.PropsWithChildren +> = ({ onClick, color = "primary", startIcon, endIcon, children }) => { + const menuContext = useContext(OverflowMenuContext)!; + + const handleClick = () => { + onClick(); + menuContext.close(); + }; + + return ( + theme.palette[color].main, + padding: 1.5, + "& .MuiSvgIcon-root": { + fontSize: "20px", + }, + }} + > + + {startIcon && ( + + {startIcon} + + )} + {children} + + {endIcon && ( + + {endIcon} + + )} + + ); +}; diff --git a/web/packages/new/photos/pages/duplicates.tsx b/web/packages/new/photos/pages/duplicates.tsx index f9644434e7..703ce30dbb 100644 --- a/web/packages/new/photos/pages/duplicates.tsx +++ b/web/packages/new/photos/pages/duplicates.tsx @@ -163,6 +163,14 @@ const sortDuplicateGroups = ( : b.prunableCount - a.prunableCount, ); +interface NavbarProps { + /** + * Called when the user changes the sort order using the sort order menu + * visible via the navbar. + */ + onChangeSortOrder: (sortOrder: DedupState["sortOrder"]) => void; +} + const Navbar: React.FC = () => { const router = useRouter(); From d30dce0896e75d2a44f1c4a702b12d69718f5d56 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 13:05:30 +0530 Subject: [PATCH 08/21] Menu --- web/packages/base/components/OverflowMenu.tsx | 3 ++ web/packages/new/photos/pages/duplicates.tsx | 51 +++++++++++++++++-- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/web/packages/base/components/OverflowMenu.tsx b/web/packages/base/components/OverflowMenu.tsx index 12db95409b..aa04a7b89a 100644 --- a/web/packages/base/components/OverflowMenu.tsx +++ b/web/packages/base/components/OverflowMenu.tsx @@ -131,6 +131,9 @@ interface OverflowMenuOptionProps { onClick: () => void; } +/** + * Individual options meant to be shown inside an {@link OverflowMenu}. + */ export const OverflowMenuOption: React.FC< React.PropsWithChildren > = ({ onClick, color = "primary", startIcon, endIcon, children }) => { diff --git a/web/packages/new/photos/pages/duplicates.tsx b/web/packages/new/photos/pages/duplicates.tsx index 703ce30dbb..99b3d41a31 100644 --- a/web/packages/new/photos/pages/duplicates.tsx +++ b/web/packages/new/photos/pages/duplicates.tsx @@ -2,9 +2,14 @@ import { ActivityErrorIndicator } from "@/base/components/ErrorIndicator"; import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; import { CenteredFill } from "@/base/components/mui/Container"; import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; +import { + OverflowMenu, + OverflowMenuOption, +} from "@/base/components/OverflowMenu"; import { pt } from "@/base/i18n"; import log from "@/base/log"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import TickIcon from "@mui/icons-material/Done"; import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; import SortIcon from "@mui/icons-material/Sort"; import { Box, IconButton, Stack, Typography } from "@mui/material"; @@ -56,7 +61,12 @@ const Page: React.FC = () => { return ( - + + dispatch({ type: "changeSortOrder", sortOrder }) + } + /> {contents} ); @@ -64,6 +74,8 @@ const Page: React.FC = () => { export default Page; +type SortOrder = "prunableCount" | "prunableSize"; + interface DedupState { status: | undefined @@ -86,7 +98,7 @@ interface DedupState { /** * The attribute to use for sorting {@link duplicateGroups}. */ - sortOrder: "prunableCount" | "prunableSize"; + sortOrder: SortOrder; /** * The number of files that will be pruned if the user decides to dedup the * current selection. @@ -103,7 +115,7 @@ type DedupAction = | { type: "analyze" } | { type: "analysisFailed" } | { type: "analysisCompleted"; duplicateGroups: DuplicateGroup[] } - | { type: "changeSortOrder"; sortOrder: DedupState["sortOrder"] } + | { type: "changeSortOrder"; sortOrder: SortOrder } | { type: "select"; index: number } | { type: "deselect"; index: number } | { type: "deselectAll" } @@ -164,14 +176,18 @@ const sortDuplicateGroups = ( ); interface NavbarProps { + /** + * The current sort order. + */ + sortOrder: SortOrder; /** * Called when the user changes the sort order using the sort order menu * visible via the navbar. */ - onChangeSortOrder: (sortOrder: DedupState["sortOrder"]) => void; + onChangeSortOrder: (sortOrder: SortOrder) => void; } -const Navbar: React.FC = () => { +const Navbar: React.FC = ({ sortOrder, onChangeSortOrder }) => { const router = useRouter(); return ( @@ -191,6 +207,31 @@ const Navbar: React.FC = () => { {pt("Remove duplicates")} + } + > + + ) : undefined + } + onClick={() => onChangeSortOrder("prunableSize")} + > + {pt("Total size")} + + + ) : undefined + } + onClick={() => onChangeSortOrder("prunableCount")} + > + {pt("Count")} + + From 8c5b77cd52249a052c31b7f1b7e91f230ac658af Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 13:08:12 +0530 Subject: [PATCH 09/21] Extr --- web/packages/new/photos/pages/duplicates.tsx | 51 +++++++++----------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/web/packages/new/photos/pages/duplicates.tsx b/web/packages/new/photos/pages/duplicates.tsx index 99b3d41a31..0ef1e80e17 100644 --- a/web/packages/new/photos/pages/duplicates.tsx +++ b/web/packages/new/photos/pages/duplicates.tsx @@ -207,34 +207,7 @@ const Navbar: React.FC = ({ sortOrder, onChangeSortOrder }) => { {pt("Remove duplicates")} - } - > - - ) : undefined - } - onClick={() => onChangeSortOrder("prunableSize")} - > - {pt("Total size")} - - - ) : undefined - } - onClick={() => onChangeSortOrder("prunableCount")} - > - {pt("Count")} - - - - - + @@ -243,6 +216,28 @@ const Navbar: React.FC = ({ sortOrder, onChangeSortOrder }) => { ); }; +type SortMenuProps = Pick; + +const SortMenu: React.FC = ({ + sortOrder, + onChangeSortOrder, +}) => ( + }> + : undefined} + onClick={() => onChangeSortOrder("prunableSize")} + > + {pt("Total size")} + + : undefined} + onClick={() => onChangeSortOrder("prunableCount")} + > + {pt("Count")} + + +); + const Loading: React.FC = () => ( From 34068d09ba335acdaaf394a8fd2d09038d1853ae Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 13:35:52 +0530 Subject: [PATCH 10/21] Use regular menu --- web/packages/base/components/OverflowMenu.tsx | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/web/packages/base/components/OverflowMenu.tsx b/web/packages/base/components/OverflowMenu.tsx index aa04a7b89a..54b06ab2ad 100644 --- a/web/packages/base/components/OverflowMenu.tsx +++ b/web/packages/base/components/OverflowMenu.tsx @@ -43,21 +43,6 @@ interface OverflowMenuProps { menuPaperProps?: Partial; } -/** - * A custom MUI {@link Menu} with some Ente specific styling applied to it. - */ -export const StyledMenu = styled(Menu)` - & .MuiPaper-root { - margin: 16px auto; - box-shadow: - 0px 0px 6px rgba(0, 0, 0, 0.16), - 0px 3px 6px rgba(0, 0, 0, 0.12); - } - & .MuiList-root { - padding: 0; - border: none; - } -`; /** * An overflow menu showing {@link OverflowMenuOptions}, alongwith a button to @@ -88,7 +73,7 @@ export const OverflowMenu: React.FC< > {triggerButtonIcon ?? } - {children} - + ); }; From 664c723c78a16a39eebf6d14ead335d302731f02 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 13:46:04 +0530 Subject: [PATCH 11/21] Tweak --- web/packages/base/components/OverflowMenu.tsx | 65 +++++++------------ 1 file changed, 23 insertions(+), 42 deletions(-) diff --git a/web/packages/base/components/OverflowMenu.tsx b/web/packages/base/components/OverflowMenu.tsx index 54b06ab2ad..7e032a45f2 100644 --- a/web/packages/base/components/OverflowMenu.tsx +++ b/web/packages/base/components/OverflowMenu.tsx @@ -1,12 +1,9 @@ -import { FluidContainer } from "@ente/shared/components/Container"; import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; import { - Box, IconButton, MenuItem, - styled, + Stack, Typography, - type ButtonProps, type IconButtonProps, type PaperProps, } from "@mui/material"; @@ -43,7 +40,6 @@ interface OverflowMenuProps { menuPaperProps?: Partial; } - /** * An overflow menu showing {@link OverflowMenuOptions}, alongwith a button to * trigger the visibility of the menu. @@ -79,20 +75,13 @@ export const OverflowMenu: React.FC< open={!!anchorEl} onClose={() => setAnchorEl(undefined)} MenuListProps={{ + // Disable padding at the top and bottom of the menu list. disablePadding: true, "aria-labelledby": ariaID, }} - slotProps={{ - paper: menuPaperProps, - }} - anchorOrigin={{ - vertical: "bottom", - horizontal: "right", - }} - transformOrigin={{ - vertical: "top", - horizontal: "right", - }} + slotProps={{ paper: menuPaperProps }} + anchorOrigin={{ vertical: "bottom", horizontal: "right" }} + transformOrigin={{ vertical: "top", horizontal: "right" }} > {children} @@ -101,7 +90,12 @@ export const OverflowMenu: React.FC< }; interface OverflowMenuOptionProps { - color?: ButtonProps["color"]; + /** + * The color of the text and icons. + * + * Default: "primary". + */ + color?: "primary"; /** * An optional icon to show at the leading edge of the menu option. */ @@ -135,36 +129,23 @@ export const OverflowMenuOption: React.FC< sx={{ minWidth: 220, color: (theme) => theme.palette[color].main, - padding: 1.5, + // Reduce the size of the icons a bit to make it fit better with + // the text. "& .MuiSvgIcon-root": { fontSize: "20px", }, }} > - - {startIcon && ( - - {startIcon} - - )} - {children} - - {endIcon && ( - - {endIcon} - - )} + + {startIcon} + + {children} + + {endIcon} + ); }; From 5583902433e319283b9b87e89a50143d368b3393 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 14:02:12 +0530 Subject: [PATCH 12/21] tt --- web/packages/new/photos/pages/duplicates.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/web/packages/new/photos/pages/duplicates.tsx b/web/packages/new/photos/pages/duplicates.tsx index 0ef1e80e17..017a254034 100644 --- a/web/packages/new/photos/pages/duplicates.tsx +++ b/web/packages/new/photos/pages/duplicates.tsx @@ -12,7 +12,7 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import TickIcon from "@mui/icons-material/Done"; import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; import SortIcon from "@mui/icons-material/Sort"; -import { Box, IconButton, Stack, Typography } from "@mui/material"; +import { Box, IconButton, Stack, Tooltip, Typography } from "@mui/material"; import { useRouter } from "next/router"; import React, { useEffect, useReducer } from "react"; import Autosizer from "react-virtualized-auto-sizer"; @@ -222,7 +222,14 @@ const SortMenu: React.FC = ({ sortOrder, onChangeSortOrder, }) => ( - }> + + + + } + > : undefined} onClick={() => onChangeSortOrder("prunableSize")} From 4bbe71e1354e14f0b4d4c869365bb733eecc0b55 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 14:11:17 +0530 Subject: [PATCH 13/21] Use --- .../Collections/CollectionHeader.tsx | 12 ++++++------ web/packages/base/components/OverflowMenu.tsx | 19 +++++++++++++------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/web/apps/photos/src/components/Collections/CollectionHeader.tsx b/web/apps/photos/src/components/Collections/CollectionHeader.tsx index 3077cf341c..38e6805c5c 100644 --- a/web/apps/photos/src/components/Collections/CollectionHeader.tsx +++ b/web/apps/photos/src/components/Collections/CollectionHeader.tsx @@ -1,6 +1,10 @@ import { assertionFailed } from "@/base/assert"; import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; import { SpaceBetweenFlex } from "@/base/components/mui/Container"; +import { + OverflowMenu, + OverflowMenuOption, +} from "@/base/components/OverflowMenu"; import { useModalVisibility } from "@/base/components/utils/modal"; import type { Collection } from "@/media/collection"; import { ItemVisibility } from "@/media/file-metadata"; @@ -23,11 +27,7 @@ import { } from "@/new/photos/services/magic-metadata"; import { useAppContext } from "@/new/photos/types/context"; import { HorizontalFlex } from "@ente/shared/components/Container"; -import { - OverflowMenu, - OverflowMenuOption, - StyledMenu, -} from "@ente/shared/components/OverflowMenu"; +import { StyledMenu } from "@ente/shared/components/OverflowMenu"; import ArchiveOutlined from "@mui/icons-material/ArchiveOutlined"; import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined"; import EditIcon from "@mui/icons-material/Edit"; @@ -330,7 +330,7 @@ const CollectionOptions: React.FC = ({ /> } > {collectionSummaryType == "trash" ? ( diff --git a/web/packages/base/components/OverflowMenu.tsx b/web/packages/base/components/OverflowMenu.tsx index 7e032a45f2..4ba7e72206 100644 --- a/web/packages/base/components/OverflowMenu.tsx +++ b/web/packages/base/components/OverflowMenu.tsx @@ -90,12 +90,16 @@ export const OverflowMenu: React.FC< }; interface OverflowMenuOptionProps { + /** + * Called when the menu option is clicked. + */ + onClick: () => void; /** * The color of the text and icons. * * Default: "primary". */ - color?: "primary"; + color?: "primary" | "critical"; /** * An optional icon to show at the leading edge of the menu option. */ @@ -104,10 +108,6 @@ interface OverflowMenuOptionProps { * An optional icon to show at the trailing edge of the menu option. */ endIcon?: React.ReactNode; - /** - * Called when the menu option is clicked. - */ - onClick: () => void; } /** @@ -138,7 +138,14 @@ export const OverflowMenuOption: React.FC< > {startIcon} From facd05bd892d7ac33dbf300bbc29b00138d0490e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 14:15:42 +0530 Subject: [PATCH 14/21] Swap --- .../src/components/Collections/CollectionHeader.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/web/apps/photos/src/components/Collections/CollectionHeader.tsx b/web/apps/photos/src/components/Collections/CollectionHeader.tsx index 38e6805c5c..25d0bd0261 100644 --- a/web/apps/photos/src/components/Collections/CollectionHeader.tsx +++ b/web/apps/photos/src/components/Collections/CollectionHeader.tsx @@ -27,7 +27,6 @@ import { } from "@/new/photos/services/magic-metadata"; import { useAppContext } from "@/new/photos/types/context"; import { HorizontalFlex } from "@ente/shared/components/Container"; -import { StyledMenu } from "@ente/shared/components/OverflowMenu"; import ArchiveOutlined from "@mui/icons-material/ArchiveOutlined"; import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined"; import EditIcon from "@mui/icons-material/Edit"; @@ -44,7 +43,7 @@ import TvIcon from "@mui/icons-material/Tv"; import Unarchive from "@mui/icons-material/Unarchive"; import VisibilityOffOutlined from "@mui/icons-material/VisibilityOffOutlined"; import VisibilityOutlined from "@mui/icons-material/VisibilityOutlined"; -import { Box, IconButton, Stack, Tooltip } from "@mui/material"; +import { Box, IconButton, Menu, Stack, Tooltip } from "@mui/material"; import { SetCollectionNamerAttributes } from "components/Collections/CollectionNamer"; import { t } from "i18next"; import { GalleryContext } from "pages/gallery"; @@ -721,8 +720,8 @@ const CollectionSortOrderMenu: React.FC = ({ }; return ( - = ({ {t("oldest_first")} - + ); }; From 613f7294e19243f1a318352104756bc34f88db79 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 14:20:19 +0530 Subject: [PATCH 15/21] Swap --- web/apps/photos/src/components/Export.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/apps/photos/src/components/Export.tsx b/web/apps/photos/src/components/Export.tsx index 8e3ea7d7c6..87cb3f2cca 100644 --- a/web/apps/photos/src/components/Export.tsx +++ b/web/apps/photos/src/components/Export.tsx @@ -1,5 +1,9 @@ import { isDesktop } from "@/base/app"; import { EnteSwitch } from "@/base/components/EnteSwitch"; +import { + OverflowMenu, + OverflowMenuOption, +} from "@/base/components/OverflowMenu"; import type { ButtonishProps } from "@/base/components/mui"; import type { ModalVisibilityProps } from "@/base/components/utils/modal"; import { ensureElectron } from "@/base/electron"; @@ -12,10 +16,6 @@ import { VerticallyCenteredFlex, } from "@ente/shared/components/Container"; import LinkButton from "@ente/shared/components/LinkButton"; -import { - OverflowMenu, - OverflowMenuOption, -} from "@ente/shared/components/OverflowMenu"; import { CustomError } from "@ente/shared/error"; import FolderIcon from "@mui/icons-material/Folder"; import { From a6c9a153e7b6add6463075ca56d3b0fd1f2921e8 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 14:24:20 +0530 Subject: [PATCH 16/21] Fix watch folder opening --- web/apps/photos/src/pages/_app.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index a0aacfc5d6..8f64cf82ac 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -189,6 +189,9 @@ export default function App({ Component, pageProps }: AppProps) { [ showLoadingBar, hideLoadingBar, + watchFolderView, + watchFolderFiles, + themeColor, showMiniDialog, onGenericError, logout, From 5c92d093ca3b3e906a7db8f403883a7bbb5c1818 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 14:54:21 +0530 Subject: [PATCH 17/21] Swap --- web/apps/photos/src/components/WatchFolder.tsx | 8 ++++---- web/apps/photos/src/pages/shared-albums.tsx | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/web/apps/photos/src/components/WatchFolder.tsx b/web/apps/photos/src/components/WatchFolder.tsx index c80d336412..22c93fb3b4 100644 --- a/web/apps/photos/src/components/WatchFolder.tsx +++ b/web/apps/photos/src/components/WatchFolder.tsx @@ -1,3 +1,7 @@ +import { + OverflowMenu, + OverflowMenuOption, +} from "@/base/components/OverflowMenu"; import { EllipsizedTypography } from "@/base/components/Typography"; import { useModalVisibility, @@ -15,10 +19,6 @@ import { SpaceBetweenFlex, VerticallyCentered, } from "@ente/shared/components/Container"; -import { - OverflowMenu, - OverflowMenuOption, -} from "@ente/shared/components/OverflowMenu"; import CheckIcon from "@mui/icons-material/Check"; import DoNotDisturbOutlinedIcon from "@mui/icons-material/DoNotDisturbOutlined"; import FolderCopyOutlinedIcon from "@mui/icons-material/FolderCopyOutlined"; diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 86408b3345..04c0f25495 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -3,6 +3,10 @@ import { FormPaper, FormPaperTitle } from "@/base/components/FormPaper"; import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; import { SpaceBetweenFlex } from "@/base/components/mui/Container"; import { NavbarBase, SelectionBar } from "@/base/components/Navbar"; +import { + OverflowMenu, + OverflowMenuOption, +} from "@/base/components/OverflowMenu"; import { useIsSmallWidth, useIsTouchscreen, @@ -30,10 +34,6 @@ import { FluidContainer, VerticallyCentered, } from "@ente/shared/components/Container"; -import { - OverflowMenu, - OverflowMenuOption, -} from "@ente/shared/components/OverflowMenu"; import SingleInputForm, { type SingleInputFormProps, } from "@ente/shared/components/SingleInputForm"; @@ -703,7 +703,7 @@ const ListHeader: React.FC = ({ fileCount={publicFiles.length} /> {downloadEnabled && ( - + } onClick={downloadAllFiles} From 0181693736b5dfa1108ebe6691eb0b88a292ee1f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 14:57:29 +0530 Subject: [PATCH 18/21] Swap --- .../components/gallery/PeopleHeader.tsx | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 73a2e680f6..91bd6ff432 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -7,6 +7,10 @@ import { } from "@/base/components/mui/Container"; import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; import { LoadingButton } from "@/base/components/mui/LoadingButton"; +import { + OverflowMenu, + OverflowMenuOption, +} from "@/base/components/OverflowMenu"; import { useIsSmallWidth } from "@/base/components/utils/hooks"; import { useModalVisibility, @@ -30,10 +34,6 @@ import { type PersonSuggestionUpdates, type PreviewableCluster, } from "@/new/photos/services/ml/people"; -import { - OverflowMenu, - OverflowMenuOption, -} from "@ente/shared/components/OverflowMenu"; import AddIcon from "@mui/icons-material/Add"; import CheckIcon from "@mui/icons-material/Check"; import ClearIcon from "@mui/icons-material/Clear"; @@ -145,24 +145,21 @@ const CGroupPersonHeader: React.FC = ({ person }) => { name={name} fileCount={person.fileIDs.length} /> - + } - centerAlign onClick={showSuggestions} > {t("review_suggestions")} } - centerAlign onClick={showNameInput} > {t("rename")} } - centerAlign onClick={handleReset} > {t("reset")} @@ -206,10 +203,9 @@ const IgnoredPersonHeader: React.FC = ({ nameProps={{ color: "text.muted" }} fileCount={person.fileIDs.length} /> - + } - centerAlign onClick={handleUndoIgnore} > {t("show_person")} @@ -264,17 +260,15 @@ const ClusterPersonHeader: React.FC = ({ - + } - centerAlign onClick={showAddPerson} > {t("add_a_name")} } - centerAlign onClick={confirmIgnore} > {t("ignore")} From 605fda2710ba6399f622be62b5f9a69c1762ef72 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 14:59:45 +0530 Subject: [PATCH 19/21] Swap --- .../new/photos/components/CollectionsSortOptions.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/packages/new/photos/components/CollectionsSortOptions.tsx b/web/packages/new/photos/components/CollectionsSortOptions.tsx index d33bce1ed1..b2965edf9e 100644 --- a/web/packages/new/photos/components/CollectionsSortOptions.tsx +++ b/web/packages/new/photos/components/CollectionsSortOptions.tsx @@ -1,8 +1,8 @@ -import type { CollectionsSortBy } from "@/new/photos/services/collection/ui"; import { OverflowMenu, OverflowMenuOption, -} from "@ente/shared/components/OverflowMenu"; +} from "@/base/components/OverflowMenu"; +import type { CollectionsSortBy } from "@/new/photos/services/collection/ui"; import TickIcon from "@mui/icons-material/Done"; import SortIcon from "@mui/icons-material/Sort"; import SvgIcon from "@mui/material/SvgIcon"; @@ -45,6 +45,10 @@ export const CollectionsSortOptions: React.FC = ({ triggerButtonIcon={} menuPaperProps={{ sx: { + // The trigger button has a colored background, so add some + // vertical margin to avoid showing the menu squat under the + // trigger button. + marginBlock: 1, backgroundColor: (theme) => nestedInDialog ? theme.colors.background.elevated2 From d4ae5c118b3101359300a3c264c99ef62e609b20 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 15:00:35 +0530 Subject: [PATCH 20/21] Swap and fin --- web/apps/auth/src/pages/auth.tsx | 10 +- .../shared/components/OverflowMenu.tsx | 180 ------------------ 2 files changed, 5 insertions(+), 185 deletions(-) delete mode 100644 web/packages/shared/components/OverflowMenu.tsx diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx index 9c0502d46a..0040dd0b3b 100644 --- a/web/apps/auth/src/pages/auth.tsx +++ b/web/apps/auth/src/pages/auth.tsx @@ -3,6 +3,10 @@ import { stashRedirect } from "@/accounts/services/redirect"; import { EnteLogo } from "@/base/components/EnteLogo"; import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; import { NavbarBase } from "@/base/components/Navbar"; +import { + OverflowMenu, + OverflowMenuOption, +} from "@/base/components/OverflowMenu"; import { isHTTP401Error } from "@/base/http"; import log from "@/base/log"; import { masterKeyFromSessionIfLoggedIn } from "@/base/session-store"; @@ -10,10 +14,6 @@ import { HorizontalFlex, VerticallyCentered, } from "@ente/shared/components/Container"; -import { - OverflowMenu, - OverflowMenuOption, -} from "@ente/shared/components/OverflowMenu"; import { AUTH_PAGES as PAGES } from "@ente/shared/constants/pages"; import LogoutOutlined from "@mui/icons-material/LogoutOutlined"; import { @@ -154,7 +154,7 @@ const AuthNavbar: React.FC = () => { - + } diff --git a/web/packages/shared/components/OverflowMenu.tsx b/web/packages/shared/components/OverflowMenu.tsx deleted file mode 100644 index 2469094358..0000000000 --- a/web/packages/shared/components/OverflowMenu.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { FluidContainer } from "@ente/shared/components/Container"; -import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; -import { - Box, - IconButton, - MenuItem, - styled, - Typography, - type ButtonProps, - type IconButtonProps, - type PaperProps, -} from "@mui/material"; -import Menu, { type MenuProps } from "@mui/material/Menu"; -import React, { createContext, useContext, useMemo, useState } from "react"; - -const OverflowMenuContext = createContext({ - // eslint-disable-next-line @typescript-eslint/no-empty-function - close: () => {}, -}); - -interface OverflowMenuProps { - /** - * An ARIA identifier for the overflow menu when it is displayed. - */ - ariaID: string; - /** - * The icon for the trigger button. - * - * If not provided, then by default the MoreHoriz icon from MUI is used. - */ - triggerButtonIcon?: React.ReactNode; - /** - * Optional additional properties for the trigger icon button. - */ - triggerButtonProps?: Partial; - /** - * Optional additional properties for the MUI {@link Paper} that underlies - * the {@link Menu}. - */ - menuPaperProps?: Partial; -} - -/** - * A custom MUI {@link Menu} with some Ente specific styling applied to it. - */ -export const StyledMenu = styled(Menu)` - & .MuiPaper-root { - margin: 16px auto; - box-shadow: - 0px 0px 6px rgba(0, 0, 0, 0.16), - 0px 3px 6px rgba(0, 0, 0, 0.12); - } - & .MuiList-root { - padding: 0; - border: none; - } -`; - -/** - * An overflow menu showing {@link OverflowMenuOptions}, alongwith a button to - * trigger the visibility of the menu. - */ -export const OverflowMenu: React.FC< - React.PropsWithChildren -> = ({ - ariaID, - triggerButtonIcon, - triggerButtonProps, - menuPaperProps, - children, -}) => { - const [anchorEl, setAnchorEl] = useState(); - const context = useMemo( - () => ({ close: () => setAnchorEl(undefined) }), - [], - ); - return ( - - setAnchorEl(event.currentTarget)} - aria-controls={anchorEl ? ariaID : undefined} - aria-haspopup="true" - aria-expanded={anchorEl ? "true" : undefined} - {...triggerButtonProps} - > - {triggerButtonIcon ?? } - - setAnchorEl(undefined)} - MenuListProps={{ - disablePadding: true, - "aria-labelledby": ariaID, - }} - slotProps={{ - paper: menuPaperProps, - }} - anchorOrigin={{ - vertical: "bottom", - horizontal: "right", - }} - transformOrigin={{ - vertical: "top", - horizontal: "right", - }} - > - {children} - - - ); -}; - -interface OverflowMenuOptionProps { - onClick: () => void; - color?: ButtonProps["color"]; - startIcon?: React.ReactNode; - endIcon?: React.ReactNode; - children?: any; - // To avoid changing old places without an audit, new code should use this - // option explicitly to fix/tweak the alignment of the button label and - // icon. Once all existing uses have migrated, can change the default. - centerAlign?: boolean; -} - -export const OverflowMenuOption: React.FC = ({ - onClick, - color = "primary", - startIcon, - endIcon, - centerAlign, - children, -}) => { - const menuContext = useContext(OverflowMenuContext); - - const handleClick = () => { - onClick(); - menuContext.close(); - }; - - return ( - theme.palette[color].main, - padding: 1.5, - "& .MuiSvgIcon-root": { - fontSize: "20px", - }, - }} - > - - {startIcon && ( - - {startIcon} - - )} - {children} - - {endIcon && ( - - {endIcon} - - )} - - ); -}; From 9a5d9774196fc477e4ca1e187617affc68514fa9 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 20 Dec 2024 15:01:06 +0530 Subject: [PATCH 21/21] lf --- web/apps/photos/src/pages/gallery.tsx | 2 +- web/apps/photos/src/services/export/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 2aa7d6308e..c8efb46bd3 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -32,6 +32,7 @@ import { shouldShowWhatsNew } from "@/new/photos/services/changelog"; import { ALL_SECTION, DUMMY_UNCATEGORIZED_COLLECTION, + getAllLocalCollections, isHiddenCollection, } from "@/new/photos/services/collection"; import { areOnlySystemCollections } from "@/new/photos/services/collection/ui"; @@ -114,7 +115,6 @@ import { createUnCategorizedCollection, getAllLatestCollections, } from "services/collectionService"; -import { getAllLocalCollections } from "@/new/photos/services/collection"; import { syncFiles } from "services/fileService"; import { preFileInfoSync, sync } from "services/sync"; import { syncTrash } from "services/trashService"; diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index 83526c7eea..e014b2cb29 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -14,6 +14,7 @@ import { FileType } from "@/media/file-type"; import { decodeLivePhoto } from "@/media/live-photo"; import { createCollectionNameByID, + getAllLocalCollections, getCollectionUserFacingName, } from "@/new/photos/services/collection"; import { @@ -26,7 +27,6 @@ import { PromiseQueue } from "@/utils/promise"; import { CustomError } from "@ente/shared/error"; import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage"; import i18n from "i18next"; -import { getAllLocalCollections } from "@/new/photos/services/collection"; import { migrateExport, type ExportRecord } from "./migration"; /** Name of the JSON file in which we keep the state of the export. */