From 3b273a9e7b248f10d233ad801229480667e46c48 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 4 Jul 2025 19:41:35 +0530 Subject: [PATCH] Sketch 6 --- .../Collections/CollectionHeader.tsx | 50 +++-- .../Collections/GalleryBarAndListHeader.tsx | 29 +-- .../src/components/FileListWithViewer.tsx | 23 +- web/apps/photos/src/pages/gallery.tsx | 8 +- web/apps/photos/src/pages/shared-albums.tsx | 38 ++-- web/apps/photos/src/utils/file/index.ts | 9 +- .../gallery/components/utils/save-groups.ts | 157 ++++++++++++++ web/packages/gallery/services/save.ts | 203 ++++++++---------- 8 files changed, 326 insertions(+), 191 deletions(-) create mode 100644 web/packages/gallery/components/utils/save-groups.ts diff --git a/web/apps/photos/src/components/Collections/CollectionHeader.tsx b/web/apps/photos/src/components/Collections/CollectionHeader.tsx index bbf99742bc..800df6edc2 100644 --- a/web/apps/photos/src/components/Collections/CollectionHeader.tsx +++ b/web/apps/photos/src/components/Collections/CollectionHeader.tsx @@ -25,6 +25,9 @@ import { import { SingleInputDialog } from "ente-base/components/SingleInputDialog"; import { useModalVisibility } from "ente-base/components/utils/modal"; import { useBaseContext } from "ente-base/context"; +import type { AddSaveGroup } from "ente-gallery/components/utils/save-groups"; +import { downloadAndSaveCollectionFiles } from "ente-gallery/services/save"; +import { uniqueFilesByID } from "ente-gallery/utils/file"; import { CollectionOrder, type Collection } from "ente-media/collection"; import { ItemVisibility } from "ente-media/file-metadata"; import type { RemotePullOpts } from "ente-new/photos/components/gallery"; @@ -33,7 +36,9 @@ import { GalleryItemsSummary, } from "ente-new/photos/components/gallery/ListHeader"; import { + defaultHiddenCollectionUserFacingName, deleteCollection, + findDefaultHiddenCollectionIDs, isHiddenCollection, leaveSharedCollection, renameCollection, @@ -46,16 +51,15 @@ import { type CollectionSummary, type CollectionSummaryType, } from "ente-new/photos/services/collection-summary"; +import { + savedCollectionFiles, + savedCollections, +} from "ente-new/photos/services/photos-fdb"; import { emptyTrash } from "ente-new/photos/services/trash"; import { usePhotosAppContext } from "ente-new/photos/types/context"; import { t } from "i18next"; import React, { useCallback, useRef } from "react"; import { Trans } from "react-i18next"; -import type { SetFilesDownloadProgressAttributesCreator } from "types/gallery"; -import { - downloadCollectionHelper, - downloadDefaultHiddenCollectionHelper, -} from "ente-gallery/services/save"; export interface CollectionHeaderProps { collectionSummary: CollectionSummary; @@ -69,7 +73,11 @@ export interface CollectionHeaderProps { onRemotePull: (opts?: RemotePullOpts) => Promise; onCollectionShare: () => void; onCollectionCast: () => void; - setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator; + /** + * A function that can be used to create a UI notification to track the + * progress of user-initiated download, and to cancel it if needed. + */ + onAddSaveGroup: AddSaveGroup; } /** @@ -119,7 +127,7 @@ const CollectionHeaderOptions: React.FC = ({ onRemotePull, onCollectionShare, onCollectionCast, - setFilesDownloadProgressAttributesCreator, + onAddSaveGroup, isActiveCollectionDownloadInProgress, }) => { const { showMiniDialog, onGenericError } = useBaseContext(); @@ -225,17 +233,31 @@ const CollectionHeaderOptions: React.FC = ({ if (isActiveCollectionDownloadInProgress()) return; if (collectionSummaryType == "hiddenItems") { - await downloadDefaultHiddenCollectionHelper( - setFilesDownloadProgressAttributesCreator, + const defaultHiddenCollectionsIDs = findDefaultHiddenCollectionIDs( + await savedCollections(), + ); + const collectionFiles = await savedCollectionFiles(); + const defaultHiddenCollectionFiles = uniqueFilesByID( + collectionFiles.filter((file) => + defaultHiddenCollectionsIDs.has(file.collectionID), + ), + ); + await downloadAndSaveCollectionFiles( + defaultHiddenCollectionUserFacingName, + PseudoCollectionID.hiddenItems, + defaultHiddenCollectionFiles, + true, + onAddSaveGroup, ); } else { - await downloadCollectionHelper( + await downloadAndSaveCollectionFiles( + activeCollection.name, activeCollection.id, - setFilesDownloadProgressAttributesCreator( - activeCollection.name, - activeCollection.id, - isHiddenCollection(activeCollection), + (await savedCollectionFiles()).filter( + (file) => file.collectionID == activeCollection.id, ), + isHiddenCollection(activeCollection), + onAddSaveGroup, ); } }; diff --git a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx index 594113060b..0aaeb27c80 100644 --- a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx +++ b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx @@ -6,10 +6,10 @@ import { import type { TimeStampListItem } from "components/FileList"; import { useModalVisibility } from "ente-base/components/utils/modal"; import { - isFilesDownloadCancelled, + isSaveCancelled, isSaveComplete, type SaveGroup, -} from "ente-gallery/services/save"; +} from "ente-gallery/components/utils/save-groups"; import type { Collection } from "ente-media/collection"; import { GalleryBarImpl, @@ -47,11 +47,8 @@ type GalleryBarAndListHeaderProps = Omit< activeCollection: Collection; setActiveCollectionID: (collectionID: number) => void; setPhotoListHeader: (value: TimeStampListItem) => void; - filesDownloadProgressAttributesList: SaveGroup[]; -} & Pick< - CollectionHeaderProps, - "setFilesDownloadProgressAttributesCreator" | "onRemotePull" - > & + saveGroups: SaveGroup[]; +} & Pick & Pick< CollectionShareProps, "user" | "emailByUserID" | "shareSuggestionEmails" | "setBlockingLoad" @@ -88,14 +85,14 @@ export const GalleryBarAndListHeader: React.FC< setActiveCollectionID, setBlockingLoad, people, + saveGroups, activePerson, emailByUserID, shareSuggestionEmails, onRemotePull, + onAddSaveGroup, onSelectPerson, setPhotoListHeader, - filesDownloadProgressAttributesList, - setFilesDownloadProgressAttributesCreator, }) => { const { show: showAllAlbums, props: allAlbumsVisibilityProps } = useModalVisibility(); @@ -125,15 +122,11 @@ export const GalleryBarAndListHeader: React.FC< ); const isActiveCollectionDownloadInProgress = useCallback(() => { - const attributes = filesDownloadProgressAttributesList.find( - (attr) => attr.collectionID === activeCollectionID, + const group = saveGroups.find( + (g) => g.collectionID == activeCollectionID, ); - return ( - attributes && - !isFilesDownloadCancelled(attributes) && - !isSaveComplete(attributes) - ); - }, [activeCollectionID, filesDownloadProgressAttributesList]); + return group && !isSaveCancelled(group) && !isSaveComplete(group); + }, [saveGroups, activeCollectionID]); useEffect(() => { if (shouldHide) return; @@ -145,9 +138,9 @@ export const GalleryBarAndListHeader: React.FC< {...{ activeCollection, setActiveCollectionID, - setFilesDownloadProgressAttributesCreator, isActiveCollectionDownloadInProgress, onRemotePull, + onAddSaveGroup, }} collectionSummary={toShowCollectionSummaries.get( activeCollectionID, diff --git a/web/apps/photos/src/components/FileListWithViewer.tsx b/web/apps/photos/src/components/FileListWithViewer.tsx index 7881e2d2f2..0d72cf0c69 100644 --- a/web/apps/photos/src/components/FileListWithViewer.tsx +++ b/web/apps/photos/src/components/FileListWithViewer.tsx @@ -1,14 +1,12 @@ import { styled } from "@mui/material"; import { isSameDay } from "ente-base/date"; import { formattedDate } from "ente-base/i18n-date"; +import type { AddSaveGroup } from "ente-gallery/components/utils/save-groups"; import { FileViewer, type FileViewerProps, } from "ente-gallery/components/viewer/FileViewer"; -import { - downloadSingleFile, - type SetFilesDownloadProgressAttributesCreator, -} from "ente-gallery/services/save"; +import { downloadAndSaveFiles } from "ente-gallery/services/save"; import type { Collection } from "ente-media/collection"; import type { EnteFile } from "ente-media/file"; import { fileCreationTime, fileFileName } from "ente-media/file-metadata"; @@ -40,7 +38,6 @@ export type FileListWithViewerProps = { * Not set in the context of the shared albums app. */ onMarkTempDeleted?: (files: EnteFile[]) => void; - setFilesDownloadProgressAttributesCreator?: SetFilesDownloadProgressAttributesCreator; /** * Called when the visibility of the file viewer dialog changes. */ @@ -50,6 +47,11 @@ export type FileListWithViewerProps = { * pull from remote. */ onRemotePull: () => Promise; + /** + * A function that can be used to create a UI notification to track the + * progress of user-initiated download, and to cancel it if needed. + */ + onAddSaveGroup: AddSaveGroup; } & Pick< FileListProps, | "mode" @@ -109,11 +111,11 @@ export const FileListWithViewer: React.FC = ({ collectionNameByID, pendingFavoriteUpdates, pendingVisibilityUpdates, - setFilesDownloadProgressAttributesCreator, onSetOpenFileViewer, onRemotePull, onRemoteFilesPull, onVisualFeedback, + onAddSaveGroup, onToggleFavorite, onFileVisibilityUpdate, onMarkTempDeleted, @@ -149,12 +151,9 @@ export const FileListWithViewer: React.FC = ({ ); const handleDownload = useCallback( - (file: EnteFile) => { - const setSingleFileDownloadProgress = - setFilesDownloadProgressAttributesCreator!(fileFileName(file)); - void downloadSingleFile(file, setSingleFileDownloadProgress); - }, - [setFilesDownloadProgressAttributesCreator], + (file: EnteFile) => + downloadAndSaveFiles([file], fileFileName(file), onAddSaveGroup), + [onAddSaveGroup], ); const handleDelete = useMemo(() => { diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index b7e01b81eb..51bd8a3324 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -1024,8 +1024,8 @@ const Page: React.FC = () => { activeCollectionID, activePerson, setPhotoListHeader, - setFilesDownloadProgressAttributesCreator, - filesDownloadProgressAttributesList, + saveGroups, + onAddSaveGroup, }} mode={barMode} shouldHide={isInSearchMode} @@ -1122,11 +1122,9 @@ const Page: React.FC = () => { fileNormalCollectionIDs, pendingFavoriteUpdates, pendingVisibilityUpdates, + onAddSaveGroup, }} emailByUserID={state.emailByUserID} - setFilesDownloadProgressAttributesCreator={ - setFilesDownloadProgressAttributesCreator - } onToggleFavorite={handleFileViewerToggleFavorite} onFileVisibilityUpdate={ handleFileViewerFileVisibilityUpdate diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 23124776e2..7d1e52b8a3 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -47,12 +47,14 @@ import { } from "ente-base/http"; import log from "ente-base/log"; import { FullScreenDropZone } from "ente-gallery/components/FullScreenDropZone"; -import { useSaveGroups } from "ente-gallery/components/utils/save-groups"; +import { + useSaveGroups, + type AddSaveGroup, +} from "ente-gallery/components/utils/save-groups"; import { downloadManager } from "ente-gallery/services/download"; import { - downloadCollectionFiles, - downloadSelectedFiles, - type SetFilesDownloadProgressAttributesCreator, + downloadAndSaveCollectionFiles, + downloadAndSaveFiles, } from "ente-gallery/services/save"; import { extractCollectionKeyFromShareURL } from "ente-gallery/services/share"; import { updateShouldDisableCFUploadProxy } from "ente-gallery/services/upload"; @@ -410,13 +412,10 @@ export default function PublicCollectionGallery() { const downloadFilesHelper = async () => { try { const selectedFiles = getSelectedFiles(selected, publicFiles); - const setFilesDownloadProgressAttributes = - setFilesDownloadProgressAttributesCreator( - t("files_count", { count: selectedFiles.length }), - ); - await downloadSelectedFiles( + await downloadAndSaveFiles( selectedFiles, - setFilesDownloadProgressAttributes, + t("files_count", { count: selectedFiles.length }), + onAddSaveGroup, ); clearSelection(); } catch (e) { @@ -634,30 +633,25 @@ const SelectedFileOptions: React.FC = ({ interface ListHeaderProps { publicCollection: Collection; publicFiles: EnteFile[]; - setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator; + onAddSaveGroup: AddSaveGroup; } const ListHeader: React.FC = ({ publicCollection, publicFiles, - setFilesDownloadProgressAttributesCreator, + onAddSaveGroup, }) => { const downloadEnabled = publicCollection.publicURLs?.[0]?.enableDownload ?? true; - const downloadAllFiles = async () => { - const setFilesDownloadProgressAttributes = - setFilesDownloadProgressAttributesCreator( - publicCollection.name, - publicCollection.id, - isHiddenCollection(publicCollection), - ); - await downloadCollectionFiles( + const downloadAllFiles = () => + downloadAndSaveCollectionFiles( publicCollection.name, + publicCollection.id, publicFiles, - setFilesDownloadProgressAttributes, + isHiddenCollection(publicCollection), + onAddSaveGroup, ); - }; return ( diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 23c35e9635..b2d3199bb7 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -1,6 +1,6 @@ import type { LocalUser } from "ente-accounts/services/user"; import type { AddSaveGroup } from "ente-gallery/components/utils/save-groups"; -import { saveFiles } from "ente-gallery/services/save"; +import { downloadAndSaveFiles } from "ente-gallery/services/save"; import type { EnteFile } from "ente-media/file"; import { ItemVisibility } from "ente-media/file-metadata"; import { type FileOp } from "ente-new/photos/components/SelectedFileOptions"; @@ -11,6 +11,7 @@ import { moveToTrash, } from "ente-new/photos/services/collection"; import { updateFilesVisibility } from "ente-new/photos/services/file"; +import { t } from "i18next"; import type { SelectedState } from "types/gallery"; export function getSelectedFiles( @@ -61,7 +62,11 @@ export const performFileOp = async ( ) => { switch (op) { case "download": { - await saveFiles(files, onAddSaveGroup); + await downloadAndSaveFiles( + files, + t("files_count", { count: files.length }), + onAddSaveGroup, + ); break; } case "fixTime": diff --git a/web/packages/gallery/components/utils/save-groups.ts b/web/packages/gallery/components/utils/save-groups.ts new file mode 100644 index 0000000000..3a6fc788dc --- /dev/null +++ b/web/packages/gallery/components/utils/save-groups.ts @@ -0,0 +1,157 @@ +import { useCallback, useState } from "react"; + +/** + * An object that keeps track of progress of a user-initiated download of a set + * of files to the user's device. + * + * This "download" is distinct from the downloads the app does from remote (e.g. + * when the user is viewing them). + * + * What we're doing here is perhaps more accurately described "a user initiated + * download of files to the user's device", but that is too long, so we instead + * refer to this process as "saving them". + * + * Note however that the app's UI itself takes the user perspective, so the + * upper (UI) layers use the word "download", while this implementation layer + * uses the word "save", and there is an unavoidable incongruity in the middle. + */ +export interface SaveGroup { + /** + * A randomly generated unique identifier of this set of saves. + */ + id: number; + /** + * The user visible title of the save group. + * + * Depending on the context can either be an auto generated string (e.g "5 + * files"), or the name of the collection which is being downloaded. + */ + title: string; + /** + * If this save group is associated with a collection, then the ID of the + * collection. + */ + collectionID?: number; + /** + * `true` if the collection associated with the save group is a hidden + * collection. + */ + isHidden?: boolean; + /** + * The path to a directory on the user's file system that was selected by + * the user to save the files in when they initiated the download on the + * desktop app. + * + * This property is only set when running in the context of the desktop app. + * The web app downloads to the user's default downloads folder, and when + * running in the web app this property will not be set. + */ + downloadDirPath?: string; + /** + * The total number of files to save to the user's device. + */ + total: number; + /** + * The number of files that have already been save. + */ + success: number; + /** + * The number of failures. + */ + failed: number; + /** + * An {@link AbortController} that can be used to cancel the save. + */ + canceller?: AbortController; +} + +export const isSaveStarted = (group: SaveGroup) => group.total > 0; + +/** + * Return `true` if there are no files in this save group that are pending. + */ +export const isSaveComplete = ({ total, success, failed }: SaveGroup) => + total == success + failed; + +/** + * Return `true` if there are no files in this save group that are pending, but + * one or more files had failed to download. + */ +export const isSaveCompleteWithErrors = (group: SaveGroup) => + group.failed > 0 && isSaveComplete(group); + +/** + * Return `true` if this save was cancelled on a user request. + */ +export const isSaveCancelled = (group: SaveGroup) => + group.canceller?.signal.aborted; + +/** + * A function that can be used to add a save group. + * + * It returns a function that can subsequently be used to update the save group + * by applying a transform to it (see {@link UpdateSaveGroup}). The UI will + * react and update itself on updates done this way. + */ +export type AddSaveGroup = (group: Partial) => UpdateSaveGroup; + +/** + * A function that can be used to update a instance of a save group by applying + * the provided transform. + * + * This is obtained by a call to an instance of {@link AddSaveGroup}. The UI + * will update itself to reflect the changes made by the transform. + */ +export type UpdateSaveGroup = ( + tranform: (prev: SaveGroup) => SaveGroup, +) => void; + +/** + * A function that can be used to remove a save group. + * + * Save groups can be removed both on user actions - if the user presses the + * close button to discard the notification showing the status of the save group + * (cancelling it if needed) - or programmatically, if it is found that there + * are no files that need saving for a particular request. + */ +export type RemoveSaveGroup = (saveGroup: SaveGroup) => void; + +/** + * A custom React hook that manages a list of active {@link SaveGroup}s, and + * provides functions to add and remove entries to the list. + */ +export const useSaveGroups = () => { + const [saveGroups, setSaveGroups] = useState([]); + + const handleAddSaveGroup: AddSaveGroup = useCallback((saveGroup) => { + const id = Math.random(); + setSaveGroups((groups) => [ + ...groups, + { + ...saveGroup, + id, + // TODO(RE): + title: saveGroup.title ?? "", + total: saveGroup.total ?? 0, + success: 0, + failed: 0, + }, + ]); + return (tx: (group: SaveGroup) => SaveGroup) => { + setSaveGroups((groups) => + groups.map((g) => (g.id == id ? tx(g) : g)), + ); + }; + }, []); + + const handleRemoveSaveGroup: RemoveSaveGroup = useCallback( + ({ id }) => setSaveGroups((groups) => groups.filter((g) => g.id != id)), + [], + ); + + return { + saveGroups, + onAddSaveGroup: handleAddSaveGroup, + onRemoveSaveGroup: handleRemoveSaveGroup, + }; +}; diff --git a/web/packages/gallery/services/save.ts b/web/packages/gallery/services/save.ts index 409a6cde67..517388c88f 100644 --- a/web/packages/gallery/services/save.ts +++ b/web/packages/gallery/services/save.ts @@ -1,31 +1,19 @@ -import { ensureElectron } from "ente-base/electron"; import { joinPath } from "ente-base/file-name"; import log from "ente-base/log"; import { type Electron } from "ente-base/types/ipc"; import { saveAsFileAndRevokeObjectURL } from "ente-base/utils/web"; import { downloadManager } from "ente-gallery/services/download"; import { detectFileTypeInfo } from "ente-gallery/utils/detect-type"; -import { uniqueFilesByID } from "ente-gallery/utils/file"; import { writeStream } from "ente-gallery/utils/native-stream"; import type { EnteFile } from "ente-media/file"; import { fileFileName } from "ente-media/file-metadata"; import { FileType } from "ente-media/file-type"; import { decodeLivePhoto } from "ente-media/live-photo"; -import { - defaultHiddenCollectionUserFacingName, - findDefaultHiddenCollectionIDs, -} from "ente-new/photos/services/collection"; -import { PseudoCollectionID } from "ente-new/photos/services/collection-summary"; -import { - savedCollectionFiles, - savedCollections, -} from "ente-new/photos/services/photos-fdb"; import { safeDirectoryName, safeFileName, } from "ente-new/photos/utils/native-fs"; import { wait } from "ente-utils/promise"; -import { t } from "i18next"; import type { AddSaveGroup } from "../components/utils/save-groups"; /** @@ -38,16 +26,60 @@ import type { AddSaveGroup } from "../components/utils/save-groups"; * * @param files The files to save. * + * @param title A title to show in the UI notification that indicates the + * progress of the save. + * * @param onAddSaveGroup A function that can be used to create a save group * associated with the save. The newly added save group will correspond to a * notification shown in the UI, and the progress and status of the save can be * communicated by updating the save group's state using the updater function * obtained when adding the save group. */ -export async function saveFiles( +export const downloadAndSaveFiles = ( files: EnteFile[], + title: string, onAddSaveGroup: AddSaveGroup, -) { +) => downloadAndSave(files, title, onAddSaveGroup); + +/** + * Save all the files of a collection to the user's device. + * + * This is a variant of {@link downloadAndSaveFiles}, except instead of taking a + * list of files to save, this variant is tailored for saving saves all the + * files that belong to a collection. Otherwise, it broadly behaves similarly; + * see that method's documentation for more details. + * + * When running in the context of the desktop app, instead of saving the files + * in the directory selected by the user, files are saved in a directory with + * the same name as the collection. + */ +export const downloadAndSaveCollectionFiles = async ( + collectionName: string, + collectionID: number | undefined, + files: EnteFile[], + isHidden: boolean, + onAddSaveGroup: AddSaveGroup, +) => + downloadAndSave( + files, + collectionName, + onAddSaveGroup, + collectionName, + collectionID, + isHidden, + ); + +/** + * The lower level primitive that the public API of this module delegates to. + */ +const downloadAndSave = async ( + files: EnteFile[], + title: string, + onAddSaveGroup: AddSaveGroup, + collectionName?: string, + collectionID?: number, + isHidden?: boolean, +) => { const electron = globalThis.electron; let downloadDirPath: string | undefined; @@ -57,14 +89,24 @@ export async function saveFiles( // The user cancelled on the directory selection dialog. return; } + if (collectionName) { + downloadDirPath = await mkdirCollectionDownloadFolder( + electron, + downloadDirPath, + collectionName, + ); + } } const canceller = new AbortController(); + const total = files.length; const updateSaveGroup = onAddSaveGroup({ - title: t("files_count", { count: files.length }), + title, + collectionID, + isHidden, downloadDirPath, - total: files.length, + total, canceller, }); @@ -82,7 +124,7 @@ export async function saveFiles( updateSaveGroup((g) => ({ ...g, failed: g.failed + 1 })); } } -} +}; /** * Save the given {@link EnteFile} as a file in the user's download folder. @@ -119,6 +161,32 @@ const createTypedObjectURL = async (blobPart: BlobPart, fileName: string) => { return URL.createObjectURL(new Blob([blob], { type: mimeType })); }; +/** + * Create a new directory on the user's file system with the same name as the + * provided {@link collectionName} under the provided {@link downloadDirPath}, + * and return the full path to the created directory. + * + * This function can be used only when running in the context of our desktop + * app, and so such requires an {@link Electron} instance as the witness. + */ +const mkdirCollectionDownloadFolder = async ( + { fs }: Electron, + downloadDirPath: string, + collectionName: string, +) => { + const collectionDownloadName = await safeDirectoryName( + downloadDirPath, + collectionName, + fs.exists, + ); + const collectionDownloadPath = joinPath( + downloadDirPath, + collectionDownloadName, + ); + await fs.mkdirIfNeeded(collectionDownloadPath); + return collectionDownloadPath; +}; + /** * Save a file to the given {@link directoryPath} using native filesystem APIs. * @@ -171,104 +239,3 @@ const saveFileDesktop = async ( await writeStreamToFile(await createExportName(fileName), stream); } }; - -export async function downloadCollectionHelper( - collectionID: number, - setFilesDownloadProgressAttributes: SetFilesDownloadProgressAttributes, -) { - try { - const allFiles = await savedCollectionFiles(); - const collectionFiles = allFiles.filter( - (file) => file.collectionID == collectionID, - ); - const allCollections = await savedCollections(); - const collection = allCollections.find( - (collection) => collection.id == collectionID, - ); - if (!collection) { - throw Error("collection not found"); - } - await downloadCollectionFiles( - collection.name, - collectionFiles, - setFilesDownloadProgressAttributes, - ); - } catch (e) { - log.error("download collection failed ", e); - } -} - -export async function downloadDefaultHiddenCollectionHelper( - setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator, -) { - try { - const defaultHiddenCollectionsIDs = findDefaultHiddenCollectionIDs( - await savedCollections(), - ); - const collectionFiles = await savedCollectionFiles(); - const defaultHiddenCollectionFiles = uniqueFilesByID( - collectionFiles.filter((file) => - defaultHiddenCollectionsIDs.has(file.collectionID), - ), - ); - const setFilesDownloadProgressAttributes = - setFilesDownloadProgressAttributesCreator( - defaultHiddenCollectionUserFacingName, - PseudoCollectionID.hiddenItems, - true, - ); - - await downloadCollectionFiles( - defaultHiddenCollectionUserFacingName, - defaultHiddenCollectionFiles, - setFilesDownloadProgressAttributes, - ); - } catch (e) { - log.error("download hidden files failed ", e); - } -} - -export async function downloadCollectionFiles( - collectionName: string, - collectionFiles: EnteFile[], - setFilesDownloadProgressAttributes: SetFilesDownloadProgressAttributes, -) { - if (!collectionFiles.length) { - return; - } - let downloadDirPath: string; - const electron = globalThis.electron; - if (electron) { - const selectedDir = await electron.selectDirectory(); - if (!selectedDir) { - return; - } - downloadDirPath = await createCollectionDownloadFolder( - selectedDir, - collectionName, - ); - } - await downloadFilesWithProgress( - collectionFiles, - downloadDirPath, - setFilesDownloadProgressAttributes, - ); -} - -async function createCollectionDownloadFolder( - downloadDirPath: string, - collectionName: string, -) { - const fs = ensureElectron().fs; - const collectionDownloadName = await safeDirectoryName( - downloadDirPath, - collectionName, - fs.exists, - ); - const collectionDownloadPath = joinPath( - downloadDirPath, - collectionDownloadName, - ); - await fs.mkdirIfNeeded(collectionDownloadPath); - return collectionDownloadPath; -}