This commit is contained in:
Manav Rathi
2025-07-04 19:41:35 +05:30
parent ec23e869e8
commit 3b273a9e7b
8 changed files with 326 additions and 191 deletions

View File

@@ -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<void>;
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<CollectionHeaderProps> = ({
onRemotePull,
onCollectionShare,
onCollectionCast,
setFilesDownloadProgressAttributesCreator,
onAddSaveGroup,
isActiveCollectionDownloadInProgress,
}) => {
const { showMiniDialog, onGenericError } = useBaseContext();
@@ -225,17 +233,31 @@ const CollectionHeaderOptions: React.FC<CollectionHeaderProps> = ({
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,
);
}
};

View File

@@ -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<CollectionHeaderProps, "onRemotePull" | "onAddSaveGroup"> &
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,

View File

@@ -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<void>;
/**
* 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<FileListWithViewerProps> = ({
collectionNameByID,
pendingFavoriteUpdates,
pendingVisibilityUpdates,
setFilesDownloadProgressAttributesCreator,
onSetOpenFileViewer,
onRemotePull,
onRemoteFilesPull,
onVisualFeedback,
onAddSaveGroup,
onToggleFavorite,
onFileVisibilityUpdate,
onMarkTempDeleted,
@@ -149,12 +151,9 @@ export const FileListWithViewer: React.FC<FileListWithViewerProps> = ({
);
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(() => {

View File

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

View File

@@ -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<SelectedFileOptionsProps> = ({
interface ListHeaderProps {
publicCollection: Collection;
publicFiles: EnteFile[];
setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator;
onAddSaveGroup: AddSaveGroup;
}
const ListHeader: React.FC<ListHeaderProps> = ({
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 (
<GalleryItemsHeaderAdapter>

View File

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

View File

@@ -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<SaveGroup>) => 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<SaveGroup[]>([]);
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,
};
};

View File

@@ -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;
}