Sketch 6
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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":
|
||||
|
||||
157
web/packages/gallery/components/utils/save-groups.ts
Normal file
157
web/packages/gallery/components/utils/save-groups.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user