From 8e08a0d71d2b32fe56fdd2e6acd80ad7a09f0a8f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 05:46:02 +0530 Subject: [PATCH 01/40] Temporary workbench Revert "Prep for merge" This reverts commit e75165d01e7e547e3219288a8a8c5f0dd44a9001. --- web/apps/photos/src/pages/_app.tsx | 4 ++-- web/apps/photos/src/styles/global.css | 3 ++- web/packages/gallery/components/viewer/photoswipe.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index 000cf2d037..aad64be0cf 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -48,9 +48,9 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { resumeExportsIfNeeded } from "services/export"; import { photosLogout } from "services/logout"; -import "photoswipe/dist/photoswipe.css"; +// import "photoswipe/dist/photoswipe.css"; // TODO(PS): Note, auto hide only works with the new CSS. -// import "../../../../packages/gallery/components/viewer/ps5/dist/photoswipe.css"; +import "../../../../packages/gallery/components/viewer/ps5/dist/photoswipe.css"; import "styles/global.css"; diff --git a/web/apps/photos/src/styles/global.css b/web/apps/photos/src/styles/global.css index aced5adc91..3eb8dd8d62 100644 --- a/web/apps/photos/src/styles/global.css +++ b/web/apps/photos/src/styles/global.css @@ -12,7 +12,7 @@ body { flex-direction: column; height: 100%; } - +@media DISABLED { .pswp__button--custom { width: 48px; height: 48px; @@ -114,6 +114,7 @@ body { .pswp__caption--empty { display: none; } +} .pswp-ente { /* The default z-index for PhotoSwipe is 10k, way beyond everything else. diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index 6452938321..adc8fd606a 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -32,7 +32,7 @@ if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { let PhotoSwipe; if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { // TODO(PS): Comment me before merging into main. - // PhotoSwipe = require("./ps5/dist/photoswipe.esm.js").default; + PhotoSwipe = require("./ps5/dist/photoswipe.esm.js").default; } /** From b11636bfdfcbba30abeb5d77a2b66aa57239ee23 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 05:59:01 +0530 Subject: [PATCH 02/40] zoom left --- web/packages/gallery/components/viewer/photoswipe.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index adc8fd606a..dee195aba8 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -423,6 +423,17 @@ export class FileViewerPhotoSwipe { // - zoom: 10 // - close: 20 pswp.on("uiRegister", () => { + // Move the zoom button to the left so that it is in the same place + // as the other items like preloader or the error indicator that + // come and go as files get loaded. + // + // We cannot use the PhotoSwipe "uiElement" filter to modify the + // order since that only allows us to edit the DOM element, not the + // underlying UI element data. + pswp.ui.uiElementsData.find((e) => e.name == "zoom").order = 6; + + // Register our custom elements... + pswp.ui.registerElement({ name: "error", order: 6, From 2fbc26c9eb5db3814c3b3f4f54ac07b4e32970f1 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 06:17:20 +0530 Subject: [PATCH 03/40] scale --- web/apps/photos/src/styles/global.css | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/web/apps/photos/src/styles/global.css b/web/apps/photos/src/styles/global.css index 3eb8dd8d62..1b45cfe9ed 100644 --- a/web/apps/photos/src/styles/global.css +++ b/web/apps/photos/src/styles/global.css @@ -149,10 +149,24 @@ body { height: 60px; /* Unlike the loading indicator, "display" is used to toggle visibility, and the opacity is fixed to be similar to that of the counter. */ - opacity: 0.85; display: none; + opacity: 0.85; } .pswp-ente .pswp__error--active { display: initial; } + +/* Scale the built in controls to better fit our requirements */ +.pswp-ente .pswp__button--zoom .pswp__icn { + transform: scale(0.85); +} + +.pswp-ente .pswp__button--arrow--prev .pswp__icn { + transform: scale(0.8); +} + +.pswp-ente .pswp__button--arrow--next .pswp__icn { + /* default is a horizontal flip, transform: scale(-1, 1); */ + transform: scale(-0.8, 0.8); +} From 561b0ea71e4f1e05e0ca4c62b5a9b135e41651c5 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 06:32:46 +0530 Subject: [PATCH 04/40] more --- web/packages/gallery/components/viewer/icons.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/packages/gallery/components/viewer/icons.tsx b/web/packages/gallery/components/viewer/icons.tsx index 99628399f4..6481d0814c 100644 --- a/web/packages/gallery/components/viewer/icons.tsx +++ b/web/packages/gallery/components/viewer/icons.tsx @@ -29,6 +29,8 @@ const paths = { // "@mui/icons-material/FavoriteRounded" unfavorite: ' Date: Fri, 28 Feb 2025 08:08:56 +0530 Subject: [PATCH 05/40] more 2 --- web/packages/gallery/components/viewer/icons.tsx | 5 ++++- .../gallery/components/viewer/photoswipe.ts | 14 +++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/web/packages/gallery/components/viewer/icons.tsx b/web/packages/gallery/components/viewer/icons.tsx index 6481d0814c..3f98051fff 100644 --- a/web/packages/gallery/components/viewer/icons.tsx +++ b/web/packages/gallery/components/viewer/icons.tsx @@ -29,8 +29,11 @@ const paths = { // "@mui/icons-material/FavoriteRounded" unfavorite: ' Date: Fri, 28 Feb 2025 08:26:10 +0530 Subject: [PATCH 06/40] fav 1 --- web/apps/photos/src/components/PhotoFrame.tsx | 1 + .../gallery/components/viewer/FileViewer.tsx | 60 ++++++++++++++++--- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index 8270ce9b20..87367976a8 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -557,6 +557,7 @@ const PhotoFrame = ({ onSaveEditedImageCopy={handleSaveEditedImageCopy} {...{ favoriteFileIDs, + markUnsyncedFavoriteUpdate, fileCollectionIDs, allCollectionsNameByID, onSelectCollection, diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index 5ecb18f79c..eb01c66ba7 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -14,6 +14,7 @@ if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { } import { isDesktop } from "@/base/app"; +import { assertionFailed } from "@/base/assert"; import { type ModalVisibilityProps } from "@/base/components/utils/modal"; import { lowercaseExtension } from "@/base/file-name"; import type { LocalUser } from "@/base/local-user"; @@ -31,6 +32,7 @@ import { ImageEditorOverlay, type ImageEditorOverlayProps, } from "@/new/photos/components/ImageEditorOverlay"; +import { wait } from "@/utils/promise"; import { Button, styled } from "@mui/material"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { fileInfoExifForFile } from "./data-source"; @@ -39,6 +41,18 @@ import { type FileViewerAnnotatedFile, } from "./photoswipe"; +// TODO +// import { +// addToFavorites, +// removeFromFavorites, +// } from "apps/photos/services/collectionService"; +const addToFavorites = async (file: EnteFile) => { + console.log(file); + await wait(3000); + throw new Error("test"); +}; +const removeFromFavorites = addToFavorites; + export type FileViewerProps = ModalVisibilityProps & { /** * The currently logged in user, if any. @@ -106,6 +120,20 @@ export type FileViewerProps = ModalVisibilityProps & { * not defined, then this prop is not used. */ onTriggerSyncWithRemote?: () => void; + /** + * Called when the viewer wants to update the in-memory, unsynced, favorite + * status of a file. + * + * For more details, see {@link unsyncedFavoriteUpdates} in the gallery + * reducer's documentation. + * + * If this is not provided then the favorite toggle button will not be shown + * in the file actions. + */ + onMarkUnsyncedFavoriteUpdate?: ( + fileID: number, + isFavorite: boolean, + ) => void; /** * Called when the user edits an image in the image editor and asks us to * save their edits as a copy. @@ -142,6 +170,7 @@ const FileViewer: React.FC = ({ onSelectCollection, onSelectPerson, onTriggerSyncWithRemote, + onMarkUnsyncedFavoriteUpdate, onSaveEditedImageCopy, }) => { const pswpRef = useRef(); @@ -183,13 +212,14 @@ const FileViewer: React.FC = ({ log.debug(() => ["viewer", { action: "annotate", file }]); const fileID = file.id; const isOwnFile = file.ownerID == user?.id; - const canFavoriteOrEdit = + const canModify = isOwnFile && !isInTrashSection && !isInHiddenSection; - const isFavorite = canFavoriteOrEdit - ? favoriteFileIDs?.has(file.id) - : undefined; + const isFavorite = + favoriteFileIDs && onMarkUnsyncedFavoriteUpdate && canModify + ? favoriteFileIDs.has(file.id) + : undefined; const isEditableImage = - onSaveEditedImageCopy && canFavoriteOrEdit + onSaveEditedImageCopy && canModify ? fileIsEditableImage(file) : undefined; return { fileID, isOwnFile, isFavorite, isEditableImage }; @@ -199,18 +229,30 @@ const FileViewer: React.FC = ({ isInTrashSection, isInHiddenSection, favoriteFileIDs, + onMarkUnsyncedFavoriteUpdate, onSaveEditedImageCopy, ], ); const handleToggleFavorite = useMemo(() => { return favoriteFileIDs - ? (annotatedFile: FileViewerAnnotatedFile) => { - setActiveAnnotatedFile(annotatedFile); - console.log("handleToggleFavorite", annotatedFile); + ? ({ file, annotation }: FileViewerAnnotatedFile) => { + const isFavorite = annotation.isFavorite; + if (isFavorite === undefined) { + assertionFailed(); + return; + } + + onMarkUnsyncedFavoriteUpdate(file.id, !isFavorite); + void (isFavorite ? removeFromFavorites : addToFavorites)( + file, + ).catch((e: unknown) => { + log.error("Failed to remove favorite", e); + onMarkUnsyncedFavoriteUpdate(file.id, undefined); + }); } : undefined; - }, [favoriteFileIDs]); + }, [favoriteFileIDs, onMarkUnsyncedFavoriteUpdate]); const handleViewInfo = useCallback( (annotatedFile: FileViewerAnnotatedFile) => { From 67206b013b85008f329aee5b1cbb1b5042cd2f35 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 08:31:24 +0530 Subject: [PATCH 07/40] stable ident --- web/apps/photos/src/components/PhotoFrame.tsx | 2 +- web/apps/photos/src/pages/gallery.tsx | 32 +++++++++++-------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index 87367976a8..ecb84371bf 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -554,10 +554,10 @@ const PhotoFrame = ({ isInHiddenSection={isInHiddenSection} disableDownload={!enableDownload} onTriggerSyncWithRemote={handleTriggerSyncWithRemote} + onMarkUnsyncedFavoriteUpdate={markUnsyncedFavoriteUpdate} onSaveEditedImageCopy={handleSaveEditedImageCopy} {...{ favoriteFileIDs, - markUnsyncedFavoriteUpdate, fileCollectionIDs, allCollectionsNameByID, onSelectCollection, diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 224e6e644c..d4a08eed7f 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -793,6 +793,20 @@ const Page: React.FC = () => { }); }; + const handleSelectCollection = useCallback( + (collectionID: number) => + dispatch({ + type: "showNormalOrHiddenCollectionSummary", + collectionSummaryID: collectionID, + }), + [], + ); + + const handleSelectPerson = useCallback( + (personID: string) => dispatch({ type: "showPerson", personID }), + [], + ); + const handleOpenCollectionSelector = useCallback( (attributes: CollectionSelectorAttributes) => { setCollectionSelectorAttributes(attributes); @@ -934,9 +948,7 @@ const Page: React.FC = () => { onSelectPeople={() => dispatch({ type: "showPeople" }) } - onSelectPerson={(personID) => - dispatch({ type: "showPerson", personID }) - } + onSelectPerson={handleSelectPerson} /> )} @@ -959,8 +971,7 @@ const Page: React.FC = () => { ? state.view.visiblePeople : undefined) ?? [], activePerson, - onSelectPerson: (personID) => - dispatch({ type: "showPerson", personID }), + onSelectPerson: handleSelectPerson, setCollectionNamerAttributes, setPhotoListHeader, setFilesDownloadProgressAttributesCreator, @@ -1049,15 +1060,8 @@ const Page: React.FC = () => { setFilesDownloadProgressAttributesCreator } selectable={true} - onSelectCollection={(collectionID) => - dispatch({ - type: "showNormalOrHiddenCollectionSummary", - collectionSummaryID: collectionID, - }) - } - onSelectPerson={(personID) => { - dispatch({ type: "showPerson", personID }); - }} + onSelectCollection={handleSelectCollection} + onSelectPerson={handleSelectPerson} /> )} Date: Fri, 28 Feb 2025 08:45:40 +0530 Subject: [PATCH 08/40] st 2 --- web/apps/photos/src/components/PhotoFrame.tsx | 18 +++++++---- .../src/components/PhotoViewer/index.tsx | 18 +++++------ web/apps/photos/src/pages/gallery.tsx | 32 ++++++++++++------- .../gallery/components/viewer/FileViewer.tsx | 22 +++++++++---- 4 files changed, 57 insertions(+), 33 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index ecb84371bf..b68f603744 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -121,7 +121,10 @@ export type PhotoFrameProps = Pick< * * Not set in the context of the shared albums app. */ - markUnsyncedFavoriteUpdate?: (fileID: number, isFavorite: boolean) => void; + onMarkUnsyncedFavoriteUpdate?: ( + fileID: number, + isFavorite: boolean, + ) => void; /** * Called when the component wants to mark the given files as deleted in the * the in-memory, unsynced, state maintained by the top level gallery. @@ -131,7 +134,7 @@ export type PhotoFrameProps = Pick< * * Not set in the context of the shared albums app. */ - markTempDeleted?: (files: EnteFile[]) => void; + onMarkTempDeleted?: (files: EnteFile[]) => void; /** This will be set if mode is not "people". */ activeCollectionID: number; /** This will be set if mode is "people". */ @@ -155,8 +158,8 @@ const PhotoFrame = ({ setSelected, selected, favoriteFileIDs, - markUnsyncedFavoriteUpdate, - markTempDeleted, + onMarkUnsyncedFavoriteUpdate, + onMarkTempDeleted, activeCollectionID, activePersonID, enableDownload, @@ -554,12 +557,13 @@ const PhotoFrame = ({ isInHiddenSection={isInHiddenSection} disableDownload={!enableDownload} onTriggerSyncWithRemote={handleTriggerSyncWithRemote} - onMarkUnsyncedFavoriteUpdate={markUnsyncedFavoriteUpdate} onSaveEditedImageCopy={handleSaveEditedImageCopy} {...{ favoriteFileIDs, fileCollectionIDs, allCollectionsNameByID, + onMarkUnsyncedFavoriteUpdate, + onMarkTempDeleted, onSelectCollection, onSelectPerson, }} @@ -592,8 +596,8 @@ const PhotoFrame = ({ enableDownload={enableDownload} {...{ favoriteFileIDs, - markUnsyncedFavoriteUpdate, - markTempDeleted, + onMarkUnsyncedFavoriteUpdate, + onMarkTempDeleted, setFilesDownloadProgressAttributesCreator, fileCollectionIDs, allCollectionsNameByID, diff --git a/web/apps/photos/src/components/PhotoViewer/index.tsx b/web/apps/photos/src/components/PhotoViewer/index.tsx index 3454d11ae7..2868a0f5d4 100644 --- a/web/apps/photos/src/components/PhotoViewer/index.tsx +++ b/web/apps/photos/src/components/PhotoViewer/index.tsx @@ -76,8 +76,8 @@ import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; export type PhotoViewerProps = Pick< PhotoFrameProps, | "favoriteFileIDs" - | "markUnsyncedFavoriteUpdate" - | "markTempDeleted" + | "onMarkUnsyncedFavoriteUpdate" + | "onMarkTempDeleted" | "fileCollectionIDs" | "allCollectionsNameByID" | "onSelectCollection" @@ -132,8 +132,8 @@ export const PhotoViewer: React.FC = ({ gettingData, forceConvertItem, favoriteFileIDs, - markUnsyncedFavoriteUpdate, - markTempDeleted, + onMarkUnsyncedFavoriteUpdate, + onMarkTempDeleted, isTrashCollection, isInHiddenSection, enableDownload, @@ -511,16 +511,16 @@ export const PhotoViewer: React.FC = ({ const isFavorite = favoriteFileIDs!.has(file.id); if (!isFavorite) { - markUnsyncedFavoriteUpdate(file.id, true); + onMarkUnsyncedFavoriteUpdate(file.id, true); void addToFavorites(file).catch((e: unknown) => { log.error("Failed to add favorite", e); - markUnsyncedFavoriteUpdate(file.id, undefined); + onMarkUnsyncedFavoriteUpdate(file.id, undefined); }); } else { - markUnsyncedFavoriteUpdate(file.id, false); + onMarkUnsyncedFavoriteUpdate(file.id, false); void removeFromFavorites(file).catch((e: unknown) => { log.error("Failed to remove favorite", e); - markUnsyncedFavoriteUpdate(file.id, undefined); + onMarkUnsyncedFavoriteUpdate(file.id, undefined); }); } @@ -538,7 +538,7 @@ export const PhotoViewer: React.FC = ({ const handleDeleteFile = async () => { const file = fileToDelete!; await moveToTrash([file]); - markTempDeleted?.([file]); + onMarkTempDeleted?.([file]); updateItems(items.filter((item) => item.id !== file.id)); setFileToDelete(undefined); needUpdate.current = true; diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index d4a08eed7f..5b88e5e9e3 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -695,7 +695,7 @@ const Page: React.FC = () => { await handleFileOps( ops, toProcessFiles, - (files) => dispatch({ type: "markTempDeleted", files }), + handleMarkTempDeleted, () => dispatch({ type: "clearTempDeleted" }), (files) => dispatch({ type: "markTempHidden", files }), () => dispatch({ type: "clearTempHidden" }), @@ -793,6 +793,21 @@ const Page: React.FC = () => { }); }; + const handleMarkUnsyncedFavoriteUpdate = useCallback( + (fileID: number, isFavorite: boolean) => + dispatch({ + type: "markUnsyncedFavoriteUpdate", + fileID, + isFavorite, + }), + [], + ); + + const handleMarkTempDeleted = useCallback( + (files: EnteFile[]) => dispatch({ type: "markTempDeleted", files }), + [], + ); + const handleSelectCollection = useCallback( (collectionID: number) => dispatch({ @@ -1032,20 +1047,11 @@ const Page: React.FC = () => { mode={barMode} modePlus={isInSearchMode ? "search" : barMode} files={filteredFiles} + // TODO: Warning: Doesn't have stable identity. syncWithRemote={syncWithRemote} setSelected={setSelected} selected={selected} favoriteFileIDs={state.favoriteFileIDs} - markUnsyncedFavoriteUpdate={(fileID, isFavorite) => - dispatch({ - type: "markUnsyncedFavoriteUpdate", - fileID, - isFavorite, - }) - } - markTempDeleted={(files) => - dispatch({ type: "markTempDeleted", files }) - } setIsPhotoSwipeOpen={setIsPhotoSwipeOpen} activeCollectionID={activeCollectionID} activePersonID={activePerson?.id} @@ -1060,6 +1066,10 @@ const Page: React.FC = () => { setFilesDownloadProgressAttributesCreator } selectable={true} + onMarkUnsyncedFavoriteUpdate={ + handleMarkUnsyncedFavoriteUpdate + } + onMarkTempDeleted={handleMarkTempDeleted} onSelectCollection={handleSelectCollection} onSelectPerson={handleSelectPerson} /> diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index eb01c66ba7..fdb262bac1 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -122,18 +122,26 @@ export type FileViewerProps = ModalVisibilityProps & { onTriggerSyncWithRemote?: () => void; /** * Called when the viewer wants to update the in-memory, unsynced, favorite - * status of a file. + * status of a file maintained by the top level gallery. For more details, + * see {@link unsyncedFavoriteUpdates} in the gallery reducer's + * documentation. * - * For more details, see {@link unsyncedFavoriteUpdates} in the gallery - * reducer's documentation. - * - * If this is not provided then the favorite toggle button will not be shown - * in the file actions. + * If this is not provided then the toggle favorite action will not be + * shown. */ onMarkUnsyncedFavoriteUpdate?: ( fileID: number, isFavorite: boolean, ) => void; + /** + * Called when the viewer wants to mark the given files as deleted in the + * the in-memory, unsynced, state maintained by the top level gallery. For + * more details, see {@link unsyncedFavoriteUpdates} in the gallery + * reducer's documentation. + * + * If this is not provided then the delete action will not be shown. + */ + onMarkTempDeleted?: (files: EnteFile[]) => void; /** * Called when the user edits an image in the image editor and asks us to * save their edits as a copy. @@ -171,6 +179,8 @@ const FileViewer: React.FC = ({ onSelectPerson, onTriggerSyncWithRemote, onMarkUnsyncedFavoriteUpdate, + // TODO + // onMarkTempDeleted, onSaveEditedImageCopy, }) => { const pswpRef = useRef(); From 15a4e3cd982230e5f8596fa852254db5f94b8ca0 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 09:20:19 +0530 Subject: [PATCH 09/40] wip delegate --- .../gallery/components/viewer/FileViewer.tsx | 75 ++++++++++++------- .../gallery/components/viewer/photoswipe.ts | 55 +++++++++----- 2 files changed, 85 insertions(+), 45 deletions(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index fdb262bac1..da73c2b1b5 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -183,7 +183,8 @@ const FileViewer: React.FC = ({ // onMarkTempDeleted, onSaveEditedImageCopy, }) => { - const pswpRef = useRef(); + const psRef = useRef(); + const psDelegateRef = useRef(); // Whenever we get a callback from our custom PhotoSwipe instance, we also // get the active file on which that action was performed as an argument. @@ -321,34 +322,56 @@ const FileViewer: React.FC = ({ [onSaveEditedImageCopy, handleImageEditorClose, handleClose], ); - useEffect(() => { - log.debug(() => ["viewer", { action: "useEffect", open }]); + useEffect(() => ( + psDelegateRef.current.onClose = handleClose; + onAnnotate: handleAnnotate, + onToggleFavorite: handleToggleFavorite, + onViewInfo: handleViewInfo, + onEditImage: handleEditImage, + }), [ + handleClose, + onAnnotate: handleAnnotate, + onToggleFavorite: handleToggleFavorite, + onViewInfo: handleViewInfo, + onEditImage: handleEditImage, + + ]); + useEffect(() => { + if (open && !psRef.current) { + // We're open, but we don't have a PhotoSwipe instance. Create + // one. This will show the file viewer dialog. + // + // Before creating it, also create a delegate. The delegate has + // a stable identity so that PhotoSwipe callbacks can be routed + // via it. When any of the + // callbacks change, we update the props of the delegate instead + // of changing the delegate itself. + log.debug(() => ["viewer", "open"]); + const delegate = { + onClose: handleClose, + onAnnotate: handleAnnotate, + onToggleFavorite: handleToggleFavorite, + onViewInfo: handleViewInfo, + onEditImage: handleEditImage, + } + const pswp = new FileViewerPhotoSwipe({ + files, + initialIndex, + disableDownload, + delegate, + }); + psRef.current = pswp; + psDelegateRef.current = delegate; + + } else if (!open && psRef.current) { + // We're closed, but we still have a PhotoSwipe instance. Cleanup. + log.debug(() => ["viewer", "close"]); + psRef.current?.closeIfNeeded(); + psRef.current = undefined; + psDelegateRef.current = undefined; - if (!open) { - // The close state will be handled by the cleanup function. - return; } - const pswp = new FileViewerPhotoSwipe({ - files, - initialIndex, - disableDownload, - onClose: handleClose, - onAnnotate: handleAnnotate, - onToggleFavorite: handleToggleFavorite, - onViewInfo: handleViewInfo, - onEditImage: handleEditImage, - }); - pswpRef.current = pswp; - - return () => { - log.debug(() => [ - "viewer", - { action: "useEffect/cleanup", pswpRef: pswpRef.current }, - ]); - pswpRef.current?.closeIfNeeded(); - pswpRef.current = undefined; - }; // The hook is missing dependencies; this is intentional - we don't want // to recreate the PhotoSwipe dialog when these dependencies change. // diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index 3bac4f85a2..f014d645fd 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -1,6 +1,7 @@ /* eslint-disable */ // @ts-nocheck +import { assertionFailed } from "@/base/assert"; import { pt } from "@/base/i18n"; import log from "@/base/log"; import type { EnteFile } from "@/media/file"; @@ -66,7 +67,7 @@ export interface FileViewerFileAnnotation { isEditableImage?: boolean | undefined; } -type FileViewerPhotoSwipeOptions = { +interface FileViewerPhotoSwipeDelegate { /** * Called when the file viewer is closed. */ @@ -99,7 +100,20 @@ type FileViewerPhotoSwipeOptions = { * {@link FileViewerFileAnnotation} for the file. */ onEditImage?: (annotatedFile: FileViewerAnnotatedFile) => void; -} & Pick; +} + +type FileViewerPhotoSwipeOptions = Pick< + FileViewerProps, + "files" | "initialIndex" | "disableDownload" +> & { + /** + * Callbacks. + * + * The extra level of indirection allows these to be updated without + * recreating us. + */ + delegate: FileViewerPhotoSwipeDelegate; +}; /** * A file and its annotation, in a nice cosy box. @@ -140,6 +154,10 @@ export class FileViewerPhotoSwipe { * The options with which we were initialized. */ private opts: Pick; + /** + * An object to which we should route various callbacks. + */ + private delegate: FileViewerPhotoSwipeDelegate; /** * An interval that invokes a periodic check of whether we should the hide * controls if the user does not perform any pointer events for a while. @@ -174,11 +192,7 @@ export class FileViewerPhotoSwipe { files, initialIndex, disableDownload, - onClose, - onAnnotate, - onToggleFavorite, - onViewInfo, - onEditImage, + delegate, }: FileViewerPhotoSwipeOptions) { this.files = files; this.opts = { disableDownload }; @@ -249,7 +263,7 @@ export class FileViewerPhotoSwipe { const file = currentFile(); let annotation = this.activeFileAnnotation; if (annotation?.fileID != file.id) { - annotation = onAnnotate(file); + annotation = delegate.onAnnotate(file); this.activeFileAnnotation = annotation; } return { @@ -266,8 +280,8 @@ export class FileViewerPhotoSwipe { const currentFileAnnotation = () => currentAnnotatedFile().annotation; const withCurrentAnnotatedFile = - (cb: (af: AnnotatedFile) => void) => () => - cb(currentAnnotatedFile()); + (cb: ((af: AnnotatedFile) => void) | undefined) => () => + cb ? cb(currentAnnotatedFile()) : assertionFailed(); // Provide data about slides to PhotoSwipe via callbacks // https://photoswipe.com/data-sources/#dynamically-generated-data @@ -404,7 +418,7 @@ export class FileViewerPhotoSwipe { forgetFailedItems(); forgetExif(); // Let our parent know that we have been closed. - onClose(); + delegate.onClose(); }); const showIf = (element: HTMLElement, condition: boolean) => @@ -451,7 +465,7 @@ export class FileViewerPhotoSwipe { }, }); - if (onToggleFavorite) { + if (delegate.onToggleFavorite) { // Only one of these two will end up being shown, so they can // safely share the same order. pswp.ui.registerElement({ @@ -460,7 +474,9 @@ export class FileViewerPhotoSwipe { order: 8, isButton: true, html: createPSRegisterElementIconHTML("favorite"), - onClick: withCurrentAnnotatedFile(onToggleFavorite), + onClick: withCurrentAnnotatedFile( + delegate.onToggleFavorite, + ), onInit: (buttonElement) => pswp.on("change", () => showIf( @@ -475,7 +491,9 @@ export class FileViewerPhotoSwipe { order: 8, isButton: true, html: createPSRegisterElementIconHTML("unfavorite"), - onClick: withCurrentAnnotatedFile(onToggleFavorite), + onClick: withCurrentAnnotatedFile( + delegate.onToggleFavorite, + ), onInit: (buttonElement) => pswp.on("change", () => showIf( @@ -492,11 +510,11 @@ export class FileViewerPhotoSwipe { order: 9, isButton: true, html: createPSRegisterElementIconHTML("info"), - onClick: withCurrentAnnotatedFile(onViewInfo), + onClick: withCurrentAnnotatedFile(delegate.onViewInfo), }); // TODO(PS): - if (onEditImage && false) { + if (delegate.onEditImage && false) { pswp.ui.registerElement({ name: "edit", // TODO(PS): @@ -505,7 +523,7 @@ export class FileViewerPhotoSwipe { order: 16, isButton: true, html: createPSRegisterElementIconHTML("edit"), - onClick: withCurrentAnnotatedFile(onEditImage), + onClick: withCurrentAnnotatedFile(delegate.onEditImage), onInit: (buttonElement) => pswp.on("change", () => showIf( @@ -523,9 +541,8 @@ export class FileViewerPhotoSwipe { order: 17, isButton: true, html: createPSRegisterElementIconHTML("more"), - onClick: withCurrentAnnotatedFile(onViewInfo), + onClick: withCurrentAnnotatedFile(delegate.onViewInfo), }); - }); // Modify the default UI elements. From 15b49816ccba009940ff8b9c28d8735150e37a09 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 09:53:56 +0530 Subject: [PATCH 10/40] d2 --- .../gallery/components/viewer/FileViewer.tsx | 128 ++++++++++-------- .../gallery/components/viewer/photoswipe.ts | 2 +- 2 files changed, 69 insertions(+), 61 deletions(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index da73c2b1b5..904fa13fde 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -39,6 +39,7 @@ import { fileInfoExifForFile } from "./data-source"; import { FileViewerPhotoSwipe, type FileViewerAnnotatedFile, + type FileViewerPhotoSwipeDelegate, } from "./photoswipe"; // TODO @@ -183,8 +184,27 @@ const FileViewer: React.FC = ({ // onMarkTempDeleted, onSaveEditedImageCopy, }) => { - const psRef = useRef(); - const psDelegateRef = useRef(); + // [Note: FileViewer architecture] + // + // There are 3 parties involved. + // + // 1. Us, "FileViewer". We're a React component. + // + // 2. The custom PhotoSwipe wrapper, "FileViewerPhotoSwipe". It is a class, + // and its currently active instance is maintained in `psRef`. + // + // 3. The delegate, "FileViewerPhotoSwipeDelegate". The delegate acts as a + // bridge between us and `psRef`. It is created once as an object with a + // stable identity (and stored in `psDelegateRef`), but its properties + // keep changing as our props change. + // + // The `psRef` is recreated each time open / close changes. The + // `psDelegateRef` remains the same. + + const psRef = useRef(undefined); + const psDelegateRef = useRef( + undefined, + ); // Whenever we get a callback from our custom PhotoSwipe instance, we also // get the active file on which that action was performed as an argument. @@ -322,78 +342,66 @@ const FileViewer: React.FC = ({ [onSaveEditedImageCopy, handleImageEditorClose, handleClose], ); - useEffect(() => ( - psDelegateRef.current.onClose = handleClose; - onAnnotate: handleAnnotate, - onToggleFavorite: handleToggleFavorite, - onViewInfo: handleViewInfo, - onEditImage: handleEditImage, - }), [ - handleClose, - onAnnotate: handleAnnotate, - onToggleFavorite: handleToggleFavorite, - onViewInfo: handleViewInfo, - onEditImage: handleEditImage, + // Initial value of psDelegateRef. + if (!psDelegateRef.current) { + psDelegateRef.current = { + onClose: handleClose, + onAnnotate: handleAnnotate, + onToggleFavorite: handleToggleFavorite, + onViewInfo: handleViewInfo, + onEditImage: handleEditImage, + }; + } + // Updates to callbacks held by psDelegateRef. + useEffect(() => { + const delegate = psDelegateRef.current!; + delegate.onClose = handleClose; + delegate.onAnnotate = handleAnnotate; + delegate.onToggleFavorite = handleToggleFavorite; + delegate.onViewInfo = handleViewInfo; + delegate.onEditImage = handleEditImage; + }, [ + handleClose, + handleAnnotate, + handleToggleFavorite, + handleViewInfo, + handleEditImage, ]); + useEffect(() => { if (open && !psRef.current) { - // We're open, but we don't have a PhotoSwipe instance. Create - // one. This will show the file viewer dialog. - // - // Before creating it, also create a delegate. The delegate has - // a stable identity so that PhotoSwipe callbacks can be routed - // via it. When any of the - // callbacks change, we update the props of the delegate instead - // of changing the delegate itself. - log.debug(() => ["viewer", "open"]); - const delegate = { - onClose: handleClose, - onAnnotate: handleAnnotate, - onToggleFavorite: handleToggleFavorite, - onViewInfo: handleViewInfo, - onEditImage: handleEditImage, - } - const pswp = new FileViewerPhotoSwipe({ - files, - initialIndex, - disableDownload, - delegate, - }); - psRef.current = pswp; - psDelegateRef.current = delegate; - + // We're open, but we don't have a PhotoSwipe instance. Create + // one. This will show the file viewer dialog. + // + // Before creating it, also create a delegate. The delegate has + // a stable identity so that PhotoSwipe callbacks can be routed + // via it. When any of the + // callbacks change, we update the props of the delegate instead + // of changing the delegate itself. + log.debug(() => ["viewer", "open"]); + const pswp = new FileViewerPhotoSwipe({ + files, + initialIndex, + disableDownload, + delegate: psDelegateRef.current!, + }); + psRef.current = pswp; } else if (!open && psRef.current) { // We're closed, but we still have a PhotoSwipe instance. Cleanup. log.debug(() => ["viewer", "close"]); - psRef.current?.closeIfNeeded(); + psRef.current.closeIfNeeded(); psRef.current = undefined; - psDelegateRef.current = undefined; - } - - // The hook is missing dependencies; this is intentional - we don't want - // to recreate the PhotoSwipe dialog when these dependencies change. - // - // - Updates to initialIndex can be safely ignored: they don't matter, - // only their initial value at the time of open mattered. - // - // - Updates to other properties are not expected after open. We could've - // also added it to the dependencies array, not adding it was a more - // conservative choice to be on the safer side and trigger too few - // instead of too many updates. - // - // - Updates to files matter, but these are conveyed separately. - // TODO(PS): - // + // TODO: How to relay files updates? // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open, onClose, handleViewInfo]); + }, [open, onClose, initialIndex, disableDownload]); const handleRefreshPhotoswipe = useCallback(() => { - pswpRef.current.refreshCurrentSlideContent(); + psRef.current.refreshCurrentSlideContent(); }, []); - log.debug(() => ["viewer", { action: "render", pswpRef: pswpRef.current }]); + log.debug(() => ["viewer", { action: "render", psRef: psRef.current }]); return ( diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index f014d645fd..1a8011d846 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -67,7 +67,7 @@ export interface FileViewerFileAnnotation { isEditableImage?: boolean | undefined; } -interface FileViewerPhotoSwipeDelegate { +export interface FileViewerPhotoSwipeDelegate { /** * Called when the file viewer is closed. */ From 502507abf562e03a83aa061c65bb911f6c2be062 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 10:45:54 +0530 Subject: [PATCH 11/40] d3 --- .../gallery/components/viewer/FileViewer.tsx | 61 +++-- .../gallery/components/viewer/photoswipe.ts | 221 ++++++++++-------- 2 files changed, 164 insertions(+), 118 deletions(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index 904fa13fde..cc4f9cc19b 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -14,7 +14,6 @@ if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { } import { isDesktop } from "@/base/app"; -import { assertionFailed } from "@/base/assert"; import { type ModalVisibilityProps } from "@/base/components/utils/modal"; import { lowercaseExtension } from "@/base/file-name"; import type { LocalUser } from "@/base/local-user"; @@ -32,7 +31,6 @@ import { ImageEditorOverlay, type ImageEditorOverlayProps, } from "@/new/photos/components/ImageEditorOverlay"; -import { wait } from "@/utils/promise"; import { Button, styled } from "@mui/material"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { fileInfoExifForFile } from "./data-source"; @@ -47,12 +45,13 @@ import { // addToFavorites, // removeFromFavorites, // } from "apps/photos/services/collectionService"; -const addToFavorites = async (file: EnteFile) => { - console.log(file); - await wait(3000); - throw new Error("test"); -}; -const removeFromFavorites = addToFavorites; +// import { wait } from "@/utils/promise"; +// const addToFavorites = async (file: EnteFile) => { +// console.log(file); +// await wait(3000); +// throw new Error("test"); +// }; +// const removeFromFavorites = addToFavorites; export type FileViewerProps = ModalVisibilityProps & { /** @@ -268,22 +267,25 @@ const FileViewer: React.FC = ({ const handleToggleFavorite = useMemo(() => { return favoriteFileIDs ? ({ file, annotation }: FileViewerAnnotatedFile) => { - const isFavorite = annotation.isFavorite; - if (isFavorite === undefined) { - assertionFailed(); - return; - } + console.log({ file, annotation }); + // TODO + // const isFavorite = annotation.isFavorite; + // if (isFavorite === undefined) { + // assertionFailed(); + // return; + // } - onMarkUnsyncedFavoriteUpdate(file.id, !isFavorite); - void (isFavorite ? removeFromFavorites : addToFavorites)( - file, - ).catch((e: unknown) => { - log.error("Failed to remove favorite", e); - onMarkUnsyncedFavoriteUpdate(file.id, undefined); - }); + // onMarkUnsyncedFavoriteUpdate(file.id, !isFavorite); + // void (isFavorite ? removeFromFavorites : addToFavorites)( + // file, + // ).catch((e: unknown) => { + // log.error("Failed to remove favorite", e); + // onMarkUnsyncedFavoriteUpdate(file.id, undefined); + // }); } : undefined; - }, [favoriteFileIDs, onMarkUnsyncedFavoriteUpdate]); + // }, [favoriteFileIDs, onMarkUnsyncedFavoriteUpdate]); + }, [favoriteFileIDs]); const handleViewInfo = useCallback( (annotatedFile: FileViewerAnnotatedFile) => { @@ -383,7 +385,12 @@ const FileViewer: React.FC = ({ const pswp = new FileViewerPhotoSwipe({ files, initialIndex, + showModifyActions: !!user, disableDownload, + onClose: handleClose, + onAnnotate: handleAnnotate, + onViewInfo: handleViewInfo, + onEditImage: handleEditImage, delegate: psDelegateRef.current!, }); psRef.current = pswp; @@ -395,7 +402,17 @@ const FileViewer: React.FC = ({ } // TODO: How to relay files updates? // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open, onClose, initialIndex, disableDownload]); + }, [ + open, + onClose, + user, + initialIndex, + disableDownload, + handleClose, + handleAnnotate, + handleViewInfo, + handleEditImage, + ]); const handleRefreshPhotoswipe = useCallback(() => { psRef.current.refreshCurrentSlideContent(); diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index 1a8011d846..dc0fe62ad7 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -1,7 +1,6 @@ /* eslint-disable */ // @ts-nocheck -import { assertionFailed } from "@/base/assert"; import { pt } from "@/base/i18n"; import log from "@/base/log"; import type { EnteFile } from "@/media/file"; @@ -52,31 +51,23 @@ export interface FileViewerFileAnnotation { */ isOwnFile: boolean; /** - * `true` if this file has been marked as a favorite by the user. - * - * The toggle favorite button will not be shown if this is not defined. - * Otherwise it determines the toggle state of the toggle favorite button. + * `true` if the toggle favorite action should be shown for this file. */ - isFavorite?: boolean | undefined; + showFavorite: boolean; /** * `true` if this is an image which can be edited. * * The edit button is shown when this is true. See also the * {@link onEditImage} option for {@link FileViewerPhotoSwipe} constructor. */ - isEditableImage?: boolean | undefined; + isEditableImage: boolean; } export interface FileViewerPhotoSwipeDelegate { /** - * Called when the file viewer is closed. + * Called to obtain the latest list of files. */ - onClose: () => void; - /** - * Called whenever the slide changes to obtain the derived data for the file - * that is about to be displayed. - */ - onAnnotate: (file: EnteFile) => FileViewerFileAnnotation; + onGetFiles?: unknown; // TODO /** * Called when the user activates the toggle favorite action on a file. * @@ -87,6 +78,38 @@ export interface FileViewerPhotoSwipeDelegate { * property will determine the current toggle state of the favorite button. */ onToggleFavorite?: (annotatedFile: FileViewerAnnotatedFile) => void; + // TODO(PS) + /** + * `true` if this file has been marked as a favorite by the user. + * + * The toggle favorite button will not be shown if this is not defined. + * Otherwise it determines the toggle state of the toggle favorite button. + */ + isFavorite?: boolean | undefined; +} + +type FileViewerPhotoSwipeOptions = { + /** + * `true` if various actions that modify the file should be shown. + * + * This is the static variant of various per file annotations. If this is + * not `true`, then various actions like favorite, delete etc are never + * shown. If this is `true`, then their visibility depends on the + * corresponding annotation. + * + * For example, the favorite action is shown only if both this and the + * {@link showFavorite} file annotation are true. + */ + showModifyActions: boolean; + /** + * Called when the file viewer is closed. + */ + onClose: () => void; + /** + * Called whenever the slide is initially displayed or changes, to obtain + * various derived data for the file that is about to be displayed. + */ + onAnnotate: (file: EnteFile) => FileViewerFileAnnotation; /** * Called when the user activates the info action on a file. */ @@ -94,26 +117,21 @@ export interface FileViewerPhotoSwipeDelegate { /** * Called when the user activates the edit action on an image. * - * If this callback is not provided, then the edit button is never shown. If - * this callback is provided, then the visibility of the edit button is - * determined by the {@link isEditableImage} property of - * {@link FileViewerFileAnnotation} for the file. + * If this callback is not provided, then the edit action is never shown. If + * this callback is provided (and {@link showModifyActions} is `true`), then + * the visibility of the edit action is determined by the + * {@link isEditableImage} property of the {@link FileViewerFileAnnotation} + * for the file. */ onEditImage?: (annotatedFile: FileViewerAnnotatedFile) => void; -} - -type FileViewerPhotoSwipeOptions = Pick< - FileViewerProps, - "files" | "initialIndex" | "disableDownload" -> & { /** - * Callbacks. + * Dynamic callbacks. * * The extra level of indirection allows these to be updated without * recreating us. */ delegate: FileViewerPhotoSwipeDelegate; -}; +} & Pick; /** * A file and its annotation, in a nice cosy box. @@ -155,7 +173,7 @@ export class FileViewerPhotoSwipe { */ private opts: Pick; /** - * An object to which we should route various callbacks. + * An object via which we should route various dynamic callbacks. */ private delegate: FileViewerPhotoSwipeDelegate; /** @@ -192,10 +210,16 @@ export class FileViewerPhotoSwipe { files, initialIndex, disableDownload, + showModifyActions, + onClose, + onAnnotate, + onViewInfo, + onEditImage, delegate, }: FileViewerPhotoSwipeOptions) { this.files = files; this.opts = { disableDownload }; + this.delegate = delegate; this.lastActivityDate = new Date(); const pswp = new PhotoSwipe({ @@ -255,34 +279,6 @@ export class FileViewerPhotoSwipe { this.pswp = pswp; - // Helper routines to obtain the file at `currIndex`. - - const currentFile = () => this.files[pswp.currIndex]!; - - const currentAnnotatedFile = () => { - const file = currentFile(); - let annotation = this.activeFileAnnotation; - if (annotation?.fileID != file.id) { - annotation = delegate.onAnnotate(file); - this.activeFileAnnotation = annotation; - } - return { - file, - // The above condition implies that annotation can never be - // undefined, but it doesn't seem to be enough to convince - // TypeScript. Writing the condition in a more unnatural way - // `(!(annotation && annotation?.fileID == file.id))` works, but - // instead we use a non-null assertion here. - annotation: annotation!, - }; - }; - - const currentFileAnnotation = () => currentAnnotatedFile().annotation; - - const withCurrentAnnotatedFile = - (cb: ((af: AnnotatedFile) => void) | undefined) => () => - cb ? cb(currentAnnotatedFile()) : assertionFailed(); - // Provide data about slides to PhotoSwipe via callbacks // https://photoswipe.com/data-sources/#dynamically-generated-data @@ -291,11 +287,11 @@ export class FileViewerPhotoSwipe { }); pswp.addFilter("itemData", (_, index) => { - const file = files[index]!; + const file = this.files[index]!; - let itemData = itemDataForFile(file, () => { - this.pswp.refreshSlideContent(index); - }); + let itemData = itemDataForFile(file, () => + pswp.refreshSlideContent(index), + ); const { fileType, videoURL, ...rest } = itemData; if (fileType === FileType.video && videoURL) { @@ -381,7 +377,9 @@ export class FileViewerPhotoSwipe { // more than 2 slides and then back, or if they reopen the viewer. // // See: [Note: File viewer error handling] - forgetFailedItemDataForFile(currentFile()); + // TODO + console.log(this.currentFile(), e); + forgetFailedItemDataForFile(this.currentFile()); // Pause the video element, if any, when we move away from the // slide. @@ -405,7 +403,7 @@ export class FileViewerPhotoSwipe { ); pswp.on("change", (e) => { - const itemData = pswp.currSlide.content.data; + const itemData = this.pswp.currSlide.content.data; updateFileInfoExifIfNeeded(itemData); }); @@ -418,7 +416,7 @@ export class FileViewerPhotoSwipe { forgetFailedItems(); forgetExif(); // Let our parent know that we have been closed. - delegate.onClose(); + onClose(); }); const showIf = (element: HTMLElement, condition: boolean) => @@ -465,25 +463,26 @@ export class FileViewerPhotoSwipe { }, }); - if (delegate.onToggleFavorite) { - // Only one of these two will end up being shown, so they can - // safely share the same order. + if (showModifyActions) { + // Only one of these two ("favorite" or "unfavorite") will end + // up being shown, so they can safely share the same order. pswp.ui.registerElement({ name: "favorite", title: t("favorite_key"), order: 8, isButton: true, html: createPSRegisterElementIconHTML("favorite"), - onClick: withCurrentAnnotatedFile( - delegate.onToggleFavorite, - ), - onInit: (buttonElement) => - pswp.on("change", () => - showIf( - buttonElement, - currentFileAnnotation().isFavorite === false, - ), - ), + // TODO + // onClick: withCurrentAnnotatedFile( + // delegate.onToggleFavorite, + // ), + // onInit: (buttonElement) => + // pswp.on("change", () => + // showIf( + // buttonElement, + // currentFileAnnotation().isFavorite === false, + // ), + // ), }); pswp.ui.registerElement({ name: "unfavorite", @@ -491,16 +490,17 @@ export class FileViewerPhotoSwipe { order: 8, isButton: true, html: createPSRegisterElementIconHTML("unfavorite"), - onClick: withCurrentAnnotatedFile( - delegate.onToggleFavorite, - ), - onInit: (buttonElement) => - pswp.on("change", () => - showIf( - buttonElement, - currentFileAnnotation().isFavorite === true, - ), - ), + // TODO + // onClick: withCurrentAnnotatedFile( + // delegate.onToggleFavorite, + // ), + // onInit: (buttonElement) => + // pswp.on("change", () => + // showIf( + // buttonElement, + // currentFileAnnotation().isFavorite === true, + // ), + // ), }); } @@ -510,11 +510,11 @@ export class FileViewerPhotoSwipe { order: 9, isButton: true, html: createPSRegisterElementIconHTML("info"), - onClick: withCurrentAnnotatedFile(delegate.onViewInfo), + onClick: () => onViewInfo(this.currentAnnotatedFile()), }); // TODO(PS): - if (delegate.onEditImage && false) { + if (showModifyActions && onEditImage && false) { pswp.ui.registerElement({ name: "edit", // TODO(PS): @@ -523,14 +523,15 @@ export class FileViewerPhotoSwipe { order: 16, isButton: true, html: createPSRegisterElementIconHTML("edit"), - onClick: withCurrentAnnotatedFile(delegate.onEditImage), - onInit: (buttonElement) => - pswp.on("change", () => - showIf( - buttonElement, - !!currentFileAnnotation().isEditableImage, - ), - ), + // TODO + // onClick: withCurrentAnnotatedFile(delegate.onEditImage), + // onInit: (buttonElement) => + // pswp.on("change", () => + // showIf( + // buttonElement, + // !!currentFileAnnotation().isEditableImage, + // ), + // ), }); } @@ -541,7 +542,7 @@ export class FileViewerPhotoSwipe { order: 17, isButton: true, html: createPSRegisterElementIconHTML("more"), - onClick: withCurrentAnnotatedFile(delegate.onViewInfo), + // onClick: withCurrentAnnotatedFile(delegate.onViewInfo), }); }); @@ -592,6 +593,34 @@ export class FileViewerPhotoSwipe { // TODO(PS) } + // Various helper routines to obtain the file at `currIndex`. + + private currentFile() { + return this.files[this.pswp.currIndex]!; + } + + private currentAnnotatedFile() { + const file = this.currentFile(); + let annotation = this.activeFileAnnotation; + if (annotation?.fileID != file.id) { + annotation = this.delegate.onAnnotate(file); + this.activeFileAnnotation = annotation; + } + return { + file, + // The above condition implies that annotation can never be + // undefined, but it doesn't seem to be enough to convince + // TypeScript. Writing the condition in a more unnatural way + // `(!(annotation && annotation?.fileID == file.id))` works, but + // instead we use a non-null assertion here. + annotation: annotation!, + }; + } + + private currentFileAnnotation() { + return this.currentAnnotatedFile().annotation; + } + private clearAutoHideIntervalIfNeeded() { if (this.autoHideCheckIntervalId) { clearInterval(this.autoHideCheckIntervalId); From 093ba4895d9e8c6bc48c0eaba3275548c170849e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 12:16:33 +0530 Subject: [PATCH 12/40] d4 --- web/apps/photos/src/components/PhotoFrame.tsx | 6 +- .../gallery/components/viewer/FileViewer.tsx | 215 +++++++----------- .../gallery/components/viewer/photoswipe.ts | 139 ++++++----- 3 files changed, 155 insertions(+), 205 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index b68f603744..d15a2c42b4 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -553,17 +553,15 @@ const PhotoFrame = ({ user={galleryContext.user ?? undefined} files={files} initialIndex={currentIndex} - isInTrashSection={activeCollectionID === TRASH_SECTION} - isInHiddenSection={isInHiddenSection} disableDownload={!enableDownload} + isInHiddenSection={isInHiddenSection} + isInTrashSection={activeCollectionID === TRASH_SECTION} onTriggerSyncWithRemote={handleTriggerSyncWithRemote} onSaveEditedImageCopy={handleSaveEditedImageCopy} {...{ favoriteFileIDs, fileCollectionIDs, allCollectionsNameByID, - onMarkUnsyncedFavoriteUpdate, - onMarkTempDeleted, onSelectCollection, onSelectPerson, }} diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index cc4f9cc19b..57c4831fd4 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -37,6 +37,7 @@ import { fileInfoExifForFile } from "./data-source"; import { FileViewerPhotoSwipe, type FileViewerAnnotatedFile, + type FileViewerFileAnnotation, type FileViewerPhotoSwipeDelegate, } from "./photoswipe"; @@ -120,28 +121,6 @@ export type FileViewerProps = ModalVisibilityProps & { * not defined, then this prop is not used. */ onTriggerSyncWithRemote?: () => void; - /** - * Called when the viewer wants to update the in-memory, unsynced, favorite - * status of a file maintained by the top level gallery. For more details, - * see {@link unsyncedFavoriteUpdates} in the gallery reducer's - * documentation. - * - * If this is not provided then the toggle favorite action will not be - * shown. - */ - onMarkUnsyncedFavoriteUpdate?: ( - fileID: number, - isFavorite: boolean, - ) => void; - /** - * Called when the viewer wants to mark the given files as deleted in the - * the in-memory, unsynced, state maintained by the top level gallery. For - * more details, see {@link unsyncedFavoriteUpdates} in the gallery - * reducer's documentation. - * - * If this is not provided then the delete action will not be shown. - */ - onMarkTempDeleted?: (files: EnteFile[]) => void; /** * Called when the user edits an image in the image editor and asks us to * save their edits as a copy. @@ -175,39 +154,36 @@ const FileViewer: React.FC = ({ favoriteFileIDs, fileCollectionIDs, allCollectionsNameByID, + onTriggerSyncWithRemote, onSelectCollection, onSelectPerson, - onTriggerSyncWithRemote, - onMarkUnsyncedFavoriteUpdate, - // TODO - // onMarkTempDeleted, onSaveEditedImageCopy, }) => { - // [Note: FileViewer architecture] - // - // There are 3 parties involved. + // There are 3 things involved in this dance: // // 1. Us, "FileViewer". We're a React component. + // 2. The custom PhotoSwipe wrapper, "FileViewerPhotoSwipe". It is a class. + // 3. The delegate, "FileViewerPhotoSwipeDelegate". // - // 2. The custom PhotoSwipe wrapper, "FileViewerPhotoSwipe". It is a class, - // and its currently active instance is maintained in `psRef`. + // The delegate acts as a bridge between us and (our custom) photoswipe + // class, to avoid recreating the class each time a "dynamic" prop changes. + // The delegate has a stable identity, we just keep updating the callback + // functions that it holds. // - // 3. The delegate, "FileViewerPhotoSwipeDelegate". The delegate acts as a - // bridge between us and `psRef`. It is created once as an object with a - // stable identity (and stored in `psDelegateRef`), but its properties - // keep changing as our props change. - // - // The `psRef` is recreated each time open / close changes. The - // `psDelegateRef` remains the same. - - const psRef = useRef(undefined); - const psDelegateRef = useRef( + // The word "dynamic" here means a prop on whose change we should not + // recreate the photoswipe dialog. + const delegateRef = useRef( undefined, ); + // We also need to maintain a ref to the currently displayed dialog since we + // might need to ask it to refresh its contents. + const psRef = useRef(undefined); + // Whenever we get a callback from our custom PhotoSwipe instance, we also - // get the active file on which that action was performed as an argument. - // Save it as a prop so that the rest of our React tree can use it. + // get the active file on which that action was performed as an argument. We + // save it as the `activeAnnotatedFile` state so that the rest of our React + // tree can use it. // // This is not guaranteed, or even intended, to be in sync with the active // file shown within the file viewer. All that this guarantees is this will @@ -215,8 +191,9 @@ const FileViewer: React.FC = ({ const [activeAnnotatedFile, setActiveAnnotatedFile] = useState< FileViewerAnnotatedFile | undefined >(undefined); - // With semantics similar to activeFile, this is the exif data associated - // with the activeAnnotatedFile, if any. + + // With semantics similar to `activeAnnotatedFile`, this is the exif data + // associated with the `activeAnnotatedFile`, if any. const [activeFileExif, setActiveFileExif] = useState< FileInfoExif | undefined >(undefined); @@ -238,55 +215,22 @@ const FileViewer: React.FC = ({ }, [onTriggerSyncWithRemote, onClose]); const handleAnnotate = useCallback( - (file: EnteFile) => { + (file: EnteFile): FileViewerFileAnnotation => { log.debug(() => ["viewer", { action: "annotate", file }]); const fileID = file.id; const isOwnFile = file.ownerID == user?.id; const canModify = isOwnFile && !isInTrashSection && !isInHiddenSection; - const isFavorite = - favoriteFileIDs && onMarkUnsyncedFavoriteUpdate && canModify - ? favoriteFileIDs.has(file.id) - : undefined; + const showFavorite = canModify; const isEditableImage = onSaveEditedImageCopy && canModify ? fileIsEditableImage(file) : undefined; - return { fileID, isOwnFile, isFavorite, isEditableImage }; + return { fileID, isOwnFile, showFavorite, isEditableImage }; }, - [ - user, - isInTrashSection, - isInHiddenSection, - favoriteFileIDs, - onMarkUnsyncedFavoriteUpdate, - onSaveEditedImageCopy, - ], + [user, isInTrashSection, isInHiddenSection, onSaveEditedImageCopy], ); - const handleToggleFavorite = useMemo(() => { - return favoriteFileIDs - ? ({ file, annotation }: FileViewerAnnotatedFile) => { - console.log({ file, annotation }); - // TODO - // const isFavorite = annotation.isFavorite; - // if (isFavorite === undefined) { - // assertionFailed(); - // return; - // } - - // onMarkUnsyncedFavoriteUpdate(file.id, !isFavorite); - // void (isFavorite ? removeFromFavorites : addToFavorites)( - // file, - // ).catch((e: unknown) => { - // log.error("Failed to remove favorite", e); - // onMarkUnsyncedFavoriteUpdate(file.id, undefined); - // }); - } - : undefined; - // }, [favoriteFileIDs, onMarkUnsyncedFavoriteUpdate]); - }, [favoriteFileIDs]); - const handleViewInfo = useCallback( (annotatedFile: FileViewerAnnotatedFile) => { setActiveAnnotatedFile(annotatedFile); @@ -344,70 +288,85 @@ const FileViewer: React.FC = ({ [onSaveEditedImageCopy, handleImageEditorClose, handleClose], ); - // Initial value of psDelegateRef. - if (!psDelegateRef.current) { - psDelegateRef.current = { - onClose: handleClose, - onAnnotate: handleAnnotate, - onToggleFavorite: handleToggleFavorite, - onViewInfo: handleViewInfo, - onEditImage: handleEditImage, - }; + const haveUser = !!user; + const showModifyActions = haveUser; + + const getFiles = useCallback(() => files, [files]); + + const isFavorite = useMemo(() => { + return showModifyActions && favoriteFileIDs + ? (annotatedFile: FileViewerAnnotatedFile): boolean => + favoriteFileIDs.has(annotatedFile.file.id) + : undefined; + }, [showModifyActions, favoriteFileIDs]); + + const toggleFavorite = useMemo(() => { + return favoriteFileIDs + ? ({ file, annotation }: FileViewerAnnotatedFile) => { + console.log({ file, annotation }); + // TODO + // const isFavorite = annotation.isFavorite; + // if (isFavorite === undefined) { + // assertionFailed(); + // return; + // } + + // onMarkUnsyncedFavoriteUpdate(file.id, !isFavorite); + // void (isFavorite ? removeFromFavorites : addToFavorites)( + // file, + // ).catch((e: unknown) => { + // log.error("Failed to remove favorite", e); + // onMarkUnsyncedFavoriteUpdate(file.id, undefined); + // }); + } + : undefined; + // }, [favoriteFileIDs, onMarkUnsyncedFavoriteUpdate]); + }, [favoriteFileIDs]); + + // Initial value of delegate. + if (!delegateRef.current) { + delegateRef.current = { getFiles, isFavorite, toggleFavorite }; } - // Updates to callbacks held by psDelegateRef. + // Updates to delegate callbacks. useEffect(() => { - const delegate = psDelegateRef.current!; - delegate.onClose = handleClose; - delegate.onAnnotate = handleAnnotate; - delegate.onToggleFavorite = handleToggleFavorite; - delegate.onViewInfo = handleViewInfo; - delegate.onEditImage = handleEditImage; - }, [ - handleClose, - handleAnnotate, - handleToggleFavorite, - handleViewInfo, - handleEditImage, - ]); + const delegate = delegateRef.current!; + delegate.getFiles = getFiles; + delegate.isFavorite = isFavorite; + delegate.toggleFavorite = toggleFavorite; + }, [getFiles, isFavorite, toggleFavorite]); useEffect(() => { - if (open && !psRef.current) { - // We're open, but we don't have a PhotoSwipe instance. Create - // one. This will show the file viewer dialog. - // - // Before creating it, also create a delegate. The delegate has - // a stable identity so that PhotoSwipe callbacks can be routed - // via it. When any of the - // callbacks change, we update the props of the delegate instead - // of changing the delegate itself. - log.debug(() => ["viewer", "open"]); + if (open) { + // We're open. Create psRef. This will show the file viewer dialog. + log.debug(() => ["viewer", { action: "open" }]); + const pswp = new FileViewerPhotoSwipe({ - files, initialIndex, - showModifyActions: !!user, disableDownload, + showModifyActions, + delegate: delegateRef.current!, onClose: handleClose, onAnnotate: handleAnnotate, onViewInfo: handleViewInfo, onEditImage: handleEditImage, - delegate: psDelegateRef.current!, }); + psRef.current = pswp; - } else if (!open && psRef.current) { - // We're closed, but we still have a PhotoSwipe instance. Cleanup. - log.debug(() => ["viewer", "close"]); - psRef.current.closeIfNeeded(); - psRef.current = undefined; + + return () => { + // Close dialog in the effect callback. + log.debug(() => ["viewer", { action: "close" }]); + pswp.closeIfNeeded(); + }; } - // TODO: How to relay files updates? - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ open, onClose, user, initialIndex, disableDownload, + showModifyActions, handleClose, handleAnnotate, handleViewInfo, @@ -415,7 +374,7 @@ const FileViewer: React.FC = ({ ]); const handleRefreshPhotoswipe = useCallback(() => { - psRef.current.refreshCurrentSlideContent(); + psRef.current!.refreshCurrentSlideContent(); }, []); log.debug(() => ["viewer", { action: "render", psRef: psRef.current }]); @@ -429,8 +388,8 @@ const FileViewer: React.FC = ({ file={activeAnnotatedFile?.file} exif={activeFileExif} allowEdits={!!activeAnnotatedFile?.annotation.isOwnFile} - allowMap={!!user} - showCollections={!!user} + allowMap={haveUser} + showCollections={haveUser} scheduleUpdate={handleScheduleUpdate} refreshPhotoswipe={handleRefreshPhotoswipe} onSelectCollection={handleSelectCollection} diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index dc0fe62ad7..f8bf5e1a09 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -14,7 +14,6 @@ import { itemDataForFile, updateFileInfoExifIfNeeded, } from "./data-source"; -import type { FileViewerProps } from "./FileViewer"; import { createPSRegisterElementIconHTML } from "./icons"; // TODO(PS): WIP gallery using upstream photoswipe @@ -66,29 +65,39 @@ export interface FileViewerFileAnnotation { export interface FileViewerPhotoSwipeDelegate { /** * Called to obtain the latest list of files. + * + * [Note: Changes to underlying files when file viewer is open] + * + * The list of files shown by the viewer might change while the viewer is + * open. We do not actively refresh the viewer when this happens since that + * would result in the user's zoom / pan state being lost. + * + * However, we always read the latest list via the delegate, so any + * subsequent user initiated slide navigation (e.g. moving to the next + * slide) will use the new list. */ - onGetFiles?: unknown; // TODO + getFiles?: () => EnteFile[]; + /** + * Return `true` if the provided file has been marked as a favorite by the + * user. + * + * The toggle favorite button will not be shown for the file if this returns + * `undefined`. Otherwise the return value determines the toggle state of + * the toggle favorite button for the file. + */ + isFavorite?: ( + annotatedFile: FileViewerAnnotatedFile, + ) => boolean | undefined; /** * Called when the user activates the toggle favorite action on a file. - * - * If this callback is not provided, then the toggle favorite button is not - * shown. If this callback is provided, then the favorite button is shown if - * the {@link isFavorite} property of {@link FileViewerFileAnnotation} for - * the file is provided. In that case, the value of the {@link isFavorite} - * property will determine the current toggle state of the favorite button. */ - onToggleFavorite?: (annotatedFile: FileViewerAnnotatedFile) => void; - // TODO(PS) - /** - * `true` if this file has been marked as a favorite by the user. - * - * The toggle favorite button will not be shown if this is not defined. - * Otherwise it determines the toggle state of the toggle favorite button. - */ - isFavorite?: boolean | undefined; + toggleFavorite?: (annotatedFile: FileViewerAnnotatedFile) => void; } -type FileViewerPhotoSwipeOptions = { +type FileViewerPhotoSwipeOptions = Pick< + FileViewerProps, + "initialIndex" | "disableDownload" +> & { /** * `true` if various actions that modify the file should be shown. * @@ -101,6 +110,13 @@ type FileViewerPhotoSwipeOptions = { * {@link showFavorite} file annotation are true. */ showModifyActions: boolean; + /** + * Dynamic callbacks. + * + * The extra level of indirection allows these to be updated without + * recreating us. + */ + delegate: FileViewerPhotoSwipeDelegate; /** * Called when the file viewer is closed. */ @@ -124,14 +140,7 @@ type FileViewerPhotoSwipeOptions = { * for the file. */ onEditImage?: (annotatedFile: FileViewerAnnotatedFile) => void; - /** - * Dynamic callbacks. - * - * The extra level of indirection allows these to be updated without - * recreating us. - */ - delegate: FileViewerPhotoSwipeDelegate; -} & Pick; +}; /** * A file and its annotation, in a nice cosy box. @@ -172,10 +181,6 @@ export class FileViewerPhotoSwipe { * The options with which we were initialized. */ private opts: Pick; - /** - * An object via which we should route various dynamic callbacks. - */ - private delegate: FileViewerPhotoSwipeDelegate; /** * An interval that invokes a periodic check of whether we should the hide * controls if the user does not perform any pointer events for a while. @@ -207,19 +212,16 @@ export class FileViewerPhotoSwipe { private activeFileAnnotation: FileViewerFileAnnotation | undefined; constructor({ - files, initialIndex, disableDownload, showModifyActions, + delegate, onClose, onAnnotate, onViewInfo, onEditImage, - delegate, }: FileViewerPhotoSwipeOptions) { - this.files = files; this.opts = { disableDownload }; - this.delegate = delegate; this.lastActivityDate = new Date(); const pswp = new PhotoSwipe({ @@ -279,15 +281,38 @@ export class FileViewerPhotoSwipe { this.pswp = pswp; + // Various helper routines to obtain the file at `currIndex`. + + const currentFile = () => delegate.getFiles()[pswp.currIndex]!; + + const currentAnnotatedFile = () => { + const file = currentFile(); + let annotation = this.activeFileAnnotation; + if (annotation?.fileID != file.id) { + annotation = onAnnotate(file); + this.activeFileAnnotation = annotation; + } + return { + file, + // The above condition implies that annotation can never be + // undefined, but it doesn't seem to be enough to convince + // TypeScript. Writing the condition in a more unnatural way + // `(!(annotation && annotation?.fileID == file.id))` works, but + // instead we use a non-null assertion here. + annotation: annotation!, + }; + }; + + const currentFileAnnotation = () => currentAnnotatedFile().annotation; + // Provide data about slides to PhotoSwipe via callbacks // https://photoswipe.com/data-sources/#dynamically-generated-data - pswp.addFilter("numItems", () => { - return this.files.length; - }); + pswp.addFilter("numItems", () => delegate.getFiles().length); pswp.addFilter("itemData", (_, index) => { - const file = this.files[index]!; + const files = delegate.getFiles(); + const file = files[index]!; let itemData = itemDataForFile(file, () => pswp.refreshSlideContent(index), @@ -378,8 +403,8 @@ export class FileViewerPhotoSwipe { // // See: [Note: File viewer error handling] // TODO - console.log(this.currentFile(), e); - forgetFailedItemDataForFile(this.currentFile()); + console.log(currentFile(), e); + forgetFailedItemDataForFile(currentFile()); // Pause the video element, if any, when we move away from the // slide. @@ -510,7 +535,7 @@ export class FileViewerPhotoSwipe { order: 9, isButton: true, html: createPSRegisterElementIconHTML("info"), - onClick: () => onViewInfo(this.currentAnnotatedFile()), + onClick: () => onViewInfo(currentAnnotatedFile()), }); // TODO(PS): @@ -589,38 +614,6 @@ export class FileViewerPhotoSwipe { this.pswp.refreshSlideContent(this.pswp.currIndex); } - updateFiles(files: EnteFile[]) { - // TODO(PS) - } - - // Various helper routines to obtain the file at `currIndex`. - - private currentFile() { - return this.files[this.pswp.currIndex]!; - } - - private currentAnnotatedFile() { - const file = this.currentFile(); - let annotation = this.activeFileAnnotation; - if (annotation?.fileID != file.id) { - annotation = this.delegate.onAnnotate(file); - this.activeFileAnnotation = annotation; - } - return { - file, - // The above condition implies that annotation can never be - // undefined, but it doesn't seem to be enough to convince - // TypeScript. Writing the condition in a more unnatural way - // `(!(annotation && annotation?.fileID == file.id))` works, but - // instead we use a non-null assertion here. - annotation: annotation!, - }; - } - - private currentFileAnnotation() { - return this.currentAnnotatedFile().annotation; - } - private clearAutoHideIntervalIfNeeded() { if (this.autoHideCheckIntervalId) { clearInterval(this.autoHideCheckIntervalId); From ec11bc70924e0113d8b2cc9ff7cffba04e94b997 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 12:25:35 +0530 Subject: [PATCH 13/40] Complete --- .../gallery/components/viewer/data-source.ts | 13 +++++-------- .../gallery/components/viewer/photoswipe.ts | 7 +++---- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/web/packages/gallery/components/viewer/data-source.ts b/web/packages/gallery/components/viewer/data-source.ts index dc0901cca5..7b931d488b 100644 --- a/web/packages/gallery/components/viewer/data-source.ts +++ b/web/packages/gallery/components/viewer/data-source.ts @@ -224,8 +224,11 @@ export const itemDataForFile = (file: EnteFile, needsRefresh: () => void) => { * This is called when the user moves away from a slide so that we attempt a * full retry when they come back the next time. */ -export const forgetFailedItemDataForFile = (file: EnteFile) => - forgetFailedItemDataForFileID(file.id); +export const forgetFailedItemDataForFileID = (fileID: number) => { + if (_state.itemDataByFileID.get(fileID)?.fetchFailed) { + _state.itemDataByFileID.delete(fileID); + } +}; /** * Forget item data for the all files whose fetch had failed. @@ -236,12 +239,6 @@ export const forgetFailedItemDataForFile = (file: EnteFile) => export const forgetFailedItems = () => [..._state.itemDataByFileID.keys()].forEach(forgetFailedItemDataForFileID); -const forgetFailedItemDataForFileID = (fileID: number) => { - if (_state.itemDataByFileID.get(fileID)?.fetchFailed) { - _state.itemDataByFileID.delete(fileID); - } -}; - const enqueueUpdates = async (file: EnteFile) => { const fileID = file.id; const fileType = file.metadata.fileType; diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index f8bf5e1a09..6cfe8f4918 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -9,7 +9,7 @@ import { t } from "i18next"; import { forgetExif, forgetExifForItemData, - forgetFailedItemDataForFile, + forgetFailedItemDataForFileID, forgetFailedItems, itemDataForFile, updateFileInfoExifIfNeeded, @@ -402,9 +402,8 @@ export class FileViewerPhotoSwipe { // more than 2 slides and then back, or if they reopen the viewer. // // See: [Note: File viewer error handling] - // TODO - console.log(currentFile(), e); - forgetFailedItemDataForFile(currentFile()); + const fileID = e.content?.data?.fileID; + if (fileID) forgetFailedItemDataForFileID(fileID); // Pause the video element, if any, when we move away from the // slide. From b851a30c1805922953b026b67785174178b9ae75 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 12:42:05 +0530 Subject: [PATCH 14/40] Change both in sync --- .../gallery/components/viewer/FileViewer.tsx | 56 +++++++++---------- .../gallery/components/viewer/photoswipe.ts | 4 +- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index 57c4831fd4..8c4164da84 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -293,36 +293,36 @@ const FileViewer: React.FC = ({ const getFiles = useCallback(() => files, [files]); - const isFavorite = useMemo(() => { - return showModifyActions && favoriteFileIDs - ? (annotatedFile: FileViewerAnnotatedFile): boolean => - favoriteFileIDs.has(annotatedFile.file.id) - : undefined; + const [isFavorite, toggleFavorite] = useMemo(() => { + if (!showModifyActions || !favoriteFileIDs) + return [undefined, undefined]; + + const isFavorite = ({ file }: FileViewerAnnotatedFile) => + favoriteFileIDs.has(file.id); + + const toggleFavorite = ({ + file, + annotation, + }: FileViewerAnnotatedFile) => { + console.log({ file, annotation }); + // TODO + // const isFavorite = annotation.isFavorite; + // if (isFavorite === undefined) { + // assertionFailed(); + // return; + // } + + // onMarkUnsyncedFavoriteUpdate(file.id, !isFavorite); + // void (isFavorite ? removeFromFavorites : addToFavorites)( + // file, + // ).catch((e: unknown) => { + // log.error("Failed to remove favorite", e); + // onMarkUnsyncedFavoriteUpdate(file.id, undefined); + // }); + }; + return [isFavorite, toggleFavorite]; }, [showModifyActions, favoriteFileIDs]); - const toggleFavorite = useMemo(() => { - return favoriteFileIDs - ? ({ file, annotation }: FileViewerAnnotatedFile) => { - console.log({ file, annotation }); - // TODO - // const isFavorite = annotation.isFavorite; - // if (isFavorite === undefined) { - // assertionFailed(); - // return; - // } - - // onMarkUnsyncedFavoriteUpdate(file.id, !isFavorite); - // void (isFavorite ? removeFromFavorites : addToFavorites)( - // file, - // ).catch((e: unknown) => { - // log.error("Failed to remove favorite", e); - // onMarkUnsyncedFavoriteUpdate(file.id, undefined); - // }); - } - : undefined; - // }, [favoriteFileIDs, onMarkUnsyncedFavoriteUpdate]); - }, [favoriteFileIDs]); - // Initial value of delegate. if (!delegateRef.current) { delegateRef.current = { getFiles, isFavorite, toggleFavorite }; diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index 6cfe8f4918..bd418dbb9e 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -85,9 +85,7 @@ export interface FileViewerPhotoSwipeDelegate { * `undefined`. Otherwise the return value determines the toggle state of * the toggle favorite button for the file. */ - isFavorite?: ( - annotatedFile: FileViewerAnnotatedFile, - ) => boolean | undefined; + isFavorite?: (annotatedFile: FileViewerAnnotatedFile) => boolean; /** * Called when the user activates the toggle favorite action on a file. */ From 28d412a12b84f3753f27562fb83e70bb8a609122 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 13:09:21 +0530 Subject: [PATCH 15/40] disabled --- .../gallery/components/viewer/FileViewer.tsx | 26 +++---- .../gallery/components/viewer/photoswipe.ts | 70 +++++++++++-------- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index 8c4164da84..1fbec36920 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -293,17 +293,19 @@ const FileViewer: React.FC = ({ const getFiles = useCallback(() => files, [files]); - const [isFavorite, toggleFavorite] = useMemo(() => { - if (!showModifyActions || !favoriteFileIDs) - return [undefined, undefined]; - - const isFavorite = ({ file }: FileViewerAnnotatedFile) => + const isFavorite = useCallback( + ({ file }: FileViewerAnnotatedFile) => { + if (!showModifyActions || !favoriteFileIDs) return undefined; favoriteFileIDs.has(file.id); + }, + [showModifyActions, favoriteFileIDs], + ); + + const toggleFavorite = useCallback( + ({ file, annotation }: FileViewerAnnotatedFile) => { + if (!showModifyActions || !favoriteFileIDs) + throw new Error("Unexpected invocation"); - const toggleFavorite = ({ - file, - annotation, - }: FileViewerAnnotatedFile) => { console.log({ file, annotation }); // TODO // const isFavorite = annotation.isFavorite; @@ -319,9 +321,9 @@ const FileViewer: React.FC = ({ // log.error("Failed to remove favorite", e); // onMarkUnsyncedFavoriteUpdate(file.id, undefined); // }); - }; - return [isFavorite, toggleFavorite]; - }, [showModifyActions, favoriteFileIDs]); + }, + [showModifyActions, favoriteFileIDs], + ); // Initial value of delegate. if (!delegateRef.current) { diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index bd418dbb9e..ac4a6a682c 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -76,20 +76,23 @@ export interface FileViewerPhotoSwipeDelegate { * subsequent user initiated slide navigation (e.g. moving to the next * slide) will use the new list. */ - getFiles?: () => EnteFile[]; + getFiles: () => EnteFile[]; /** * Return `true` if the provided file has been marked as a favorite by the * user. * - * The toggle favorite button will not be shown for the file if this returns - * `undefined`. Otherwise the return value determines the toggle state of - * the toggle favorite button for the file. + * The toggle favorite button will not be shown for the file if + * this callback returns `undefined`. Otherwise the return value determines + * the toggle state of the toggle favorite button for the file. */ - isFavorite?: (annotatedFile: FileViewerAnnotatedFile) => boolean; + isFavorite: (annotatedFile: FileViewerAnnotatedFile) => boolean | undefined; /** * Called when the user activates the toggle favorite action on a file. + * + * The toggle favorite button will be disabled for the file until the + * promise returned by this function returns settles. */ - toggleFavorite?: (annotatedFile: FileViewerAnnotatedFile) => void; + toggleFavorite: (annotatedFile: FileViewerAnnotatedFile) => Promise; } type FileViewerPhotoSwipeOptions = Pick< @@ -208,6 +211,10 @@ export class FileViewerPhotoSwipe { * scope. */ private activeFileAnnotation: FileViewerFileAnnotation | undefined; + /** + * IDs of files for which a there is a favorite update in progress. + */ + private pendingFavoriteUpdates = new Set(); constructor({ initialIndex, @@ -486,6 +493,25 @@ export class FileViewerPhotoSwipe { }); if (showModifyActions) { + const toggleFavorite = async () => { + const af = currentAnnotatedFile(); + this.pendingFavoriteUpdates.add(af.file.id); + await delegate.toggleFavorite(af); + this.pendingFavoriteUpdates.delete(af.file.id); + }; + + const forIsFavorite = ( + buttonElement: HTMLElement, + value: boolean, + ) => { + const af = currentAnnotatedFile(); + const isFavorite = delegate.isFavorite(af); + showIf(buttonElement, isFavorite === value); + buttonElement.disabled = this.pendingFavoriteUpdates.has( + af.file.id, + ); + }; + // Only one of these two ("favorite" or "unfavorite") will end // up being shown, so they can safely share the same order. pswp.ui.registerElement({ @@ -494,17 +520,11 @@ export class FileViewerPhotoSwipe { order: 8, isButton: true, html: createPSRegisterElementIconHTML("favorite"), - // TODO - // onClick: withCurrentAnnotatedFile( - // delegate.onToggleFavorite, - // ), - // onInit: (buttonElement) => - // pswp.on("change", () => - // showIf( - // buttonElement, - // currentFileAnnotation().isFavorite === false, - // ), - // ), + onClick: toggleFavorite, + onInit: (buttonElement) => + pswp.on("change", () => + forIsFavorite(buttonElement, false), + ), }); pswp.ui.registerElement({ name: "unfavorite", @@ -512,17 +532,11 @@ export class FileViewerPhotoSwipe { order: 8, isButton: true, html: createPSRegisterElementIconHTML("unfavorite"), - // TODO - // onClick: withCurrentAnnotatedFile( - // delegate.onToggleFavorite, - // ), - // onInit: (buttonElement) => - // pswp.on("change", () => - // showIf( - // buttonElement, - // currentFileAnnotation().isFavorite === true, - // ), - // ), + onClick: toggleFavorite, + onInit: (buttonElement) => + pswp.on("change", () => + forIsFavorite(buttonElement, true), + ), }); } From cea88b91d243a132b6e9b00f2f525a993bb4812f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 13:24:21 +0530 Subject: [PATCH 16/40] dis 2 --- .../gallery/components/viewer/FileViewer.tsx | 5 +++-- .../gallery/components/viewer/photoswipe.ts | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index 1fbec36920..44d51eab81 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -296,16 +296,17 @@ const FileViewer: React.FC = ({ const isFavorite = useCallback( ({ file }: FileViewerAnnotatedFile) => { if (!showModifyActions || !favoriteFileIDs) return undefined; - favoriteFileIDs.has(file.id); + return favoriteFileIDs.has(file.id); }, [showModifyActions, favoriteFileIDs], ); const toggleFavorite = useCallback( - ({ file, annotation }: FileViewerAnnotatedFile) => { + async ({ file, annotation }: FileViewerAnnotatedFile) => { if (!showModifyActions || !favoriteFileIDs) throw new Error("Unexpected invocation"); + await new Promise((r) => setTimeout(r, 7000)); console.log({ file, annotation }); // TODO // const isFavorite = annotation.isFavorite; diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index ac4a6a682c..d6dbd2997f 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -493,15 +493,20 @@ export class FileViewerPhotoSwipe { }); if (showModifyActions) { - const toggleFavorite = async () => { + const toggleFavorite = async ( + buttonElement: HTMLButtonElement, + value: boolean, + ) => { const af = currentAnnotatedFile(); this.pendingFavoriteUpdates.add(af.file.id); + buttonElement.disabled = true; await delegate.toggleFavorite(af); this.pendingFavoriteUpdates.delete(af.file.id); + showFavoriteIf(buttonElement, value); }; - const forIsFavorite = ( - buttonElement: HTMLElement, + const showFavoriteIf = ( + buttonElement: HTMLButtonElement, value: boolean, ) => { const af = currentAnnotatedFile(); @@ -520,10 +525,10 @@ export class FileViewerPhotoSwipe { order: 8, isButton: true, html: createPSRegisterElementIconHTML("favorite"), - onClick: toggleFavorite, + onClick: (e) => toggleFavorite(e.target, false), onInit: (buttonElement) => pswp.on("change", () => - forIsFavorite(buttonElement, false), + showFavoriteIf(buttonElement, false), ), }); pswp.ui.registerElement({ @@ -532,10 +537,10 @@ export class FileViewerPhotoSwipe { order: 8, isButton: true, html: createPSRegisterElementIconHTML("unfavorite"), - onClick: toggleFavorite, + onClick: (e) => toggleFavorite(e.target), onInit: (buttonElement) => pswp.on("change", () => - forIsFavorite(buttonElement, true), + showFavoriteIf(buttonElement, true), ), }); } From ffc1db7369d4cbd71618cbefe2d595bbbff04bc6 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 13:33:50 +0530 Subject: [PATCH 17/40] err 1 --- .../gallery/components/viewer/FileViewer.tsx | 12 ++++++++++-- web/packages/gallery/components/viewer/photoswipe.ts | 6 +++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index 44d51eab81..5aefa1cb72 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -15,6 +15,7 @@ if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { import { isDesktop } from "@/base/app"; import { type ModalVisibilityProps } from "@/base/components/utils/modal"; +import { useBaseContext } from "@/base/context"; import { lowercaseExtension } from "@/base/file-name"; import type { LocalUser } from "@/base/local-user"; import log from "@/base/log"; @@ -159,6 +160,8 @@ const FileViewer: React.FC = ({ onSelectPerson, onSaveEditedImageCopy, }) => { + const { onGenericError } = useBaseContext(); + // There are 3 things involved in this dance: // // 1. Us, "FileViewer". We're a React component. @@ -306,7 +309,12 @@ const FileViewer: React.FC = ({ if (!showModifyActions || !favoriteFileIDs) throw new Error("Unexpected invocation"); - await new Promise((r) => setTimeout(r, 7000)); + try { + await new Promise((r) => setTimeout(r, 3000)); + throw new Error("test"); + } catch (e) { + onGenericError(e); + } console.log({ file, annotation }); // TODO // const isFavorite = annotation.isFavorite; @@ -323,7 +331,7 @@ const FileViewer: React.FC = ({ // onMarkUnsyncedFavoriteUpdate(file.id, undefined); // }); }, - [showModifyActions, favoriteFileIDs], + [showModifyActions, onGenericError, favoriteFileIDs], ); // Initial value of delegate. diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index d6dbd2997f..d7f63e69d3 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -90,7 +90,11 @@ export interface FileViewerPhotoSwipeDelegate { * Called when the user activates the toggle favorite action on a file. * * The toggle favorite button will be disabled for the file until the - * promise returned by this function returns settles. + * promise returned by this function returns fulfills. + * + * > Note: The caller is expected to handle any errors that occur, and + * > should not reject for foreseeable failures, otherwise the button will + * > remain in the disabled state (until the file viewer is closed). */ toggleFavorite: (annotatedFile: FileViewerAnnotatedFile) => Promise; } From eb7f0c0bed901982f80d443d50806608e849bf56 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 13:48:53 +0530 Subject: [PATCH 18/40] Fix extra invalidations --- web/apps/photos/src/components/PhotoFrame.tsx | 10 +- web/apps/photos/src/pages/gallery.tsx | 165 +++++++++--------- 2 files changed, 91 insertions(+), 84 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index d15a2c42b4..a18242a46c 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -101,7 +101,6 @@ export type PhotoFrameProps = Pick< */ modePlus?: GalleryBarMode | "search"; files: EnteFile[]; - syncWithRemote: () => Promise; setSelected: ( selected: SelectedState | ((selected: SelectedState) => SelectedState), ) => void; @@ -145,6 +144,7 @@ export type PhotoFrameProps = Pick< isInHiddenSection?: boolean; setFilesDownloadProgressAttributesCreator?: SetFilesDownloadProgressAttributesCreator; selectable?: boolean; + onSyncWithRemote: () => Promise; }; /** @@ -154,7 +154,6 @@ const PhotoFrame = ({ mode, modePlus, files, - syncWithRemote, setSelected, selected, favoriteFileIDs, @@ -170,6 +169,7 @@ const PhotoFrame = ({ isInHiddenSection, setFilesDownloadProgressAttributesCreator, selectable, + onSyncWithRemote, onSelectCollection, onSelectPerson, }: PhotoFrameProps) => { @@ -262,8 +262,8 @@ const PhotoFrame = ({ }, [selected]); const handleTriggerSyncWithRemote = useCallback( - () => void syncWithRemote(), - [syncWithRemote], + () => void onSyncWithRemote(), + [onSyncWithRemote], ); const handleSaveEditedImageCopy = useCallback( @@ -303,7 +303,7 @@ const PhotoFrame = ({ throw new Error("Not implemented"); } else { setOpen(false); - needUpdate && syncWithRemote(); + needUpdate && onSyncWithRemote(); setIsPhotoSwipeOpen?.(false); } }; diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 5b88e5e9e3..cdacf81631 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -515,88 +515,96 @@ const Page: React.FC = () => { }; }, [selectAll, clearSelection]); - const showSessionExpiredDialog = () => - showMiniDialog(sessionExpiredDialogAttributes(logout)); + const showSessionExpiredDialog = useCallback( + () => showMiniDialog(sessionExpiredDialogAttributes(logout)), + [showMiniDialog, logout], + ); - const syncWithRemote = async (force = false, silent = false) => { - if (!navigator.onLine) return; - if (syncInProgress.current && !force) { - resync.current = { force, silent }; - return; - } - const isForced = syncInProgress.current && force; - syncInProgress.current = true; - try { - const token = getToken(); - if (!token) { + const handleSyncWithRemote = useCallback( + async (force = false, silent = false) => { + if (!navigator.onLine) return; + if (syncInProgress.current && !force) { + resync.current = { force, silent }; return; } - const tokenValid = await isTokenValid(token); - if (!tokenValid) { - throw new Error(CustomError.SESSION_EXPIRED); + const isForced = syncInProgress.current && force; + syncInProgress.current = true; + try { + const token = getToken(); + if (!token) { + return; + } + const tokenValid = await isTokenValid(token); + if (!tokenValid) { + throw new Error(CustomError.SESSION_EXPIRED); + } + !silent && showLoadingBar(); + await preCollectionsAndFilesSync(); + const allCollections = await getAllLatestCollections(); + const [hiddenCollections, collections] = splitByPredicate( + allCollections, + isHiddenCollection, + ); + dispatch({ + type: "setAllCollections", + collections, + hiddenCollections, + }); + const didUpdateNormalFiles = await syncFiles( + "normal", + collections, + (files) => dispatch({ type: "setFiles", files }), + (files) => dispatch({ type: "fetchFiles", files }), + ); + const didUpdateHiddenFiles = await syncFiles( + "hidden", + hiddenCollections, + (hiddenFiles) => + dispatch({ type: "setHiddenFiles", hiddenFiles }), + (hiddenFiles) => + dispatch({ type: "fetchHiddenFiles", hiddenFiles }), + ); + if (didUpdateNormalFiles || didUpdateHiddenFiles) + exportService.onLocalFilesUpdated(); + await syncTrash(allCollections, (trashedFiles: EnteFile[]) => + dispatch({ type: "setTrashedFiles", trashedFiles }), + ); + // syncWithRemote is called with the force flag set to true before + // doing an upload. So it is possible, say when resuming a pending + // upload, that we get two syncWithRemotes happening in parallel. + // + // Do the non-file-related sync only for one of these parallel ones. + if (!isForced) { + await sync(); + } + } catch (e) { + switch (e.message) { + case CustomError.SESSION_EXPIRED: + showSessionExpiredDialog(); + break; + case CustomError.KEY_MISSING: + clearKeys(); + router.push(PAGES.CREDENTIALS); + break; + default: + log.error("syncWithRemote failed", e); + } + } finally { + dispatch({ type: "clearUnsyncedState" }); + !silent && hideLoadingBar(); } - !silent && showLoadingBar(); - await preCollectionsAndFilesSync(); - const allCollections = await getAllLatestCollections(); - const [hiddenCollections, collections] = splitByPredicate( - allCollections, - isHiddenCollection, - ); - dispatch({ - type: "setAllCollections", - collections, - hiddenCollections, - }); - const didUpdateNormalFiles = await syncFiles( - "normal", - collections, - (files) => dispatch({ type: "setFiles", files }), - (files) => dispatch({ type: "fetchFiles", files }), - ); - const didUpdateHiddenFiles = await syncFiles( - "hidden", - hiddenCollections, - (hiddenFiles) => - dispatch({ type: "setHiddenFiles", hiddenFiles }), - (hiddenFiles) => - dispatch({ type: "fetchHiddenFiles", hiddenFiles }), - ); - if (didUpdateNormalFiles || didUpdateHiddenFiles) - exportService.onLocalFilesUpdated(); - await syncTrash(allCollections, (trashedFiles: EnteFile[]) => - dispatch({ type: "setTrashedFiles", trashedFiles }), - ); - // syncWithRemote is called with the force flag set to true before - // doing an upload. So it is possible, say when resuming a pending - // upload, that we get two syncWithRemotes happening in parallel. - // - // Do the non-file-related sync only for one of these parallel ones. - if (!isForced) { - await sync(); + syncInProgress.current = false; + if (resync.current) { + const { force, silent } = resync.current; + setTimeout(() => handleSyncWithRemote(force, silent), 0); + resync.current = undefined; } - } catch (e) { - switch (e.message) { - case CustomError.SESSION_EXPIRED: - showSessionExpiredDialog(); - break; - case CustomError.KEY_MISSING: - clearKeys(); - router.push(PAGES.CREDENTIALS); - break; - default: - log.error("syncWithRemote failed", e); - } - } finally { - dispatch({ type: "clearUnsyncedState" }); - !silent && hideLoadingBar(); - } - syncInProgress.current = false; - if (resync.current) { - const { force, silent } = resync.current; - setTimeout(() => syncWithRemote(force, silent), 0); - resync.current = undefined; - } - }; + }, + [showLoadingBar, hideLoadingBar, router, showSessionExpiredDialog], + ); + + // Alias for existing code. + const syncWithRemote = handleSyncWithRemote; const setupSelectAllKeyBoardShortcutHandler = () => { const handleKeyUp = (e: KeyboardEvent) => { @@ -1047,8 +1055,6 @@ const Page: React.FC = () => { mode={barMode} modePlus={isInSearchMode ? "search" : barMode} files={filteredFiles} - // TODO: Warning: Doesn't have stable identity. - syncWithRemote={syncWithRemote} setSelected={setSelected} selected={selected} favoriteFileIDs={state.favoriteFileIDs} @@ -1070,6 +1076,7 @@ const Page: React.FC = () => { handleMarkUnsyncedFavoriteUpdate } onMarkTempDeleted={handleMarkTempDeleted} + onSyncWithRemote={handleSyncWithRemote} onSelectCollection={handleSelectCollection} onSelectPerson={handleSelectPerson} /> From 1605f71a544f0c73b609041835b71722bae326f9 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 14:11:32 +0530 Subject: [PATCH 19/40] Fill in --- .../gallery/components/viewer/FileViewer.tsx | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index 5aefa1cb72..55800ab218 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -14,6 +14,7 @@ if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { } import { isDesktop } from "@/base/app"; +import { assertionFailed } from "@/base/assert"; import { type ModalVisibilityProps } from "@/base/components/utils/modal"; import { useBaseContext } from "@/base/context"; import { lowercaseExtension } from "@/base/file-name"; @@ -305,33 +306,22 @@ const FileViewer: React.FC = ({ ); const toggleFavorite = useCallback( - async ({ file, annotation }: FileViewerAnnotatedFile) => { - if (!showModifyActions || !favoriteFileIDs) - throw new Error("Unexpected invocation"); + async (annotatedFile: FileViewerAnnotatedFile) => { + const { file, annotation } = annotatedFile; + + const isFav = isFavorite(annotatedFile); + if (isFav === undefined || !annotation.showFavorite) { + assertionFailed(); + return; + } try { - await new Promise((r) => setTimeout(r, 3000)); - throw new Error("test"); + await (isFav ? removeFromFavorites : addToFavorites)(file); } catch (e) { onGenericError(e); } - console.log({ file, annotation }); - // TODO - // const isFavorite = annotation.isFavorite; - // if (isFavorite === undefined) { - // assertionFailed(); - // return; - // } - - // onMarkUnsyncedFavoriteUpdate(file.id, !isFavorite); - // void (isFavorite ? removeFromFavorites : addToFavorites)( - // file, - // ).catch((e: unknown) => { - // log.error("Failed to remove favorite", e); - // onMarkUnsyncedFavoriteUpdate(file.id, undefined); - // }); }, - [showModifyActions, onGenericError, favoriteFileIDs], + [onGenericError, isFavorite], ); // Initial value of delegate. From c1a2f226ef83b22853587962f5e838c92a358b37 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 14:13:34 +0530 Subject: [PATCH 20/40] Move --- .../new/photos/services/{collection/index.ts => collection.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename web/packages/new/photos/services/{collection/index.ts => collection.ts} (100%) diff --git a/web/packages/new/photos/services/collection/index.ts b/web/packages/new/photos/services/collection.ts similarity index 100% rename from web/packages/new/photos/services/collection/index.ts rename to web/packages/new/photos/services/collection.ts From 0f732c0b61e8bde7ee1cc4d0701c60d850083a70 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 14:37:30 +0530 Subject: [PATCH 21/40] Impl --- web/apps/photos/src/components/PhotoFrame.tsx | 18 ++++++- .../gallery/components/viewer/FileViewer.tsx | 50 +++++++------------ 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index a18242a46c..56a5b5a4eb 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -21,8 +21,12 @@ import { t } from "i18next"; import { useRouter } from "next/router"; import { GalleryContext } from "pages/gallery"; import PhotoSwipe from "photoswipe"; -import { useCallback, useContext, useEffect, useState } from "react"; +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import AutoSizer from "react-virtualized-auto-sizer"; +import { + addToFavorites, + removeFromFavorites, +} from "services/collectionService"; import uploadManager from "services/upload/uploadManager"; import { SelectedState, @@ -266,6 +270,17 @@ const PhotoFrame = ({ [onSyncWithRemote], ); + const handleToggleFavorite = useMemo(() => { + return favoriteFileIDs + ? (file: EnteFile) => { + const isFavorite = favoriteFileIDs!.has(file.id); + return (isFavorite ? removeFromFavorites : addToFavorites)( + file, + ); + } + : undefined; + }, [favoriteFileIDs]); + const handleSaveEditedImageCopy = useCallback( (editedFile: File, collection: Collection, enteFile: EnteFile) => { uploadManager.prepareForNewUpload(); @@ -557,6 +572,7 @@ const PhotoFrame = ({ isInHiddenSection={isInHiddenSection} isInTrashSection={activeCollectionID === TRASH_SECTION} onTriggerSyncWithRemote={handleTriggerSyncWithRemote} + onToggleFavorite={handleToggleFavorite} onSaveEditedImageCopy={handleSaveEditedImageCopy} {...{ favoriteFileIDs, diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index 55800ab218..1a8244c39a 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -14,7 +14,6 @@ if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { } import { isDesktop } from "@/base/app"; -import { assertionFailed } from "@/base/assert"; import { type ModalVisibilityProps } from "@/base/components/utils/modal"; import { useBaseContext } from "@/base/context"; import { lowercaseExtension } from "@/base/file-name"; @@ -43,19 +42,6 @@ import { type FileViewerPhotoSwipeDelegate, } from "./photoswipe"; -// TODO -// import { -// addToFavorites, -// removeFromFavorites, -// } from "apps/photos/services/collectionService"; -// import { wait } from "@/utils/promise"; -// const addToFavorites = async (file: EnteFile) => { -// console.log(file); -// await wait(3000); -// throw new Error("test"); -// }; -// const removeFromFavorites = addToFavorites; - export type FileViewerProps = ModalVisibilityProps & { /** * The currently logged in user, if any. @@ -123,6 +109,16 @@ export type FileViewerProps = ModalVisibilityProps & { * not defined, then this prop is not used. */ onTriggerSyncWithRemote?: () => void; + /** + * Called when the favorite status of given {@link file} should be toggled + * from its current value. + * + * If this is not provided then the favorite toggle button will not be shown + * in the file actions. + * + * See also {@link favoriteFileIDs}. + */ + onToggleFavorite?: (file: EnteFile) => Promise; /** * Called when the user edits an image in the image editor and asks us to * save their edits as a copy. @@ -157,6 +153,7 @@ const FileViewer: React.FC = ({ fileCollectionIDs, allCollectionsNameByID, onTriggerSyncWithRemote, + onToggleFavorite, onSelectCollection, onSelectPerson, onSaveEditedImageCopy, @@ -299,29 +296,18 @@ const FileViewer: React.FC = ({ const isFavorite = useCallback( ({ file }: FileViewerAnnotatedFile) => { - if (!showModifyActions || !favoriteFileIDs) return undefined; + if (!showModifyActions || !favoriteFileIDs || !onToggleFavorite) { + return undefined; + } return favoriteFileIDs.has(file.id); }, - [showModifyActions, favoriteFileIDs], + [showModifyActions, favoriteFileIDs, onToggleFavorite], ); const toggleFavorite = useCallback( - async (annotatedFile: FileViewerAnnotatedFile) => { - const { file, annotation } = annotatedFile; - - const isFav = isFavorite(annotatedFile); - if (isFav === undefined || !annotation.showFavorite) { - assertionFailed(); - return; - } - - try { - await (isFav ? removeFromFavorites : addToFavorites)(file); - } catch (e) { - onGenericError(e); - } - }, - [onGenericError, isFavorite], + ({ file }: FileViewerAnnotatedFile) => + onToggleFavorite!(file).catch(onGenericError), + [onToggleFavorite, onGenericError], ); // Initial value of delegate. From df21d20dd0424f627edd367b36412d71fa3eb591 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 15:21:00 +0530 Subject: [PATCH 22/40] Workable --- web/apps/photos/src/components/PhotoFrame.tsx | 7 ++++--- .../gallery/components/viewer/photoswipe.ts | 21 ++++++++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index 56a5b5a4eb..959395833e 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -272,14 +272,15 @@ const PhotoFrame = ({ const handleToggleFavorite = useMemo(() => { return favoriteFileIDs - ? (file: EnteFile) => { + ? async (file: EnteFile) => { const isFavorite = favoriteFileIDs!.has(file.id); - return (isFavorite ? removeFromFavorites : addToFavorites)( + await (isFavorite ? removeFromFavorites : addToFavorites)( file, ); + onMarkUnsyncedFavoriteUpdate(file.id, !isFavorite); } : undefined; - }, [favoriteFileIDs]); + }, [favoriteFileIDs, onMarkUnsyncedFavoriteUpdate]); const handleSaveEditedImageCopy = useCallback( (editedFile: File, collection: Collection, enteFile: EnteFile) => { diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index d7f63e69d3..a4087157ce 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -499,14 +499,29 @@ export class FileViewerPhotoSwipe { if (showModifyActions) { const toggleFavorite = async ( buttonElement: HTMLButtonElement, - value: boolean, ) => { const af = currentAnnotatedFile(); this.pendingFavoriteUpdates.add(af.file.id); buttonElement.disabled = true; await delegate.toggleFavorite(af); + // TODO: This can be improved in two ways: + // + // 1. We currently have a setTimeout to ensure that the + // updated `favoriteFileIDs` have made their way to our + // delegate before we query for the status again. + // Obviously, this is hacky. Note that a timeout of 0 + // (i.e., just deferring till the next tick) isn't enough + // here, for reasons I need to investigate more (hence + // this TODO). + // + // 2. We reload the entire slide instead of just updating + // the button state. This is because there are two + // buttons, instead of a single button toggling between + // two states (e.g. like the zoom button). A single + // button can be achieved by moving the fill as a layer. + await new Promise((r) => setTimeout(r, 100)); this.pendingFavoriteUpdates.delete(af.file.id); - showFavoriteIf(buttonElement, value); + this.refreshCurrentSlideContent(); }; const showFavoriteIf = ( @@ -529,7 +544,7 @@ export class FileViewerPhotoSwipe { order: 8, isButton: true, html: createPSRegisterElementIconHTML("favorite"), - onClick: (e) => toggleFavorite(e.target, false), + onClick: (e) => toggleFavorite(e.target), onInit: (buttonElement) => pswp.on("change", () => showFavoriteIf(buttonElement, false), From de9cad09c4eeedda00f88548d3d8c3ef3f2b69cf Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 15:28:43 +0530 Subject: [PATCH 23/40] sigh --- web/apps/photos/src/components/PhotoFrame.tsx | 1 + .../photos/src/services/collectionService.ts | 25 ++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index 959395833e..c779b8412d 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -276,6 +276,7 @@ const PhotoFrame = ({ const isFavorite = favoriteFileIDs!.has(file.id); await (isFavorite ? removeFromFavorites : addToFavorites)( file, + true, ); onMarkUnsyncedFavoriteUpdate(file.id, !isFavorite); } diff --git a/web/apps/photos/src/services/collectionService.ts b/web/apps/photos/src/services/collectionService.ts index 2e9250b67a..211021c33b 100644 --- a/web/apps/photos/src/services/collectionService.ts +++ b/web/apps/photos/src/services/collectionService.ts @@ -139,11 +139,17 @@ export const createFavoritesCollection = () => { return createCollection(FAVORITE_COLLECTION_NAME, CollectionType.favorites); }; -export const addToFavorites = async (file: EnteFile) => { - await addMultipleToFavorites([file]); +export const addToFavorites = async ( + file: EnteFile, + disableOldWorkaround?: boolean, +) => { + await addMultipleToFavorites([file], disableOldWorkaround); }; -export const addMultipleToFavorites = async (files: EnteFile[]) => { +export const addMultipleToFavorites = async ( + files: EnteFile[], + disableOldWorkaround?: boolean, +) => { try { let favCollection = await getFavCollection(); if (!favCollection) { @@ -152,10 +158,19 @@ export const addMultipleToFavorites = async (files: EnteFile[]) => { await addToCollection(favCollection, files); } catch (e) { log.error("failed to add to favorite", e); + // Old code swallowed the error here. This isn't good, but to + // avoid changing existing behaviour only new code will set the + // disableOldWorkaround flag to instead rethrow it. + // + // TODO: Migrate old code, remove this flag, always throw. + if (disableOldWorkaround) throw e; } }; -export const removeFromFavorites = async (file: EnteFile) => { +export const removeFromFavorites = async ( + file: EnteFile, + disableOldWorkaround?: boolean, +) => { try { const favCollection = await getFavCollection(); if (!favCollection) { @@ -164,6 +179,8 @@ export const removeFromFavorites = async (file: EnteFile) => { await removeFromCollection(favCollection.id, [file]); } catch (e) { log.error("remove from favorite failed", e); + // TODO: See disableOldWorkaround in addMultipleToFavorites. + if (disableOldWorkaround) throw e; } }; From 67dc1b77d433a5cbaf3cc4adeab5e6f675a816b7 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 15:32:33 +0530 Subject: [PATCH 24/40] Fix --- web/packages/gallery/components/viewer/photoswipe.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index a4087157ce..a421309941 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -530,7 +530,10 @@ export class FileViewerPhotoSwipe { ) => { const af = currentAnnotatedFile(); const isFavorite = delegate.isFavorite(af); - showIf(buttonElement, isFavorite === value); + showIf( + buttonElement, + af.annotation.showFavorite && isFavorite === value, + ); buttonElement.disabled = this.pendingFavoriteUpdates.has( af.file.id, ); From dce95ad4f1183438a9b5be45e77a18610edc542d Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 15:49:00 +0530 Subject: [PATCH 25/40] more --- .../gallery/components/viewer/FileViewer.tsx | 11 +++++++++++ .../gallery/components/viewer/photoswipe.ts | 13 ++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index 1a8244c39a..30afccb4a3 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -266,6 +266,16 @@ const FileViewer: React.FC = ({ : undefined; }, [onSelectPerson, handleClose]); + const handleMore = useCallback( + ( + annotatedFile: FileViewerAnnotatedFile, + buttonElement: HTMLElement, + ) => { + console.log({ annotatedFile, buttonElement }); + }, + [], + ); + const handleEditImage = useMemo(() => { return onSaveEditedImageCopy ? (annotatedFile: FileViewerAnnotatedFile) => { @@ -336,6 +346,7 @@ const FileViewer: React.FC = ({ onClose: handleClose, onAnnotate: handleAnnotate, onViewInfo: handleViewInfo, + onMore: handleMore, onEditImage: handleEditImage, }); diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index a421309941..8d60b8dbda 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -135,6 +135,16 @@ type FileViewerPhotoSwipeOptions = Pick< * Called when the user activates the info action on a file. */ onViewInfo: (annotatedFile: FileViewerAnnotatedFile) => void; + /** + * Called when the user activates the more action on a file. + * + * In addition to the file, callback is also passed a reference to the HTML + * DOM more button element. + */ + onMore: ( + annotatedFile: FileViewerAnnotatedFile, + buttonElement: HTMLElement, + ) => void; /** * Called when the user activates the edit action on an image. * @@ -228,6 +238,7 @@ export class FileViewerPhotoSwipe { onClose, onAnnotate, onViewInfo, + onMore, onEditImage, }: FileViewerPhotoSwipeOptions) { this.opts = { disableDownload }; @@ -605,7 +616,7 @@ export class FileViewerPhotoSwipe { order: 17, isButton: true, html: createPSRegisterElementIconHTML("more"), - // onClick: withCurrentAnnotatedFile(delegate.onViewInfo), + onClick: (e) => onMore(currentAnnotatedFile(), e.target), }); }); From 5adac095a61fe61607f7afc1e9d9a3920e71d590 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 15:59:35 +0530 Subject: [PATCH 26/40] Anchor --- .../gallery/components/viewer/FileViewer.tsx | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index 30afccb4a3..dc70c52075 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -32,7 +32,7 @@ import { ImageEditorOverlay, type ImageEditorOverlayProps, } from "@/new/photos/components/ImageEditorOverlay"; -import { Button, styled } from "@mui/material"; +import { Button, Menu, styled } from "@mui/material"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { fileInfoExifForFile } from "./data-source"; import { @@ -200,6 +200,9 @@ const FileViewer: React.FC = ({ >(undefined); const [openFileInfo, setOpenFileInfo] = useState(false); + const [moreMenuAnchorEl, setMoreMenuAnchorEl] = useState< + HTMLElement | undefined + >(undefined); const [openImageEditor, setOpenImageEditor] = useState(false); // If `true`, then we need to trigger a sync with remote when we close. @@ -245,7 +248,7 @@ const FileViewer: React.FC = ({ [], ); - const handleInfoClose = useCallback(() => setOpenFileInfo(false), []); + const handleFileInfoClose = useCallback(() => setOpenFileInfo(false), []); const handleScheduleUpdate = useCallback(() => setNeedsSync(true), []); @@ -271,11 +274,17 @@ const FileViewer: React.FC = ({ annotatedFile: FileViewerAnnotatedFile, buttonElement: HTMLElement, ) => { - console.log({ annotatedFile, buttonElement }); + setActiveAnnotatedFile(annotatedFile); + setMoreMenuAnchorEl(buttonElement); }, [], ); + const handleMoreMenuClose = useCallback( + () => setMoreMenuAnchorEl(undefined), + [], + ); + const handleEditImage = useMemo(() => { return onSaveEditedImageCopy ? (annotatedFile: FileViewerAnnotatedFile) => { @@ -368,6 +377,7 @@ const FileViewer: React.FC = ({ handleClose, handleAnnotate, handleViewInfo, + handleMore, handleEditImage, ]); @@ -382,7 +392,7 @@ const FileViewer: React.FC = ({ = ({ onSelectPerson={handleSelectPerson} {...{ fileCollectionIDs, allCollectionsNameByID }} /> + + Test + Date: Fri, 28 Feb 2025 16:02:55 +0530 Subject: [PATCH 27/40] type --- web/packages/gallery/components/viewer/FileViewer.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index dc70c52075..30821b1908 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -200,9 +200,8 @@ const FileViewer: React.FC = ({ >(undefined); const [openFileInfo, setOpenFileInfo] = useState(false); - const [moreMenuAnchorEl, setMoreMenuAnchorEl] = useState< - HTMLElement | undefined - >(undefined); + const [moreMenuAnchorEl, setMoreMenuAnchorEl] = + useState(null); const [openImageEditor, setOpenImageEditor] = useState(false); // If `true`, then we need to trigger a sync with remote when we close. @@ -281,7 +280,7 @@ const FileViewer: React.FC = ({ ); const handleMoreMenuClose = useCallback( - () => setMoreMenuAnchorEl(undefined), + () => setMoreMenuAnchorEl(null), [], ); From 805042231356a214091f6cc9673216f17108b029 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 16:32:11 +0530 Subject: [PATCH 28/40] Fix --- web/apps/photos/src/pages/shared-albums.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 1e52800a82..97a8374827 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -60,7 +60,7 @@ import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList"; import { Upload } from "components/Upload"; import { t } from "i18next"; import { useRouter } from "next/router"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { type FileWithPath } from "react-dropzone"; import { getLocalPublicCollection, @@ -289,7 +289,7 @@ export default function PublicCollectionGallery() { ); }, [onAddPhotos]); - const syncWithRemote = async () => { + const handleSyncWithRemote = useCallback(async () => { const collectionUID = getPublicCollectionUID( credentials.current.accessToken, ); @@ -369,7 +369,10 @@ export default function PublicCollectionGallery() { hideLoadingBar(); setLoading(false); } - }; + }, [showLoadingBar, hideLoadingBar]); + + // TODO: See gallery + const syncWithRemote = handleSyncWithRemote; const verifyLinkPassword: SingleInputFormProps["callback"] = async ( password, @@ -510,7 +513,7 @@ export default function PublicCollectionGallery() { Date: Fri, 28 Feb 2025 16:21:09 +0530 Subject: [PATCH 29/40] Update deps to pick up new MUI MenuListProps are (will eventually be) deprecated, but the alternative is only in the latest release. --- web/packages/base/components/utils/theme.ts | 2 +- web/packages/base/package.json | 16 +- web/packages/new/package.json | 6 +- web/yarn.lock | 222 +++++++++++--------- 4 files changed, 129 insertions(+), 117 deletions(-) diff --git a/web/packages/base/components/utils/theme.ts b/web/packages/base/components/utils/theme.ts index 04c38e9859..960637e135 100644 --- a/web/packages/base/components/utils/theme.ts +++ b/web/packages/base/components/utils/theme.ts @@ -41,7 +41,7 @@ const getTheme = (appName: AppName): Theme => { * * 2. These can be groups of color values that have roughly the same hue, but * different levels of saturation. Such hue groups are arranged together into - * a "Colors" exported by "@/mui/material": + * a "Colors" exported by "@mui/material": * * export interface Color { * 50: string; diff --git a/web/packages/base/package.json b/web/packages/base/package.json index a700cab88b..566fccca9d 100644 --- a/web/packages/base/package.json +++ b/web/packages/base/package.json @@ -7,8 +7,8 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@fontsource-variable/inter": "^5.1.1", - "@mui/icons-material": "^6.4.3", - "@mui/material": "^6.4.3", + "@mui/icons-material": "^6.4.6", + "@mui/material": "^6.4.6", "comlink": "^4.4.2", "formik": "^2.4.6", "get-user-locale": "^2.3.2", @@ -16,18 +16,18 @@ "i18next-resources-to-backend": "^1.2.1", "idb": "^8.0.2", "libsodium-wrappers-sumo": "^0.7.15", - "nanoid": "^5.0.9", - "next": "^15.1.6", + "nanoid": "^5.1.2", + "next": "^15.2.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-i18next": "^15.4.0", + "react-i18next": "^15.4.1", "yup": "^1.6.1", - "zod": "^3.24.1" + "zod": "^3.24.2" }, "devDependencies": { "@/build-config": "*", "@types/libsodium-wrappers-sumo": "^0.7.8", - "@types/react": "^19.0.8", - "@types/react-dom": "^19.0.3" + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4" } } diff --git a/web/packages/new/package.json b/web/packages/new/package.json index 860c93a9a9..e3d807db95 100644 --- a/web/packages/new/package.json +++ b/web/packages/new/package.json @@ -7,9 +7,9 @@ "@/gallery": "*", "@/utils": "*", "@ente/shared": "*", - "@mui/material": "^6.4.3", - "@mui/system": "^6.4.3", - "@mui/x-date-pickers": "^7.25.0", + "@mui/material": "^6.4.6", + "@mui/system": "^6.4.6", + "@mui/x-date-pickers": "^7.27.1", "dayjs": "^1.11.13", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/web/yarn.lock b/web/yarn.lock index faab8e0c40..70cb2f7127 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -684,28 +684,28 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@mui/core-downloads-tracker@^6.4.3": - version "6.4.3" - resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.3.tgz#aa9506e4d43e02d61fb82e23de9741fd24b22b61" - integrity sha512-hlyOzo2ObarllAOeT1ZSAusADE5NZNencUeIvXrdQ1Na+FL1lcznhbxfV5He1KqGiuR8Az3xtCUcYKwMVGFdzg== +"@mui/core-downloads-tracker@^6.4.6": + version "6.4.6" + resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.6.tgz#42820be160159df81976f467ce496f5310e08eb1" + integrity sha512-rho5Q4IscbrVmK9rCrLTJmjLjfH6m/NcqKr/mchvck0EIXlyYUB9+Z0oVmkt/+Mben43LMRYBH8q/Uzxj/c4Vw== -"@mui/icons-material@^6.4.3": - version "6.4.3" - resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-6.4.3.tgz#11f275781f442f864ca5522a6196a0a17cc063ea" - integrity sha512-3IY9LpjkwIJVgL/SkZQKKCUcumdHdQEsJaIavvsQze2QEztBt0HJ17naToN0DBBdhKdtwX5xXrfD6ZFUeWWk8g== +"@mui/icons-material@^6.4.6": + version "6.4.6" + resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-6.4.6.tgz#a26eaeae2f7f1359b48dac3fe8a8eec61640c325" + integrity sha512-rGJBvIQQbQAlyKYljHQ8wAQS/K2/uYwvemcpygnAmCizmCI4zSF9HQPuiG8Ql4YLZ6V/uKjA3WHIYmF/8sV+pQ== dependencies: "@babel/runtime" "^7.26.0" -"@mui/material@^6.4.3": - version "6.4.3" - resolved "https://registry.yarnpkg.com/@mui/material/-/material-6.4.3.tgz#18cb003b6526bf4b3ad6f2bb46ab53154a51b19f" - integrity sha512-ubtQjplbWneIEU8Y+4b2VA0CDBlyH5I3AmVFGmsLyDe/bf0ubxav5t11c8Afem6rkSFWPlZA2DilxmGka1xiKQ== +"@mui/material@^6.4.6": + version "6.4.6" + resolved "https://registry.yarnpkg.com/@mui/material/-/material-6.4.6.tgz#6114a02977735d70170243efc487aed0ca974197" + integrity sha512-6UyAju+DBOdMogfYmLiT3Nu7RgliorimNBny1pN/acOjc+THNFVE7hlxLyn3RDONoZJNDi/8vO4AQQr6dLAXqA== dependencies: "@babel/runtime" "^7.26.0" - "@mui/core-downloads-tracker" "^6.4.3" - "@mui/system" "^6.4.3" + "@mui/core-downloads-tracker" "^6.4.6" + "@mui/system" "^6.4.6" "@mui/types" "^7.2.21" - "@mui/utils" "^6.4.3" + "@mui/utils" "^6.4.6" "@popperjs/core" "^2.11.8" "@types/react-transition-group" "^4.4.12" clsx "^2.1.1" @@ -714,19 +714,19 @@ react-is "^19.0.0" react-transition-group "^4.4.5" -"@mui/private-theming@^6.4.3": - version "6.4.3" - resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-6.4.3.tgz#40d7d95316e9e52d465f0c96da23f9fb8f6a989f" - integrity sha512-7x9HaNwDCeoERc4BoEWLieuzKzXu5ZrhRnEM6AUcRXUScQLvF1NFkTlP59+IJfTbEMgcGg1wWHApyoqcksrBpQ== +"@mui/private-theming@^6.4.6": + version "6.4.6" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-6.4.6.tgz#77c0b150be94c061b34b34ce00eb60cdfb92837f" + integrity sha512-T5FxdPzCELuOrhpA2g4Pi6241HAxRwZudzAuL9vBvniuB5YU82HCmrARw32AuCiyTfWzbrYGGpZ4zyeqqp9RvQ== dependencies: "@babel/runtime" "^7.26.0" - "@mui/utils" "^6.4.3" + "@mui/utils" "^6.4.6" prop-types "^15.8.1" -"@mui/styled-engine@^6.4.3": - version "6.4.3" - resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-6.4.3.tgz#fbd7a6b925dfaeaa84ffbf8ed9be78a0ff0b3d6e" - integrity sha512-OC402VfK+ra2+f12Gef8maY7Y9n7B6CZcoQ9u7mIkh/7PKwW/xH81xwX+yW+Ak1zBT3HYcVjh2X82k5cKMFGoQ== +"@mui/styled-engine@^6.4.6": + version "6.4.6" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-6.4.6.tgz#cd0783adbb066a349e1995f0e1a7b8c3c2d59738" + integrity sha512-vSWYc9ZLX46be5gP+FCzWVn5rvDr4cXC5JBZwSIkYk9xbC7GeV+0kCvB8Q6XLFQJy+a62bbqtmdwS4Ghi9NBlQ== dependencies: "@babel/runtime" "^7.26.0" "@emotion/cache" "^11.13.5" @@ -735,16 +735,16 @@ csstype "^3.1.3" prop-types "^15.8.1" -"@mui/system@^6.4.3": - version "6.4.3" - resolved "https://registry.yarnpkg.com/@mui/system/-/system-6.4.3.tgz#f1e093850c8cc23c6605297c8a4134bea6fe290b" - integrity sha512-Q0iDwnH3+xoxQ0pqVbt8hFdzhq1g2XzzR4Y5pVcICTNtoCLJmpJS3vI4y/OIM1FHFmpfmiEC2IRIq7YcZ8nsmg== +"@mui/system@^6.4.6": + version "6.4.6" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-6.4.6.tgz#f7078e403bd11377c539a05829cd71c441c8b6e6" + integrity sha512-FQjWwPec7pMTtB/jw5f9eyLynKFZ6/Ej9vhm5kGdtmts1z5b7Vyn3Rz6kasfYm1j2TfrfGnSXRvvtwVWxjpz6g== dependencies: "@babel/runtime" "^7.26.0" - "@mui/private-theming" "^6.4.3" - "@mui/styled-engine" "^6.4.3" + "@mui/private-theming" "^6.4.6" + "@mui/styled-engine" "^6.4.6" "@mui/types" "^7.2.21" - "@mui/utils" "^6.4.3" + "@mui/utils" "^6.4.6" clsx "^2.1.1" csstype "^3.1.3" prop-types "^15.8.1" @@ -771,10 +771,10 @@ prop-types "^15.8.1" react-is "^19.0.0" -"@mui/utils@^6.4.3": - version "6.4.3" - resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.4.3.tgz#e08bc3a5ae1552a48dd13ddc7c65e3eebdb4cd58" - integrity sha512-jxHRHh3BqVXE9ABxDm+Tc3wlBooYz/4XPa0+4AI+iF38rV1/+btJmSUgG4shDtSWVs/I97aDn5jBCt6SF2Uq2A== +"@mui/utils@^6.4.6": + version "6.4.6" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.4.6.tgz#307828bee501d30ed5cd1e339ca28c9efcc4e3f9" + integrity sha512-43nZeE1pJF2anGafNydUcYFPtHwAqiBiauRtaMvurdrZI3YrUjHkAu43RBsxef7OFtJMXGiHFvq43kb7lig0sA== dependencies: "@babel/runtime" "^7.26.0" "@mui/types" "^7.2.21" @@ -783,71 +783,71 @@ prop-types "^15.8.1" react-is "^19.0.0" -"@mui/x-date-pickers@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.25.0.tgz#3cbd6733e84061bf1643571248de217f24075027" - integrity sha512-t62OSFAKwj7KYQ8KcwTuKj6OgDuLQPSe4QUJcKDzD9rEhRIJVRUw2x27gBSdcls4l0PTrba19TghvDxCZprriw== +"@mui/x-date-pickers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.27.1.tgz#5c9769744598b1e10c1919ce0589b8d235131a17" + integrity sha512-2YPhTM9TM39dmIkEQdSB6P6NASePB9LuhXXKQqq0PX4FXGymYEPz/acQXkk617zwfxJJaDhJZ6g8SAv5pklTJQ== dependencies: "@babel/runtime" "^7.25.7" "@mui/utils" "^5.16.6 || ^6.0.0" - "@mui/x-internals" "7.25.0" + "@mui/x-internals" "7.26.0" "@types/react-transition-group" "^4.4.11" clsx "^2.1.1" prop-types "^15.8.1" react-transition-group "^4.4.5" -"@mui/x-internals@7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.25.0.tgz#981a5365391190ab3aa0e9c7a346315b3d1f621c" - integrity sha512-tBUN54YznAkmtCIRAOl35Kgl0MjFDIjUbzIrbWRgVSIR3QJ8bXnVSkiRBi+P91SZEl9+ZW0rDj+osq7xFJV0kg== +"@mui/x-internals@7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.26.0.tgz#e8c3060582c102127ab55b0a93e881930dac107b" + integrity sha512-VxTCYQcZ02d3190pdvys2TDg9pgbvewAVakEopiOgReKAUhLdRlgGJHcOA/eAuGLyK1YIo26A6Ow6ZKlSRLwMg== dependencies: "@babel/runtime" "^7.25.7" "@mui/utils" "^5.16.6 || ^6.0.0" -"@next/env@15.1.6": - version "15.1.6" - resolved "https://registry.yarnpkg.com/@next/env/-/env-15.1.6.tgz#2fa863d8c568a56b1c8328a86e621b8bdd4f2a20" - integrity sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w== +"@next/env@15.2.0": + version "15.2.0" + resolved "https://registry.yarnpkg.com/@next/env/-/env-15.2.0.tgz#4c3508ca2c0bb2bc324066818bb8d0415f767641" + integrity sha512-eMgJu1RBXxxqqnuRJQh5RozhskoNUDHBFybvi+Z+yK9qzKeG7dadhv/Vp1YooSZmCnegf7JxWuapV77necLZNA== -"@next/swc-darwin-arm64@15.1.6": - version "15.1.6" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.6.tgz#92f99badab6cb41f4c5c11a3feffa574bd6a9276" - integrity sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw== +"@next/swc-darwin-arm64@15.2.0": + version "15.2.0" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.0.tgz#51ebba2162330ee3e8b3412bf31defd94a7b85e7" + integrity sha512-rlp22GZwNJjFCyL7h5wz9vtpBVuCt3ZYjFWpEPBGzG712/uL1bbSkS675rVAUCRZ4hjoTJ26Q7IKhr5DfJrHDA== -"@next/swc-darwin-x64@15.1.6": - version "15.1.6" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.6.tgz#f56f4f8d5f6cb5d3915912ac95590d387f897da5" - integrity sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg== +"@next/swc-darwin-x64@15.2.0": + version "15.2.0" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.0.tgz#90fd6c6cee494d4348342434cfb9ca9506eae895" + integrity sha512-DiU85EqSHogCz80+sgsx90/ecygfCSGl5P3b4XDRVZpgujBm5lp4ts7YaHru7eVTyZMjHInzKr+w0/7+qDrvMA== -"@next/swc-linux-arm64-gnu@15.1.6": - version "15.1.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.6.tgz#0aaffae519c93d1006419d7b98c34ebfd80ecacd" - integrity sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg== +"@next/swc-linux-arm64-gnu@15.2.0": + version "15.2.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.0.tgz#f10a26cdbacf2e3de2a02a926c72857b3cb613e1" + integrity sha512-VnpoMaGukiNWVxeqKHwi8MN47yKGyki5q+7ql/7p/3ifuU2341i/gDwGK1rivk0pVYbdv5D8z63uu9yMw0QhpQ== -"@next/swc-linux-arm64-musl@15.1.6": - version "15.1.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.6.tgz#e7398d3d31ca60033f708a718cd6c31edcee2e9a" - integrity sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ== +"@next/swc-linux-arm64-musl@15.2.0": + version "15.2.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.0.tgz#1821c9a1dd17c441d8182f5cefd586f7902fcdb5" + integrity sha512-ka97/ssYE5nPH4Qs+8bd8RlYeNeUVBhcnsNUmFM6VWEob4jfN9FTr0NBhXVi1XEJpj3cMfgSRW+LdE3SUZbPrw== -"@next/swc-linux-x64-gnu@15.1.6": - version "15.1.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.6.tgz#d76c72508f4d79d6016cab0c52640b93e590cffb" - integrity sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ== +"@next/swc-linux-x64-gnu@15.2.0": + version "15.2.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.0.tgz#522f55c7672346bab43bce0bcb35c4cb668ad20f" + integrity sha512-zY1JduE4B3q0k2ZCE+DAF/1efjTXUsKP+VXRtrt/rJCTgDlUyyryx7aOgYXNc1d8gobys/Lof9P9ze8IyRDn7Q== -"@next/swc-linux-x64-musl@15.1.6": - version "15.1.6" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.6.tgz#0b8ba80a53e65bf8970ed11ea923001e2512c7cb" - integrity sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ== +"@next/swc-linux-x64-musl@15.2.0": + version "15.2.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.0.tgz#e23be1d046c9a630a0315588f9d692d9705ac355" + integrity sha512-QqvLZpurBD46RhaVaVBepkVQzh8xtlUN00RlG4Iq1sBheNugamUNPuZEH1r9X1YGQo1KqAe1iiShF0acva3jHQ== -"@next/swc-win32-arm64-msvc@15.1.6": - version "15.1.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.6.tgz#81b5dbbfdada2c05deef688e799af4a24097b65f" - integrity sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg== +"@next/swc-win32-arm64-msvc@15.2.0": + version "15.2.0" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.0.tgz#9e2cb008b82c676dad7d632a43549f969cb2194f" + integrity sha512-ODZ0r9WMyylTHAN6pLtvUtQlGXBL9voljv6ujSlcsjOxhtXPI1Ag6AhZK0SE8hEpR1374WZZ5w33ChpJd5fsjw== -"@next/swc-win32-x64-msvc@15.1.6": - version "15.1.6" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.6.tgz#131993c45ffd124fb4b15258e2f3f9669c143e3c" - integrity sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ== +"@next/swc-win32-x64-msvc@15.2.0": + version "15.2.0" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.0.tgz#d280f450a5b6dbb7437c3265f81ea62febf4bf3c" + integrity sha512-8+4Z3Z7xa13NdUuUAcpVNA6o76lNPniBd9Xbo02bwXQXnZgFvEopwY2at5+z7yHl47X9qbZpvwatZ2BRo3EdZw== "@noble/hashes@^1.2.0": version "1.7.0" @@ -1114,6 +1114,11 @@ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.3.tgz#0804dfd279a165d5a0ad8b53a5b9e65f338050a4" integrity sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA== +"@types/react-dom@^19.0.4": + version "19.0.4" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.4.tgz#bedba97f9346bd4c0fe5d39e689713804ec9ac89" + integrity sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg== + "@types/react-transition-group@^4.4.0", "@types/react-transition-group@^4.4.11", "@types/react-transition-group@^4.4.12": version "4.4.12" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" @@ -1140,6 +1145,13 @@ dependencies: csstype "^3.0.2" +"@types/react@^19.0.10": + version "19.0.10" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.10.tgz#d0c66dafd862474190fe95ce11a68de69ed2b0eb" + integrity sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g== + dependencies: + csstype "^3.0.2" + "@types/react@^19.0.8": version "19.0.8" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.8.tgz#7098e6159f2a61e4f4cef2c1223c044a9bec590e" @@ -2972,22 +2984,22 @@ nanoid@^3.3.6, nanoid@^3.3.7: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== -nanoid@^5.0.9: - version "5.0.9" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.9.tgz#977dcbaac055430ce7b1e19cf0130cea91a20e50" - integrity sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q== +nanoid@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.1.2.tgz#b87c6cb6941d127a23b24dffc4659bba48b219d7" + integrity sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g== natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -next@^15.1.6: - version "15.1.6" - resolved "https://registry.yarnpkg.com/next/-/next-15.1.6.tgz#ce22fd0a8f36da1fc4aba86e3ec7e98eb248c555" - integrity sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q== +next@^15.2.0: + version "15.2.0" + resolved "https://registry.yarnpkg.com/next/-/next-15.2.0.tgz#00f4619ae4322102b08c1a8bf315f7b757525508" + integrity sha512-VaiM7sZYX8KIAHBrRGSFytKknkrexNfGb8GlG6e93JqueCspuGte8i4ybn8z4ww1x3f2uzY4YpTaBEW4/hvsoQ== dependencies: - "@next/env" "15.1.6" + "@next/env" "15.2.0" "@swc/counter" "0.1.3" "@swc/helpers" "0.5.15" busboy "1.6.0" @@ -2995,14 +3007,14 @@ next@^15.1.6: postcss "8.4.31" styled-jsx "5.1.6" optionalDependencies: - "@next/swc-darwin-arm64" "15.1.6" - "@next/swc-darwin-x64" "15.1.6" - "@next/swc-linux-arm64-gnu" "15.1.6" - "@next/swc-linux-arm64-musl" "15.1.6" - "@next/swc-linux-x64-gnu" "15.1.6" - "@next/swc-linux-x64-musl" "15.1.6" - "@next/swc-win32-arm64-msvc" "15.1.6" - "@next/swc-win32-x64-msvc" "15.1.6" + "@next/swc-darwin-arm64" "15.2.0" + "@next/swc-darwin-x64" "15.2.0" + "@next/swc-linux-arm64-gnu" "15.2.0" + "@next/swc-linux-arm64-musl" "15.2.0" + "@next/swc-linux-x64-gnu" "15.2.0" + "@next/swc-linux-x64-musl" "15.2.0" + "@next/swc-win32-arm64-msvc" "15.2.0" + "@next/swc-win32-x64-msvc" "15.2.0" sharp "^0.33.5" node-releases@^2.0.19: @@ -3289,10 +3301,10 @@ react-fast-compare@^2.0.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== -react-i18next@^15.4.0: - version "15.4.0" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.4.0.tgz#87c755fb6d7a567eec134e4759b022a0baacb19e" - integrity sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw== +react-i18next@^15.4.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.4.1.tgz#33f3e89c2f6c68e2bfcbf9aa59986ad42fe78758" + integrity sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw== dependencies: "@babel/runtime" "^7.25.0" html-parse-stringify "^3.0.1" @@ -4199,10 +4211,10 @@ yup@^1.6.1: toposort "^2.0.2" type-fest "^2.19.0" -zod@^3.24.1: - version "3.24.1" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee" - integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A== +zod@^3.24.2: + version "3.24.2" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3" + integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ== zxcvbn@^4.4.2: version "4.4.2" From 14bfaf97f3d6b2544e5293715f32b95d19adba74 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 16:32:30 +0530 Subject: [PATCH 30/40] Update deprecated --- web/apps/auth/src/pages/auth.tsx | 15 ++++++++----- .../Collections/CollectionHeader.tsx | 8 ++++--- web/packages/base/components/OverflowMenu.tsx | 12 +++++----- .../new/photos/components/DropdownInput.tsx | 22 +++++++++---------- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx index 8e03adf9f0..9f702136a2 100644 --- a/web/apps/auth/src/pages/auth.tsx +++ b/web/apps/auth/src/pages/auth.tsx @@ -217,12 +217,15 @@ const CodeDisplay: React.FC = ({ code }) => { ({ - backgroundColor: theme.vars.palette.fill.faint, - color: theme.vars.palette.primary.main, - backdropFilter: "blur(10px)", - }), + slotProps={{ + content: { + sx: (theme) => ({ + backgroundColor: + theme.vars.palette.fill.faint, + color: theme.vars.palette.primary.main, + backdropFilter: "blur(10px)", + }), + }, }} /> diff --git a/web/apps/photos/src/components/Collections/CollectionHeader.tsx b/web/apps/photos/src/components/Collections/CollectionHeader.tsx index 4935dea01f..0e59cabb25 100644 --- a/web/apps/photos/src/components/Collections/CollectionHeader.tsx +++ b/web/apps/photos/src/components/Collections/CollectionHeader.tsx @@ -717,9 +717,11 @@ const CollectionSortOrderMenu: React.FC = ({ anchorEl={overFlowMenuIconRef.current} open={open} onClose={onClose} - MenuListProps={{ - disablePadding: true, - "aria-labelledby": "collection-files-sort", + slotProps={{ + list: { + disablePadding: true, + "aria-labelledby": "collection-files-sort", + }, }} anchorOrigin={{ vertical: "bottom", diff --git a/web/packages/base/components/OverflowMenu.tsx b/web/packages/base/components/OverflowMenu.tsx index b3a474c92b..8f61ba9cbf 100644 --- a/web/packages/base/components/OverflowMenu.tsx +++ b/web/packages/base/components/OverflowMenu.tsx @@ -74,12 +74,14 @@ export const OverflowMenu: React.FC< {...(anchorEl ? { anchorEl } : {})} open={!!anchorEl} onClose={() => setAnchorEl(undefined)} - MenuListProps={{ - // Disable padding at the top and bottom of the menu list. - disablePadding: true, - "aria-labelledby": ariaID, + slotProps={{ + paper: { sx: menuPaperSxProps }, + list: { + // Disable padding at the top and bottom of the menu list. + disablePadding: true, + "aria-labelledby": ariaID, + }, }} - slotProps={{ paper: { sx: menuPaperSxProps } }} anchorOrigin={{ vertical: "bottom", horizontal: "right" }} transformOrigin={{ vertical: "top", horizontal: "right" }} > diff --git a/web/packages/new/photos/components/DropdownInput.tsx b/web/packages/new/photos/components/DropdownInput.tsx index af095dfcfb..5522028ace 100644 --- a/web/packages/new/photos/components/DropdownInput.tsx +++ b/web/packages/new/photos/components/DropdownInput.tsx @@ -73,17 +73,17 @@ export const DropdownInput = ({ // maxWidth to 0 forces element widths to equal minWidth. sx: { maxWidth: 0 }, }, - }, - MenuListProps: { - sx: { - backgroundColor: "background.paper2", - ".MuiMenuItem-root": { - color: "text.faint", - whiteSpace: "normal", - }, - // Make the selected item pop out by using color. - "&&& > .Mui-selected": { - color: "text.base", + list: { + sx: { + backgroundColor: "background.paper2", + ".MuiMenuItem-root": { + color: "text.faint", + whiteSpace: "normal", + }, + // Make the selected item pop out by using color. + "&&& > .Mui-selected": { + color: "text.base", + }, }, }, }, From a73e7af704980fe4a5b3e6735e511a408f740c24 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 16:42:50 +0530 Subject: [PATCH 31/40] Don't initialize face DB in non-desktop builds --- web/packages/gallery/components/FileInfo.tsx | 3 ++- web/packages/new/photos/services/ml/index.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/web/packages/gallery/components/FileInfo.tsx b/web/packages/gallery/components/FileInfo.tsx index ec49c17b17..40d8d3e44d 100644 --- a/web/packages/gallery/components/FileInfo.tsx +++ b/web/packages/gallery/components/FileInfo.tsx @@ -200,6 +200,7 @@ export const FileInfo: React.FC = ({ useEffect(() => { if (!file) return; + if (!isMLEnabled()) return; let didCancel = false; @@ -317,7 +318,7 @@ export const FileInfo: React.FC = ({ ) } /> - {isMLEnabled() && annotatedFaces.length > 0 && ( + {annotatedFaces.length > 0 && ( }> => { + if (!isMLEnabled()) return []; + const index = await savedFaceIndex(file.id); if (!index) return []; From 9d61eaa04d67a5dd25e8c46f3a33baf9a6443f99 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 16:50:07 +0530 Subject: [PATCH 32/40] :\ --- web/packages/base/next.config.base.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/packages/base/next.config.base.js b/web/packages/base/next.config.base.js index 2c8155bf7a..594a8b9a3c 100644 --- a/web/packages/base/next.config.base.js +++ b/web/packages/base/next.config.base.js @@ -104,6 +104,10 @@ if (process.env.NEXT_PUBLIC_ENTE_FAMILY_URL) { const nextConfig = { // Generate a static export when we run `next build`. output: "export", + // Instead of the nice and useful HMR indicator that used to exist prior to + // 15.2, the Next.js folks have made this a persistent "branding" indicator + // that gets in the way and needs to be disabled. + devIndicators: false, compiler: { emotion: true, }, From 02a7024cdceceb7a3afe8e8150409839bd899dea Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 16:57:24 +0530 Subject: [PATCH 33/40] id --- web/packages/gallery/components/viewer/FileViewer.tsx | 5 +++++ web/packages/gallery/components/viewer/photoswipe.ts | 3 +++ 2 files changed, 8 insertions(+) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index 30821b1908..d5952add20 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -407,6 +407,11 @@ const FileViewer: React.FC = ({ open={!!moreMenuAnchorEl} onClose={handleMoreMenuClose} anchorEl={moreMenuAnchorEl} + slotProps={{ + list: { + "aria-labelledby": moreMenuAnchorEl?.id, + }, + }} > Test diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index 8d60b8dbda..75a1b15c7f 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -617,6 +617,9 @@ export class FileViewerPhotoSwipe { isButton: true, html: createPSRegisterElementIconHTML("more"), onClick: (e) => onMore(currentAnnotatedFile(), e.target), + onInit: (buttonElement) => { + buttonElement.id = "ente-pswp-show-more"; + }, }); }); From 61779a6ca7ec101195cb12fb246e454f72d53d1d Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 18:47:42 +0530 Subject: [PATCH 34/40] More aria taken from the basic example in https://mui.com/material-ui/react-menu/ --- .../gallery/components/viewer/FileViewer.tsx | 18 ++++++---- .../gallery/components/viewer/photoswipe.ts | 35 +++++++++++++++++-- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index d5952add20..ab1b385905 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -37,6 +37,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { fileInfoExifForFile } from "./data-source"; import { FileViewerPhotoSwipe, + moreButtonID, + moreMenuID, + resetMoreMenuButtonOnMenuClose, type FileViewerAnnotatedFile, type FileViewerFileAnnotation, type FileViewerPhotoSwipeDelegate, @@ -279,10 +282,12 @@ const FileViewer: React.FC = ({ [], ); - const handleMoreMenuClose = useCallback( - () => setMoreMenuAnchorEl(null), - [], - ); + const handleMoreMenuClose = useCallback(() => { + setMoreMenuAnchorEl((el) => { + resetMoreMenuButtonOnMenuClose(el); + return null; + }); + }, []); const handleEditImage = useMemo(() => { return onSaveEditedImageCopy @@ -407,10 +412,9 @@ const FileViewer: React.FC = ({ open={!!moreMenuAnchorEl} onClose={handleMoreMenuClose} anchorEl={moreMenuAnchorEl} + id={moreMenuID} slotProps={{ - list: { - "aria-labelledby": moreMenuAnchorEl?.id, - }, + list: { "aria-labelledby": moreButtonID }, }} > Test diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index 75a1b15c7f..4fe76277d8 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -165,6 +165,21 @@ export interface FileViewerAnnotatedFile { annotation: FileViewerFileAnnotation; } +/** + * The ID that is used by the "more" action button (if one is being displayed). + * + * @see also {@link moreMenuID}. + */ +export const moreButtonID = "ente-pswp-more-button"; + +/** + * The ID this is expected to be used by the more menu that is shown in response + * to the more action button being activated. + * + * @see also {@link moreButtonID}. + */ +export const moreMenuID = "ente-pswp-more-menu"; + /** * A wrapper over {@link PhotoSwipe} to tailor its interface for use by our file * viewer. @@ -616,9 +631,16 @@ export class FileViewerPhotoSwipe { order: 17, isButton: true, html: createPSRegisterElementIconHTML("more"), - onClick: (e) => onMore(currentAnnotatedFile(), e.target), onInit: (buttonElement) => { - buttonElement.id = "ente-pswp-show-more"; + buttonElement.setAttribute("id", moreButtonID); + buttonElement.setAttribute("aria-haspopup", "true"); + }, + onClick: (e) => { + const buttonElement = e.target; + // See also: `resetMoreMenuButtonOnMenuClose`. + buttonElement.setAttribute("aria-controls", moreMenuID); + buttonElement.setAttribute("aria-expanded", true); + onMore(currentAnnotatedFile(), buttonElement); }, }); }); @@ -740,3 +762,12 @@ const createElementFromHTMLString = (htmlString: string) => { template.innerHTML = htmlString.trim(); return template.content.firstChild; }; + +/** + * Update the ARIA attributes for the button that controls the more menu when + * the menu is closed. + */ +export const resetMoreMenuButtonOnMenuClose = (buttonElement: HTMLElement) => { + buttonElement.removeAttribute("aria-controls"); + buttonElement.removeAttribute("aria-expanded"); +}; From d7587d12ed5240ddca990a695e012f363dc0fffd Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 19:04:30 +0530 Subject: [PATCH 35/40] vis --- .../gallery/components/viewer/FileViewer.tsx | 7 ++- .../gallery/components/viewer/icons.tsx | 5 -- .../gallery/components/viewer/photoswipe.ts | 56 +++++++------------ 3 files changed, 26 insertions(+), 42 deletions(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index ab1b385905..ad7247a3ea 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -17,6 +17,7 @@ import { isDesktop } from "@/base/app"; import { type ModalVisibilityProps } from "@/base/components/utils/modal"; import { useBaseContext } from "@/base/context"; import { lowercaseExtension } from "@/base/file-name"; +import { pt } from "@/base/i18n"; import type { LocalUser } from "@/base/local-user"; import log from "@/base/log"; import { @@ -32,7 +33,7 @@ import { ImageEditorOverlay, type ImageEditorOverlayProps, } from "@/new/photos/components/ImageEditorOverlay"; -import { Button, Menu, styled } from "@mui/material"; +import { Button, Menu, MenuItem, styled } from "@mui/material"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { fileInfoExifForFile } from "./data-source"; import { @@ -417,7 +418,9 @@ const FileViewer: React.FC = ({ list: { "aria-labelledby": moreButtonID }, }} > - Test + {activeAnnotatedFile?.annotation.isEditableImage && ( + {/*TODO */ pt("Edit image")} + )} onViewInfo(currentAnnotatedFile()), }); - // TODO(PS): - if (showModifyActions && onEditImage && false) { + if (showModifyActions && onEditImage) { pswp.ui.registerElement({ - name: "edit", + name: "more", // TODO(PS): - // title: t("edit_image"), - title: pt("Edit image"), + title: pt("More"), order: 16, isButton: true, - html: createPSRegisterElementIconHTML("edit"), - // TODO - // onClick: withCurrentAnnotatedFile(delegate.onEditImage), - // onInit: (buttonElement) => - // pswp.on("change", () => - // showIf( - // buttonElement, - // !!currentFileAnnotation().isEditableImage, - // ), - // ), + html: createPSRegisterElementIconHTML("more"), + onInit: (buttonElement) => { + buttonElement.setAttribute("id", moreButtonID); + buttonElement.setAttribute("aria-haspopup", "true"); + pswp.on("change", () => + showIf( + buttonElement, + !!currentFileAnnotation().isEditableImage, + ), + ); + }, + onClick: (e) => { + const buttonElement = e.target; + // See also: `resetMoreMenuButtonOnMenuClose`. + buttonElement.setAttribute("aria-controls", moreMenuID); + buttonElement.setAttribute("aria-expanded", true); + onMore(currentAnnotatedFile(), buttonElement); + }, }); } - - pswp.ui.registerElement({ - name: "more", - // TODO(PS): - title: pt("More"), - order: 17, - isButton: true, - html: createPSRegisterElementIconHTML("more"), - onInit: (buttonElement) => { - buttonElement.setAttribute("id", moreButtonID); - buttonElement.setAttribute("aria-haspopup", "true"); - }, - onClick: (e) => { - const buttonElement = e.target; - // See also: `resetMoreMenuButtonOnMenuClose`. - buttonElement.setAttribute("aria-controls", moreMenuID); - buttonElement.setAttribute("aria-expanded", true); - onMore(currentAnnotatedFile(), buttonElement); - }, - }); }); // Modify the default UI elements. From acaf1939e71ad61113d8e30ecdabe6520cc00864 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 19:23:13 +0530 Subject: [PATCH 36/40] edit --- .../gallery/components/viewer/FileViewer.tsx | 123 +++++++++--------- .../gallery/components/viewer/photoswipe.ts | 36 ++--- 2 files changed, 76 insertions(+), 83 deletions(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index ad7247a3ea..ff360ceba8 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -211,33 +211,6 @@ const FileViewer: React.FC = ({ // If `true`, then we need to trigger a sync with remote when we close. const [, setNeedsSync] = useState(false); - const handleClose = useCallback(() => { - setNeedsSync((needSync) => { - if (needSync) onTriggerSyncWithRemote?.(); - return false; - }); - setOpenFileInfo(false); - setOpenImageEditor(false); - onClose(); - }, [onTriggerSyncWithRemote, onClose]); - - const handleAnnotate = useCallback( - (file: EnteFile): FileViewerFileAnnotation => { - log.debug(() => ["viewer", { action: "annotate", file }]); - const fileID = file.id; - const isOwnFile = file.ownerID == user?.id; - const canModify = - isOwnFile && !isInTrashSection && !isInHiddenSection; - const showFavorite = canModify; - const isEditableImage = - onSaveEditedImageCopy && canModify - ? fileIsEditableImage(file) - : undefined; - return { fileID, isOwnFile, showFavorite, isEditableImage }; - }, - [user, isInTrashSection, isInHiddenSection, onSaveEditedImageCopy], - ); - const handleViewInfo = useCallback( (annotatedFile: FileViewerAnnotatedFile) => { setActiveAnnotatedFile(annotatedFile); @@ -253,25 +226,6 @@ const FileViewer: React.FC = ({ const handleFileInfoClose = useCallback(() => setOpenFileInfo(false), []); - const handleScheduleUpdate = useCallback(() => setNeedsSync(true), []); - - const handleSelectCollection = useCallback( - (collectionID: number) => { - onSelectCollection(collectionID); - handleClose(); - }, - [onSelectCollection, handleClose], - ); - - const handleSelectPerson = useMemo(() => { - return onSelectPerson - ? (personID: string) => { - onSelectPerson(personID); - handleClose(); - } - : undefined; - }, [onSelectPerson, handleClose]); - const handleMore = useCallback( ( annotatedFile: FileViewerAnnotatedFile, @@ -292,40 +246,91 @@ const FileViewer: React.FC = ({ const handleEditImage = useMemo(() => { return onSaveEditedImageCopy - ? (annotatedFile: FileViewerAnnotatedFile) => { - setActiveAnnotatedFile(annotatedFile); + ? () => { + handleMoreMenuClose(); setOpenImageEditor(true); } : undefined; - }, [onSaveEditedImageCopy]); + }, [onSaveEditedImageCopy, handleMoreMenuClose]); const handleImageEditorClose = useCallback( () => setOpenImageEditor(false), [], ); + const handleClose = useCallback(() => { + setNeedsSync((needSync) => { + if (needSync) onTriggerSyncWithRemote?.(); + return false; + }); + handleFileInfoClose(); + handleImageEditorClose(); + handleMoreMenuClose(); + onClose(); + }, [ + onTriggerSyncWithRemote, + handleFileInfoClose, + handleImageEditorClose, + handleMoreMenuClose, + onClose, + ]); + const handleSaveEditedCopy = useCallback( (editedFile: File, collection: Collection, enteFile: EnteFile) => { onSaveEditedImageCopy(editedFile, collection, enteFile); - handleImageEditorClose(); handleClose(); }, - [onSaveEditedImageCopy, handleImageEditorClose, handleClose], + [onSaveEditedImageCopy, handleClose], ); + const handleAnnotate = useCallback( + (file: EnteFile): FileViewerFileAnnotation => { + log.debug(() => ["viewer", { action: "annotate", file }]); + const fileID = file.id; + const isOwnFile = file.ownerID == user?.id; + const canModify = + isOwnFile && !isInTrashSection && !isInHiddenSection; + const showFavorite = canModify; + const isEditableImage = + handleEditImage && canModify + ? fileIsEditableImage(file) + : undefined; + return { fileID, isOwnFile, showFavorite, isEditableImage }; + }, + [user, isInTrashSection, isInHiddenSection, handleEditImage], + ); + + const handleScheduleUpdate = useCallback(() => setNeedsSync(true), []); + + const handleSelectCollection = useCallback( + (collectionID: number) => { + onSelectCollection(collectionID); + handleClose(); + }, + [onSelectCollection, handleClose], + ); + + const handleSelectPerson = useMemo(() => { + return onSelectPerson + ? (personID: string) => { + onSelectPerson(personID); + handleClose(); + } + : undefined; + }, [onSelectPerson, handleClose]); + const haveUser = !!user; - const showModifyActions = haveUser; const getFiles = useCallback(() => files, [files]); const isFavorite = useCallback( ({ file }: FileViewerAnnotatedFile) => { - if (!showModifyActions || !favoriteFileIDs || !onToggleFavorite) { + if (!haveUser || !favoriteFileIDs || !onToggleFavorite) { return undefined; } return favoriteFileIDs.has(file.id); }, - [showModifyActions, favoriteFileIDs, onToggleFavorite], + [haveUser, favoriteFileIDs, onToggleFavorite], ); const toggleFavorite = useCallback( @@ -355,13 +360,12 @@ const FileViewer: React.FC = ({ const pswp = new FileViewerPhotoSwipe({ initialIndex, disableDownload, - showModifyActions, + haveUser, delegate: delegateRef.current!, onClose: handleClose, onAnnotate: handleAnnotate, onViewInfo: handleViewInfo, onMore: handleMore, - onEditImage: handleEditImage, }); psRef.current = pswp; @@ -378,12 +382,11 @@ const FileViewer: React.FC = ({ user, initialIndex, disableDownload, - showModifyActions, + haveUser, handleClose, handleAnnotate, handleViewInfo, handleMore, - handleEditImage, ]); const handleRefreshPhotoswipe = useCallback(() => { @@ -419,7 +422,9 @@ const FileViewer: React.FC = ({ }} > {activeAnnotatedFile?.annotation.isEditableImage && ( - {/*TODO */ pt("Edit image")} + + {/*TODO */ pt("Edit image")} + )} & { /** - * `true` if various actions that modify the file should be shown. + * `true` if we're running in the context of a logged in user, and so + * various actions that modify the file should be shown. * - * This is the static variant of various per file annotations. If this is - * not `true`, then various actions like favorite, delete etc are never - * shown. If this is `true`, then their visibility depends on the - * corresponding annotation. + * This is the static variant of various per file annotations that control + * various modifications. If this is not `true`, then various actions like + * favorite, delete etc are never shown. If this is `true`, then their + * visibility depends on the corresponding annotation. * * For example, the favorite action is shown only if both this and the * {@link showFavorite} file annotation are true. */ - showModifyActions: boolean; + haveUser: boolean; /** * Dynamic callbacks. * @@ -145,16 +144,6 @@ type FileViewerPhotoSwipeOptions = Pick< annotatedFile: FileViewerAnnotatedFile, buttonElement: HTMLElement, ) => void; - /** - * Called when the user activates the edit action on an image. - * - * If this callback is not provided, then the edit action is never shown. If - * this callback is provided (and {@link showModifyActions} is `true`), then - * the visibility of the edit action is determined by the - * {@link isEditableImage} property of the {@link FileViewerFileAnnotation} - * for the file. - */ - onEditImage?: (annotatedFile: FileViewerAnnotatedFile) => void; }; /** @@ -248,13 +237,12 @@ export class FileViewerPhotoSwipe { constructor({ initialIndex, disableDownload, - showModifyActions, + haveUser, delegate, onClose, onAnnotate, onViewInfo, onMore, - onEditImage, }: FileViewerPhotoSwipeOptions) { this.opts = { disableDownload }; this.lastActivityDate = new Date(); @@ -522,7 +510,7 @@ export class FileViewerPhotoSwipe { }, }); - if (showModifyActions) { + if (haveUser) { const toggleFavorite = async ( buttonElement: HTMLButtonElement, ) => { @@ -602,7 +590,7 @@ export class FileViewerPhotoSwipe { onClick: () => onViewInfo(currentAnnotatedFile()), }); - if (showModifyActions && onEditImage) { + if (haveUser) { pswp.ui.registerElement({ name: "more", // TODO(PS): From 80859d91b08632289ea2750b48ec7c97be0a5d7e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 19:40:15 +0530 Subject: [PATCH 37/40] schedule update --- .../gallery/components/viewer/FileViewer.tsx | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index ff360ceba8..590af3e324 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -211,6 +211,20 @@ const FileViewer: React.FC = ({ // If `true`, then we need to trigger a sync with remote when we close. const [, setNeedsSync] = useState(false); + const handleClose = useCallback(() => { + setNeedsSync((needSync) => { + console.log("needs sync", needSync); + if (needSync) onTriggerSyncWithRemote?.(); + return false; + }); + setOpenFileInfo(false); + setOpenImageEditor(false); + // No need to `resetMoreMenuButtonOnMenuClose` since we're closing + // anyway and it'll be removed from the DOM. + setMoreMenuAnchorEl(null); + onClose(); + }, [onTriggerSyncWithRemote, onClose]); + const handleViewInfo = useCallback( (annotatedFile: FileViewerAnnotatedFile) => { setActiveAnnotatedFile(annotatedFile); @@ -258,23 +272,6 @@ const FileViewer: React.FC = ({ [], ); - const handleClose = useCallback(() => { - setNeedsSync((needSync) => { - if (needSync) onTriggerSyncWithRemote?.(); - return false; - }); - handleFileInfoClose(); - handleImageEditorClose(); - handleMoreMenuClose(); - onClose(); - }, [ - onTriggerSyncWithRemote, - handleFileInfoClose, - handleImageEditorClose, - handleMoreMenuClose, - onClose, - ]); - const handleSaveEditedCopy = useCallback( (editedFile: File, collection: Collection, enteFile: EnteFile) => { onSaveEditedImageCopy(editedFile, collection, enteFile); @@ -335,8 +332,10 @@ const FileViewer: React.FC = ({ const toggleFavorite = useCallback( ({ file }: FileViewerAnnotatedFile) => - onToggleFavorite!(file).catch(onGenericError), - [onToggleFavorite, onGenericError], + onToggleFavorite!(file) + .then(handleScheduleUpdate) + .catch(onGenericError), + [onToggleFavorite, handleScheduleUpdate, onGenericError], ); // Initial value of delegate. From 2859e02dacdda5313eb1ff200c8b0e0d2409e255 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 19:40:39 +0530 Subject: [PATCH 38/40] Revert "Temporary workbench" This reverts commit 8e08a0d71d2b32fe56fdd2e6acd80ad7a09f0a8f. --- web/apps/photos/src/pages/_app.tsx | 4 ++-- web/apps/photos/src/styles/global.css | 3 +-- web/packages/gallery/components/viewer/photoswipe.ts | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index aad64be0cf..000cf2d037 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -48,9 +48,9 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { resumeExportsIfNeeded } from "services/export"; import { photosLogout } from "services/logout"; -// import "photoswipe/dist/photoswipe.css"; +import "photoswipe/dist/photoswipe.css"; // TODO(PS): Note, auto hide only works with the new CSS. -import "../../../../packages/gallery/components/viewer/ps5/dist/photoswipe.css"; +// import "../../../../packages/gallery/components/viewer/ps5/dist/photoswipe.css"; import "styles/global.css"; diff --git a/web/apps/photos/src/styles/global.css b/web/apps/photos/src/styles/global.css index 1b45cfe9ed..fe0efe365a 100644 --- a/web/apps/photos/src/styles/global.css +++ b/web/apps/photos/src/styles/global.css @@ -12,7 +12,7 @@ body { flex-direction: column; height: 100%; } -@media DISABLED { + .pswp__button--custom { width: 48px; height: 48px; @@ -114,7 +114,6 @@ body { .pswp__caption--empty { display: none; } -} .pswp-ente { /* The default z-index for PhotoSwipe is 10k, way beyond everything else. diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index d84458c268..168fe3aacb 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -31,7 +31,7 @@ if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { let PhotoSwipe; if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { // TODO(PS): Comment me before merging into main. - PhotoSwipe = require("./ps5/dist/photoswipe.esm.js").default; + // PhotoSwipe = require("./ps5/dist/photoswipe.esm.js").default; } /** From b20de9ed83208b564b2756aed0247a2da9a058c6 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 19:42:58 +0530 Subject: [PATCH 39/40] Convert deprecated prop --- .../base/components/mui/SidebarDrawer.tsx | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/web/packages/base/components/mui/SidebarDrawer.tsx b/web/packages/base/components/mui/SidebarDrawer.tsx index 6c96b47859..97f321e427 100644 --- a/web/packages/base/components/mui/SidebarDrawer.tsx +++ b/web/packages/base/components/mui/SidebarDrawer.tsx @@ -23,16 +23,23 @@ import React from "react"; * It also does some trickery with a sticky opaque bar to ensure that the * content scrolls below our inline title bar on desktop. */ -export const SidebarDrawer: React.FC = ({ children, ...rest }) => ( +export const SidebarDrawer: React.FC = ({ + slotProps, + children, + ...rest +}) => ( From 7355d99299457ec651f033762f5e8aa369966b3f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 28 Feb 2025 19:54:38 +0530 Subject: [PATCH 40/40] Simplify --- web/apps/auth/src/pages/auth.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx index 9f702136a2..eb4ddee58a 100644 --- a/web/apps/auth/src/pages/auth.tsx +++ b/web/apps/auth/src/pages/auth.tsx @@ -219,12 +219,11 @@ const CodeDisplay: React.FC = ({ code }) => { message={t("copied")} slotProps={{ content: { - sx: (theme) => ({ - backgroundColor: - theme.vars.palette.fill.faint, - color: theme.vars.palette.primary.main, + sx: { + backgroundColor: "fill.faint", + color: "primary.main", backdropFilter: "blur(10px)", - }), + }, }, }} />