From a4ba2edc54ca98cdd831e6bbfe019b87582e6aeb Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 27 May 2025 06:23:46 +0530 Subject: [PATCH] Update --- web/apps/photos/src/components/Export.tsx | 172 ++++++++++++---------- web/apps/photos/src/pages/gallery.tsx | 2 +- 2 files changed, 92 insertions(+), 82 deletions(-) diff --git a/web/apps/photos/src/components/Export.tsx b/web/apps/photos/src/components/Export.tsx index d22c6e52b4..fe3e3ad840 100644 --- a/web/apps/photos/src/components/Export.tsx +++ b/web/apps/photos/src/components/Export.tsx @@ -44,9 +44,14 @@ import { } from "ente-shared/components/Container"; import { CustomError } from "ente-shared/error"; import { t } from "i18next"; -import React, { memo, useEffect, useState } from "react"; +import React, { memo, useCallback, useEffect, useState } from "react"; import { Trans } from "react-i18next"; -import { areEqual, FixedSizeList, ListChildComponentProps } from "react-window"; +import { + areEqual, + FixedSizeList, + ListChildComponentProps, + type ListItemKeySelector, +} from "react-window"; import exportService, { ExportStage, selectAndPrepareExportDirectory, @@ -56,13 +61,23 @@ import exportService, { } from "services/export"; type ExportProps = ModalVisibilityProps & { - allCollectionsNameByID: Map; + /** + * A map from collection IDs to their user visible name. + * + * It will contain entries for all collections (both normal and hidden). + */ + collectionNameByID: Map; }; +/** + * A dialog that allows the user to view and manage the export of their data. + * + * Available only in the desktop app (export requires direct disk access). + */ export const Export: React.FC = ({ open, onClose, - allCollectionsNameByID, + collectionNameByID, }) => { const { showMiniDialog } = useBaseContext(); const [exportStage, setExportStage] = useState( @@ -75,6 +90,7 @@ export const Export: React.FC = ({ failed: 0, total: 0, }); + // The list of EnteFiles that have not been exported yet. const [pendingFiles, setPendingFiles] = useState([]); const [lastExportTime, setLastExportTime] = useState(0); @@ -113,7 +129,7 @@ export const Export: React.FC = ({ // HELPER FUNCTIONS // ======================= - const verifyExportFolderExists = async () => { + const verifyExportFolderExists = useCallback(async () => { if (!(await exportService.exportFolderExists(exportFolder))) { showMiniDialog({ title: t("export_directory_does_not_exist"), @@ -127,7 +143,7 @@ export const Export: React.FC = ({ return false; } return true; - }; + }, [showMiniDialog]); const syncExportRecord = async (exportFolder: string): Promise => { try { @@ -175,15 +191,20 @@ export const Export: React.FC = ({ setContinuousExport(newContinuousExport); }; - const startExport = async (opts?: ExportOpts) => { - if (!(await verifyExportFolderExists())) return; + const handleStartExport = useCallback( + (opts?: ExportOpts) => { + void (async () => { + if (!(await verifyExportFolderExists())) return; - await exportService.scheduleExport(opts ?? {}); - }; + await exportService.scheduleExport(opts ?? {}); + }); + }, + [verifyExportFolderExists], + ); - const stopExport = () => { + const handleStopExport = useCallback(() => { void exportService.stopRunningExport(); - }; + }, []); return ( @@ -207,14 +228,17 @@ export const Export: React.FC = ({ handleStartExport({ resync: true })} + onStopExport={handleStopExport} /> ); @@ -304,26 +328,20 @@ const ContinuousExport: React.FC = ({ ); -interface ExportDialogStageContentProps { - exportStage: ExportStage; - stopExport: () => void; - onHide: () => void; - lastExportTime: number; - exportProgress: ExportProgress; - pendingFiles: EnteFile[]; - allCollectionsNameByID: Map; - onStartExport: (opts?: ExportOpts) => void; -} +type ExportDialogStageContentProps = ExportInitDialogContentProps & + ExportInProgressDialogContentProps & + ExportFinishedDialogContentProps; const ExportDialogStageContent: React.FC = ({ exportStage, - onStartExport, - stopExport, - onHide, - lastExportTime, exportProgress, pendingFiles, - allCollectionsNameByID, + lastExportTime, + collectionNameByID, + onClose, + onStartExport, + onStopExport, + onResyncExport, }) => { switch (exportStage) { case ExportStage.init: @@ -337,20 +355,19 @@ const ExportDialogStageContent: React.FC = ({ case ExportStage.trashingDeletedCollections: return ( ); case ExportStage.finished: return ( onStartExport({ resync: true })} + {...{ + pendingFiles, + lastExportTime, + collectionNameByID, + onClose, + onResyncExport, + }} /> ); @@ -382,20 +399,13 @@ const ExportInitDialogContent: React.FC = ({ interface ExportInProgressDialogContentProps { exportStage: ExportStage; exportProgress: ExportProgress; - /** - * Called when the user wants to stop the export. - */ - onStop: () => void; - /** - * Called when the user closes the export dialog. - * @returns - */ onClose: () => void; + onStopExport: () => void; } const ExportInProgressDialogContent: React.FC< ExportInProgressDialogContentProps -> = ({ exportStage, exportProgress, onClose, onStop }) => ( +> = ({ exportStage, exportProgress, onClose, onStopExport }) => ( <> @@ -461,7 +471,11 @@ const ExportInProgressDialogContent: React.FC< {t("close")} - + {t("stop")} @@ -469,17 +483,11 @@ const ExportInProgressDialogContent: React.FC< ); interface ExportFinishedDialogContentProps { - /** - * The list of {@link EnteFile}s that have not been exported yet. - */ pendingFiles: EnteFile[]; lastExportTime: number; - allCollectionsNameByID: Map; + collectionNameByID: Map; onClose: () => void; - /** - * Called when the user presses the "Resync" button. - */ - onResync: () => void; + onResyncExport: () => void; } const ExportFinishedDialogContent: React.FC< @@ -487,9 +495,9 @@ const ExportFinishedDialogContent: React.FC< > = ({ pendingFiles, lastExportTime, - allCollectionsNameByID, + collectionNameByID, onClose, - onResync, + onResyncExport, }) => { const { show: showPendingList, props: pendingListVisibilityProps } = useModalVisibility(); @@ -532,34 +540,40 @@ const ExportFinishedDialogContent: React.FC< > {t("close")} - + {t("export_again")} ); }; -type ExportPendingListDialogProps = ModalVisibilityProps & { - pendingFiles: EnteFile[]; - allCollectionsNameByID: Map; -}; +type ExportPendingListDialogProps = ModalVisibilityProps & + ExportPendingListItemData; const ExportPendingListDialog: React.FC = ({ open, onClose, - allCollectionsNameByID, + collectionNameByID, pendingFiles, }) => { const itemSize = 50; /* px */ const itemCount = pendingFiles.length; const listHeight = Math.min(itemCount * itemSize, 240); + const itemKey: ListItemKeySelector = ( + index, + { pendingFiles }, + ) => { + const file = pendingFiles[index]!; + return `${file.collectionID}/${file.id}`; + }; + return ( = ({ title={t("pending_items")} > { - const file = pendingFiles[index]!; - return `${file.collectionID}/${file.id}`; - }} + {...{ itemSize, itemCount, itemKey }} > {ExportPendingListItem} @@ -591,17 +601,17 @@ const ExportPendingListDialog: React.FC = ({ }; interface ExportPendingListItemData { - allCollectionsNameByID: Map; pendingFiles: EnteFile[]; + collectionNameByID: Map; } const ExportPendingListItem: React.FC< ListChildComponentProps > = memo(({ index, style, data }) => { - const { allCollectionsNameByID, pendingFiles } = data; - const file = pendingFiles[index] as EnteFile; + const { pendingFiles, collectionNameByID } = data; + const file = pendingFiles[index]!; - const itemTitle = `${allCollectionsNameByID.get(file.collectionID)} / ${ + const itemTitle = `${collectionNameByID.get(file.collectionID)} / ${ file.metadata.title }`; diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index a22b351ddb..f102a921f2 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -1146,7 +1146,7 @@ const Page: React.FC = () => { )}