[web] File selection handler options refactoring (#6410)

This commit is contained in:
Manav Rathi
2025-06-30 15:11:02 +05:30
committed by GitHub
8 changed files with 473 additions and 470 deletions

View File

@@ -92,17 +92,13 @@ interface UploadProps {
* albums app, this prop can be omitted.
*/
user?: LocalUser;
isFirstUpload?: boolean;
uploadTypeSelectorView: boolean;
dragAndDropFiles: File[];
uploadCollection?: Collection;
uploadTypeSelectorIntent: UploadTypeSelectorIntent;
activeCollection?: Collection;
closeUploadTypeSelector: () => void;
/**
* Show the collection selector with the given {@link attributes}.
*/
onOpenCollectionSelector?: (
attributes: CollectionSelectorAttributes,
) => void;
/**
* Close the collection selector if it is open.
*/
onCloseCollectionSelector?: () => void;
setLoading: SetLoading;
setShouldDisableDropzone: (value: boolean) => void;
showCollectionSelector?: () => void;
@@ -127,6 +123,16 @@ interface UploadProps {
* this property is optional; the public albums code need not provide it.
*/
onRemoteFilesPull?: () => Promise<void>;
/**
* Show the collection selector with the given {@link attributes}.
*/
onOpenCollectionSelector?: (
attributes: CollectionSelectorAttributes,
) => void;
/**
* Close the collection selector if it is open.
*/
onCloseCollectionSelector?: () => void;
/**
* Callback invoked when a file is uploaded.
*
@@ -140,13 +146,11 @@ interface UploadProps {
* app, where the scenario requiring this will not arise.
*/
onShowPlanSelector?: () => void;
isFirstUpload?: boolean;
uploadTypeSelectorView: boolean;
showSessionExpiredMessage: () => void;
dragAndDropFiles: File[];
uploadCollection?: Collection;
uploadTypeSelectorIntent: UploadTypeSelectorIntent;
activeCollection?: Collection;
/**
* Called when the upload failed because the user's session has expired, and
* the Upload component wants to prompt the user to log in again.
*/
onShowSessionExpiredDialog: () => void;
}
type UploadType = "files" | "folders" | "zips";
@@ -160,9 +164,11 @@ export const Upload: React.FC<UploadProps> = ({
dragAndDropFiles,
onRemotePull,
onRemoteFilesPull,
onOpenCollectionSelector,
onCloseCollectionSelector,
onUploadFile,
onShowPlanSelector,
showSessionExpiredMessage,
onShowSessionExpiredDialog,
...props
}) => {
const { showMiniDialog, onGenericError } = useBaseContext();
@@ -570,7 +576,7 @@ export const Upload: React.FC<UploadProps> = ({
};
}
props.onOpenCollectionSelector({
onOpenCollectionSelector({
action: "upload",
onSelectCollection: uploadFilesToExistingCollection,
onCreateCollection: showNextModal,
@@ -580,7 +586,7 @@ export const Upload: React.FC<UploadProps> = ({
}, [webFiles, desktopFiles, desktopFilePaths, desktopZipItems]);
const preCollectionCreationAction = async () => {
props.onCloseCollectionSelector?.();
onCloseCollectionSelector?.();
props.setShouldDisableDropzone(uploadManager.isUploadInProgress());
setUploadPhase("preparing");
setUploadProgressView(true);
@@ -756,7 +762,7 @@ export const Upload: React.FC<UploadProps> = ({
const notifyUser = (e: unknown) => {
switch (e instanceof Error && e.message) {
case sessionExpiredErrorMessage:
showSessionExpiredMessage();
onShowSessionExpiredDialog();
break;
case subscriptionExpiredErrorMessage:
showNotification({

View File

@@ -1,390 +0,0 @@
import ClockIcon from "@mui/icons-material/AccessTime";
import AddIcon from "@mui/icons-material/Add";
import ArchiveIcon from "@mui/icons-material/ArchiveOutlined";
import MoveIcon from "@mui/icons-material/ArrowForward";
import CloseIcon from "@mui/icons-material/Close";
import DeleteIcon from "@mui/icons-material/Delete";
import DownloadIcon from "@mui/icons-material/Download";
import FavoriteBorderIcon from "@mui/icons-material/FavoriteBorderRounded";
import RemoveIcon from "@mui/icons-material/RemoveCircleOutline";
import RestoreIcon from "@mui/icons-material/Restore";
import UnArchiveIcon from "@mui/icons-material/Unarchive";
import VisibilityOffOutlinedIcon from "@mui/icons-material/VisibilityOffOutlined";
import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
import { IconButton, Tooltip, Typography } from "@mui/material";
import { SpacedRow } from "ente-base/components/containers";
import { useBaseContext } from "ente-base/context";
import type { Collection } from "ente-media/collection";
import type { CollectionSelectorAttributes } from "ente-new/photos/components/CollectionSelector";
import type { GalleryBarMode } from "ente-new/photos/components/gallery/reducer";
import {
PseudoCollectionID,
type CollectionSummary,
} from "ente-new/photos/services/collection-summary";
import { t } from "i18next";
import { type CollectionOp } from "utils/collection";
import { type FileOp } from "utils/file";
interface Props {
handleCollectionOp: (op: CollectionOp) => (...args: any[]) => void;
handleFileOp: (op: FileOp) => (...args: any[]) => void;
showCreateCollectionModal: (op: CollectionOp) => () => void;
/**
* Callback to open a dialog where the user can choose a collection.
*
* The reason for opening the dialog and other properties are passed as the
* {@link attributes} argument.
*/
onOpenCollectionSelector: (
attributes: CollectionSelectorAttributes,
) => void;
count: number;
ownCount: number;
clearSelection: () => void;
barMode?: GalleryBarMode;
activeCollectionID: number;
/**
* TODO: Need to implement delete-equivalent from shared albums.
*
* Notes:
*
* - Delete action should not be enabled 3 selected (0 Yours). There should
* be separate remove action.
*
* - On remove, if the file and collection both belong to current user, we
* just use move api to existing or uncat collection.
*
* - Otherwise, we call /collections/v3/remove-files (when collection and
* file belong to different users).
*
* - Album owner can remove files of all other users from their collection.
* Particiapant (viewer/collaborator) can only remove files that belong to
* them.
*
* Also note that that user cannot delete files that are not owned by the
* user, even if they are in an album owned by the user.
*/
activeCollectionSummary: CollectionSummary | undefined;
isInSearchMode: boolean;
selectedCollection: Collection;
isInHiddenSection: boolean;
}
const SelectedFileOptions = ({
showCreateCollectionModal,
onOpenCollectionSelector,
handleCollectionOp,
handleFileOp,
selectedCollection,
count,
ownCount,
clearSelection,
barMode,
activeCollectionID,
activeCollectionSummary,
isInSearchMode,
isInHiddenSection,
}: Props) => {
const { showMiniDialog } = useBaseContext();
const peopleMode = barMode == "people";
const isFavoriteCollection =
!!activeCollectionSummary?.attributes.has("userFavorites");
const isUncategorizedCollection =
activeCollectionSummary?.type == "uncategorized";
const isSharedIncomingCollection =
!!activeCollectionSummary?.attributes.has("sharedIncoming");
const addToCollection = () =>
onOpenCollectionSelector({
action: "add",
onSelectCollection: handleCollectionOp("add"),
onCreateCollection: showCreateCollectionModal("add"),
relatedCollectionID:
isInSearchMode || peopleMode ? undefined : activeCollectionID,
});
const trashHandler = () =>
showMiniDialog({
title: t("trash_files_title"),
message: t("trash_files_message"),
continue: {
text: t("move_to_trash"),
color: "critical",
action: handleFileOp("trash"),
},
});
const permanentlyDeleteHandler = () =>
showMiniDialog({
title: t("delete_files_title"),
message: t("delete_files_message"),
continue: {
text: t("delete"),
color: "critical",
action: handleFileOp("deletePermanently"),
},
});
const restoreHandler = () =>
onOpenCollectionSelector({
action: "restore",
onSelectCollection: handleCollectionOp("restore"),
onCreateCollection: showCreateCollectionModal("restore"),
});
const removeFromCollectionHandler = () => {
if (ownCount === count) {
showMiniDialog({
title: t("remove_from_album"),
message: t("confirm_remove_message"),
continue: {
text: t("yes_remove"),
color: "primary",
action: () =>
handleCollectionOp("remove")(selectedCollection),
},
});
} else {
showMiniDialog({
title: t("remove_from_album"),
message: t("confirm_remove_incl_others_message"),
continue: {
text: t("yes_remove"),
color: "critical",
action: () =>
handleCollectionOp("remove")(selectedCollection),
},
});
}
};
const moveToCollection = () => {
onOpenCollectionSelector({
action: "move",
onSelectCollection: handleCollectionOp("move"),
onCreateCollection: showCreateCollectionModal("move"),
relatedCollectionID:
isInSearchMode || peopleMode ? undefined : activeCollectionID,
});
};
const unhideToCollection = () => {
onOpenCollectionSelector({
action: "unhide",
onSelectCollection: handleCollectionOp("unhide"),
onCreateCollection: showCreateCollectionModal("unhide"),
});
};
return (
<SpacedRow sx={{ flex: 1, gap: 1, flexWrap: "wrap" }}>
<IconButton onClick={clearSelection}>
<CloseIcon />
</IconButton>
<Typography sx={{ mr: "auto" }}>
{ownCount === count
? t("selected_count", { selected: count })
: t("selected_and_yours_count", {
selected: count,
yours: ownCount,
})}
</Typography>
{isInSearchMode ? (
<>
<Tooltip title={t("fix_creation_time")}>
<IconButton onClick={handleFileOp("fixTime")}>
<ClockIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("download")}>
<IconButton onClick={handleFileOp("download")}>
<DownloadIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("add")}>
<IconButton onClick={addToCollection}>
<AddIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("archive")}>
<IconButton onClick={handleFileOp("archive")}>
<ArchiveIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("hide")}>
<IconButton onClick={handleFileOp("hide")}>
<VisibilityOffOutlinedIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("delete")}>
<IconButton onClick={trashHandler}>
<DeleteIcon />
</IconButton>
</Tooltip>
</>
) : peopleMode ? (
<>
<Tooltip title={t("download")}>
<IconButton onClick={handleFileOp("download")}>
<DownloadIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("add")}>
<IconButton onClick={addToCollection}>
<AddIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("archive")}>
<IconButton onClick={handleFileOp("archive")}>
<ArchiveIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("hide")}>
<IconButton onClick={handleFileOp("hide")}>
<VisibilityOffOutlinedIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("delete")}>
<IconButton onClick={trashHandler}>
<DeleteIcon />
</IconButton>
</Tooltip>
</>
) : activeCollectionID == PseudoCollectionID.trash ? (
<>
<Tooltip title={t("restore")}>
<IconButton onClick={restoreHandler}>
<RestoreIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("delete_permanently")}>
<IconButton onClick={permanentlyDeleteHandler}>
<DeleteIcon />
</IconButton>
</Tooltip>
</>
) : isUncategorizedCollection ? (
<>
<Tooltip title={t("download")}>
<IconButton onClick={handleFileOp("download")}>
<DownloadIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("move")}>
<IconButton onClick={moveToCollection}>
<MoveIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("delete")}>
<IconButton onClick={trashHandler}>
<DeleteIcon />
</IconButton>
</Tooltip>
</>
) : isSharedIncomingCollection ? (
<Tooltip title={t("download")}>
<IconButton onClick={handleFileOp("download")}>
<DownloadIcon />
</IconButton>
</Tooltip>
) : isInHiddenSection ? (
<>
<Tooltip title={t("unhide")}>
<IconButton onClick={unhideToCollection}>
<VisibilityOutlinedIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("download")}>
<IconButton onClick={handleFileOp("download")}>
<DownloadIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("delete")}>
<IconButton onClick={trashHandler}>
<DeleteIcon />
</IconButton>
</Tooltip>
</>
) : (
<>
<Tooltip title={t("fix_creation_time")}>
<IconButton onClick={handleFileOp("fixTime")}>
<ClockIcon />
</IconButton>
</Tooltip>
{!isFavoriteCollection &&
activeCollectionID !=
PseudoCollectionID.archiveItems && (
<Tooltip title={t("favorite")}>
<IconButton onClick={handleFileOp("favorite")}>
<FavoriteBorderIcon />
</IconButton>
</Tooltip>
)}
<Tooltip title={t("download")}>
<IconButton onClick={handleFileOp("download")}>
<DownloadIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("add")}>
<IconButton onClick={addToCollection}>
<AddIcon />
</IconButton>
</Tooltip>
{activeCollectionID == PseudoCollectionID.archiveItems && (
<Tooltip title={t("unarchive")}>
<IconButton onClick={handleFileOp("unarchive")}>
<UnArchiveIcon />
</IconButton>
</Tooltip>
)}
{activeCollectionID === PseudoCollectionID.all && (
<Tooltip title={t("archive")}>
<IconButton onClick={handleFileOp("archive")}>
<ArchiveIcon />
</IconButton>
</Tooltip>
)}
{activeCollectionID !== PseudoCollectionID.all &&
activeCollectionID != PseudoCollectionID.archiveItems &&
!isFavoriteCollection && (
<>
<Tooltip title={t("move")}>
<IconButton onClick={moveToCollection}>
<MoveIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("remove")}>
<IconButton
onClick={removeFromCollectionHandler}
>
<RemoveIcon />
</IconButton>
</Tooltip>
</>
)}
<Tooltip title={t("hide")}>
<IconButton onClick={handleFileOp("hide")}>
<VisibilityOffOutlinedIcon />
</IconButton>
</Tooltip>
<Tooltip title={t("delete")}>
<IconButton onClick={trashHandler}>
<DeleteIcon />
</IconButton>
</Tooltip>
</>
)}
</SpacedRow>
);
};
export default SelectedFileOptions;

View File

@@ -13,7 +13,6 @@ import {
import { FixCreationTime } from "components/FixCreationTime";
import { Sidebar } from "components/Sidebar";
import { Upload } from "components/Upload";
import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions";
import { sessionExpiredDialogAttributes } from "ente-accounts/components/utils/dialog";
import { stashRedirect } from "ente-accounts/services/redirect";
import { isSessionInvalid } from "ente-accounts/services/session";
@@ -49,6 +48,11 @@ import {
SearchBar,
type SearchBarProps,
} from "ente-new/photos/components/SearchBar";
import {
SelectedFileOptions,
type CollectionOp,
type FileOp,
} from "ente-new/photos/components/SelectedFileOptions";
import { WhatsNew } from "ente-new/photos/components/WhatsNew";
import {
GalleryEmptyState,
@@ -121,12 +125,8 @@ import {
SetFilesDownloadProgressAttributes,
SetFilesDownloadProgressAttributesCreator,
} from "types/gallery";
import {
getSelectedCollection,
handleCollectionOp,
type CollectionOp,
} from "utils/collection";
import { getSelectedFiles, handleFileOp, type FileOp } from "utils/file";
import { handleCollectionOp } from "utils/collection";
import { getSelectedFiles, handleFileOp } from "utils/file";
/**
* The default view for logged in users.
@@ -921,22 +921,21 @@ const Page: React.FC = () => {
>
{showSelectionBar ? (
<SelectedFileOptions
barMode={barMode}
isInSearchMode={isInSearchMode}
collection={
isInSearchMode ? undefined : activeCollection
}
collectionSummary={
isInSearchMode ? undefined : activeCollectionSummary
}
selectedFileCount={selected.count}
selectedOwnFileCount={selected.ownCount}
onClearSelection={clearSelection}
onShowCreateCollectionModal={handleCreateAlbumForOp}
onOpenCollectionSelector={handleOpenCollectionSelector}
handleCollectionOp={collectionOpsHelper}
handleFileOp={fileOpHelper}
showCreateCollectionModal={handleCreateAlbumForOp}
onOpenCollectionSelector={handleOpenCollectionSelector}
count={selected.count}
ownCount={selected.ownCount}
clearSelection={clearSelection}
barMode={barMode}
activeCollectionID={activeCollectionID}
selectedCollection={getSelectedCollection(
selected.collectionID,
state.collections,
)}
activeCollectionSummary={activeCollectionSummary}
isInSearchMode={isInSearchMode}
isInHiddenSection={barMode == "hidden-albums"}
/>
) : barMode == "hidden-albums" ? (
<HiddenSectionNavbarContents
@@ -993,23 +992,23 @@ const Page: React.FC = () => {
uploadTypeSelectorIntent,
uploadTypeSelectorView,
}}
isFirstUpload={haveOnlySystemCollections(
normalCollectionSummaries,
)}
activeCollection={activeCollection}
closeUploadTypeSelector={setUploadTypeSelectorView.bind(
null,
false,
)}
onOpenCollectionSelector={handleOpenCollectionSelector}
onCloseCollectionSelector={handleCloseCollectionSelector}
setLoading={setBlockingLoad}
setShouldDisableDropzone={setShouldDisableDropzone}
onRemotePull={remotePull}
onRemoteFilesPull={remoteFilesPull}
onOpenCollectionSelector={handleOpenCollectionSelector}
onCloseCollectionSelector={handleCloseCollectionSelector}
onUploadFile={(file) => dispatch({ type: "uploadFile", file })}
onShowPlanSelector={showPlanSelector}
isFirstUpload={haveOnlySystemCollections(
normalCollectionSummaries,
)}
showSessionExpiredMessage={showSessionExpiredDialog}
onShowSessionExpiredDialog={showSessionExpiredDialog}
/>
<Sidebar
{...sidebarVisibilityProps}

View File

@@ -495,9 +495,9 @@ export default function PublicCollectionGallery() {
>
{selected.count > 0 ? (
<SelectedFileOptions
downloadFilesHelper={downloadFilesHelper}
clearSelection={clearSelection}
count={selected.count}
clearSelection={clearSelection}
downloadFilesHelper={downloadFilesHelper}
/>
) : (
<SpacedRow sx={{ flex: 1 }}>
@@ -531,14 +531,14 @@ export default function PublicCollectionGallery() {
uploadCollection={publicCollection}
setLoading={setBlockingLoad}
setShouldDisableDropzone={setShouldDisableDropzone}
uploadTypeSelectorIntent="collect"
uploadTypeSelectorView={uploadTypeSelectorView}
onRemotePull={publicAlbumsRemotePull}
onUploadFile={(file) =>
setPublicFiles(sortFiles([...publicFiles, file]))
}
uploadTypeSelectorView={uploadTypeSelectorView}
closeUploadTypeSelector={closeUploadTypeSelectorView}
showSessionExpiredMessage={showPublicLinkExpiredMessage}
uploadTypeSelectorIntent="collect"
onShowSessionExpiredDialog={showPublicLinkExpiredMessage}
{...{ dragAndDropFiles }}
/>
<FilesDownloadProgress
@@ -616,9 +616,9 @@ interface SelectedFileOptionsProps {
}
const SelectedFileOptions: React.FC<SelectedFileOptionsProps> = ({
downloadFilesHelper,
count,
clearSelection,
downloadFilesHelper,
}) => (
<Stack
direction="row"

View File

@@ -5,6 +5,7 @@ import log from "ente-base/log";
import { uniqueFilesByID } from "ente-gallery/utils/file";
import { type Collection, CollectionSubType } from "ente-media/collection";
import { EnteFile } from "ente-media/file";
import { type CollectionOp } from "ente-new/photos/components/SelectedFileOptions";
import {
addToCollection,
createAlbum,
@@ -29,8 +30,6 @@ import {
} from "types/gallery";
import { downloadFilesWithProgress } from "utils/file";
export type CollectionOp = "add" | "move" | "remove" | "restore" | "unhide";
export async function handleCollectionOp(
op: CollectionOp,
collection: Collection,
@@ -60,13 +59,6 @@ export async function handleCollectionOp(
}
}
export function getSelectedCollection(
collectionID: number,
collections: Collection[],
) {
return collections.find((collection) => collection.id === collectionID);
}
export async function downloadCollectionHelper(
collectionID: number,
setFilesDownloadProgressAttributes: SetFilesDownloadProgressAttributes,

View File

@@ -14,6 +14,7 @@ import {
} from "ente-media/file-metadata";
import { FileType } from "ente-media/file-type";
import { decodeLivePhoto } from "ente-media/live-photo";
import { type FileOp } from "ente-new/photos/components/SelectedFileOptions";
import {
addToFavorites,
deleteFromTrash,
@@ -31,16 +32,6 @@ import {
SetFilesDownloadProgressAttributesCreator,
} from "types/gallery";
export type FileOp =
| "download"
| "fixTime"
| "favorite"
| "archive"
| "unarchive"
| "hide"
| "trash"
| "deletePermanently";
function getSelectedFileIds(selectedFiles: SelectedState) {
const filesIDs: number[] = [];
for (const [key, val] of Object.entries(selectedFiles)) {

View File

@@ -39,6 +39,14 @@ export interface CollectionSelectorAttributes {
* particular action.
*/
action: CollectionSelectorAction;
/**
* Some actions, like "add" and "move", happen in the context of an existing
* collection summary.
*
* In such cases, the ID of the collection summary can be set as the
* {@link sourceCollectionID} to omit showing it in the list again.
*/
sourceCollectionSummaryID?: number;
/**
* Callback invoked when the user selects one the existing collections
* listed in the dialog.
@@ -53,12 +61,6 @@ export interface CollectionSelectorAttributes {
* Callback invoked when the user cancels the collection selection dialog.
*/
onCancel?: () => void;
/**
* Some actions, like "add" and "move", happen in the context of an existing
* collection. In such cases, the ID of this collection can be set as the
* {@link relatedCollectionID} to omit showing it in the list again.
*/
relatedCollectionID?: number | undefined;
}
type CollectionSelectorProps = ModalVisibilityProps & {
@@ -123,7 +125,7 @@ export const CollectionSelector: React.FC<CollectionSelectorProps> = ({
const collections = [...collectionSummaries.values()]
.filter((cs) => {
if (cs.id === attributes.relatedCollectionID) {
if (cs.id === attributes.sourceCollectionSummaryID) {
return false;
} else if (attributes.action == "add") {
return canAddToCollection(cs);

View File

@@ -0,0 +1,403 @@
import ClockIcon from "@mui/icons-material/AccessTime";
import AddIcon from "@mui/icons-material/Add";
import ArchiveIcon from "@mui/icons-material/ArchiveOutlined";
import MoveIcon from "@mui/icons-material/ArrowForward";
import CloseIcon from "@mui/icons-material/Close";
import DeleteIcon from "@mui/icons-material/Delete";
import DownloadIcon from "@mui/icons-material/Download";
import FavoriteBorderIcon from "@mui/icons-material/FavoriteBorderRounded";
import RemoveIcon from "@mui/icons-material/RemoveCircleOutline";
import RestoreIcon from "@mui/icons-material/Restore";
import UnArchiveIcon from "@mui/icons-material/Unarchive";
import VisibilityOffOutlinedIcon from "@mui/icons-material/VisibilityOffOutlined";
import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined";
import { IconButton, Tooltip, Typography } from "@mui/material";
import { SpacedRow } from "ente-base/components/containers";
import type { ButtonishProps } from "ente-base/components/mui";
import { useBaseContext } from "ente-base/context";
import type { Collection } from "ente-media/collection";
import type { CollectionSelectorAttributes } from "ente-new/photos/components/CollectionSelector";
import type { GalleryBarMode } from "ente-new/photos/components/gallery/reducer";
import {
PseudoCollectionID,
type CollectionSummary,
} from "ente-new/photos/services/collection-summary";
import { t } from "i18next";
export type CollectionOp = "add" | "move" | "remove" | "restore" | "unhide";
export type FileOp =
| "download"
| "fixTime"
| "favorite"
| "archive"
| "unarchive"
| "hide"
| "trash"
| "deletePermanently";
interface SelectedFileOptionsProps {
barMode?: GalleryBarMode;
isInSearchMode: boolean;
selectedCollection?: Collection;
/**
* If {@link collectionSummary} is set and is not a pseudo-collection, then
* this will be set to the corresponding {@link Collection}.
*/
collection: Collection | undefined;
/**
* The collection summary in whose context the selection happened.
*
* This will not be set if we are in the people section, or if we are
* showing search results.
*
* TODO: Need to implement delete-equivalent from shared albums.
*
* Notes:
*
* - Delete action should not be enabled 3 selected (0 Yours). There should
* be separate remove action.
*
* - On remove, if the file and collection both belong to current user, we
* just use move api to existing or uncat collection.
*
* - Otherwise, we call /collections/v3/remove-files (when collection and
* file belong to different users).
*
* - Album owner can remove files of all other users from their collection.
* Particiapant (viewer/collaborator) can only remove files that belong to
* them.
*
* Also note that that user cannot delete files that are not owned by the
* user, even if they are in an album owned by the user.
*/
collectionSummary: CollectionSummary | undefined;
/**
* The total number of files selected by the user.
*/
selectedFileCount: number;
/**
* The subset of {@link selectedFileCount} that are also owned by the user.
*/
selectedOwnFileCount: number;
/**
* Called when the user clears the selection by pressing the cancel button
* on the selection bar.
*/
onClearSelection: () => void;
/**
* Called when an operation requires prompting the user to create a new
* collection (e.g. adding to a new album).
*
* The callback is also passed the operation that caused it to be shown.
*/
onShowCreateCollectionModal: (op: CollectionOp) => () => void;
/**
* Callback to open a dialog where the user can choose a collection.
*
* The reason for opening the dialog and other properties are passed as the
* {@link attributes} argument.
*/
onOpenCollectionSelector: (
attributes: CollectionSelectorAttributes,
) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handleCollectionOp: (op: CollectionOp) => (...args: any[]) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handleFileOp: (op: FileOp) => (...args: any[]) => void;
}
/**
* The selection bar shown at the top of the viewport when the user has selected
* one or more files in the photos app gallery.
*/
export const SelectedFileOptions: React.FC<SelectedFileOptionsProps> = ({
barMode,
isInSearchMode,
collection,
collectionSummary,
selectedFileCount,
selectedOwnFileCount,
onClearSelection,
onShowCreateCollectionModal,
onOpenCollectionSelector,
handleCollectionOp,
handleFileOp,
}) => {
const { showMiniDialog } = useBaseContext();
const isUserFavorites =
!!collectionSummary?.attributes.has("userFavorites");
const handleUnhide = () => {
onOpenCollectionSelector({
action: "unhide",
onSelectCollection: handleCollectionOp("unhide"),
onCreateCollection: onShowCreateCollectionModal("unhide"),
});
};
const handleDelete = () =>
showMiniDialog({
title: t("trash_files_title"),
message: t("trash_files_message"),
continue: {
text: t("move_to_trash"),
color: "critical",
action: handleFileOp("trash"),
},
});
const handleRestore = () =>
onOpenCollectionSelector({
action: "restore",
onSelectCollection: handleCollectionOp("restore"),
onCreateCollection: onShowCreateCollectionModal("restore"),
});
const handleDeletePermanently = () =>
showMiniDialog({
title: t("delete_files_title"),
message: t("delete_files_message"),
continue: {
text: t("delete"),
color: "critical",
action: handleFileOp("deletePermanently"),
},
});
const handleAddToCollection = () =>
onOpenCollectionSelector({
action: "add",
sourceCollectionSummaryID: collectionSummary?.id,
onSelectCollection: handleCollectionOp("add"),
onCreateCollection: onShowCreateCollectionModal("add"),
});
const handleRemoveFromOwnCollection = () => {
showMiniDialog(
selectedFileCount == selectedOwnFileCount
? {
title: t("remove_from_album"),
message: t("confirm_remove_message"),
continue: {
text: t("yes_remove"),
color: "primary",
action: () =>
handleCollectionOp("remove")(collection),
},
}
: {
title: t("remove_from_album"),
message: t("confirm_remove_incl_others_message"),
continue: {
text: t("yes_remove"),
color: "critical",
action: () =>
handleCollectionOp("remove")(collection),
},
},
);
};
const handleMoveToCollection = () => {
onOpenCollectionSelector({
action: "move",
sourceCollectionSummaryID: collectionSummary?.id,
onSelectCollection: handleCollectionOp("move"),
onCreateCollection: onShowCreateCollectionModal("move"),
});
};
return (
<SpacedRow sx={{ flex: 1, gap: 1, flexWrap: "wrap" }}>
<IconButton onClick={onClearSelection}>
<CloseIcon />
</IconButton>
<Typography sx={{ mr: "auto" }}>
{selectedFileCount == selectedOwnFileCount
? t("selected_count", { selected: selectedFileCount })
: t("selected_and_yours_count", {
selected: selectedFileCount,
yours: selectedOwnFileCount,
})}
</Typography>
{isInSearchMode ? (
<>
<FixTimeButton onClick={handleFileOp("fixTime")} />
<DownloadButton onClick={handleFileOp("download")} />
<AddToCollectionButton onClick={handleAddToCollection} />
<ArchiveButton onClick={handleFileOp("archive")} />
<HideButton onClick={handleFileOp("hide")} />
<DeleteButton onClick={handleDelete} />
</>
) : barMode == "people" ? (
<>
<DownloadButton onClick={handleFileOp("download")} />
<AddToCollectionButton onClick={handleAddToCollection} />
<ArchiveButton onClick={handleFileOp("archive")} />
<HideButton onClick={handleFileOp("hide")} />
<DeleteButton onClick={handleDelete} />
</>
) : collectionSummary?.id == PseudoCollectionID.trash ? (
<>
<RestoreButton onClick={handleRestore} />
<DeletePermanentlyButton
onClick={handleDeletePermanently}
/>
</>
) : collectionSummary?.attributes.has("uncategorized") ? (
<>
<DownloadButton onClick={handleFileOp("download")} />
<MoveToCollectionButton onClick={handleMoveToCollection} />
<DeleteButton onClick={handleDelete} />
</>
) : collectionSummary?.attributes.has("sharedIncoming") ? (
<DownloadButton onClick={handleFileOp("download")} />
) : barMode == "hidden-albums" ? (
<>
<DownloadButton onClick={handleFileOp("download")} />
<UnhideButton onClick={handleUnhide} />
<DeleteButton onClick={handleDelete} />
</>
) : (
<>
{!isUserFavorites &&
collectionSummary?.id !=
PseudoCollectionID.archiveItems && (
<FavoriteButton
onClick={handleFileOp("favorite")}
/>
)}
<FixTimeButton onClick={handleFileOp("fixTime")} />
<DownloadButton onClick={handleFileOp("download")} />
<AddToCollectionButton onClick={handleAddToCollection} />
{collectionSummary?.id === PseudoCollectionID.all ? (
<ArchiveButton onClick={handleFileOp("archive")} />
) : collectionSummary?.id ==
PseudoCollectionID.archiveItems ? (
<UnarchiveButton onClick={handleFileOp("unarchive")} />
) : (
!isUserFavorites && (
<>
<MoveToCollectionButton
onClick={handleMoveToCollection}
/>
<RemoveFromCollectionButton
onClick={handleRemoveFromOwnCollection}
/>
</>
)
)}
<HideButton onClick={handleFileOp("hide")} />
<DeleteButton onClick={handleDelete} />
</>
)}
</SpacedRow>
);
};
const DownloadButton: React.FC<ButtonishProps> = ({ onClick }) => (
<Tooltip title={t("download")}>
<IconButton {...{ onClick }}>
<DownloadIcon />
</IconButton>
</Tooltip>
);
const FavoriteButton: React.FC<ButtonishProps> = ({ onClick }) => (
<Tooltip title={t("favorite")}>
<IconButton {...{ onClick }}>
<FavoriteBorderIcon />
</IconButton>
</Tooltip>
);
const ArchiveButton: React.FC<ButtonishProps> = ({ onClick }) => (
<Tooltip title={t("archive")}>
<IconButton {...{ onClick }}>
<ArchiveIcon />
</IconButton>
</Tooltip>
);
const UnarchiveButton: React.FC<ButtonishProps> = ({ onClick }) => (
<Tooltip title={t("unarchive")}>
<IconButton {...{ onClick }}>
<UnArchiveIcon />
</IconButton>
</Tooltip>
);
const HideButton: React.FC<ButtonishProps> = ({ onClick }) => (
<Tooltip title={t("hide")}>
<IconButton {...{ onClick }}>
<VisibilityOffOutlinedIcon />
</IconButton>
</Tooltip>
);
const UnhideButton: React.FC<ButtonishProps> = ({ onClick }) => (
<Tooltip title={t("unhide")}>
<IconButton {...{ onClick }}>
<VisibilityOutlinedIcon />
</IconButton>
</Tooltip>
);
const DeleteButton: React.FC<ButtonishProps> = ({ onClick }) => (
<Tooltip title={t("delete")}>
<IconButton {...{ onClick }}>
<DeleteIcon />
</IconButton>
</Tooltip>
);
const RestoreButton: React.FC<ButtonishProps> = ({ onClick }) => (
<Tooltip title={t("restore")}>
<IconButton {...{ onClick }}>
<RestoreIcon />
</IconButton>
</Tooltip>
);
const DeletePermanentlyButton: React.FC<ButtonishProps> = ({ onClick }) => (
<Tooltip title={t("delete_permanently")}>
<IconButton {...{ onClick }}>
<DeleteIcon />
</IconButton>
</Tooltip>
);
const FixTimeButton: React.FC<ButtonishProps> = ({ onClick }) => (
<Tooltip title={t("fix_creation_time")}>
<IconButton {...{ onClick }}>
<ClockIcon />
</IconButton>
</Tooltip>
);
const AddToCollectionButton: React.FC<ButtonishProps> = ({ onClick }) => (
<Tooltip title={t("add")}>
<IconButton {...{ onClick }}>
<AddIcon />
</IconButton>
</Tooltip>
);
const MoveToCollectionButton: React.FC<ButtonishProps> = ({ onClick }) => (
<Tooltip title={t("move")}>
<IconButton {...{ onClick }}>
<MoveIcon />
</IconButton>
</Tooltip>
);
const RemoveFromCollectionButton: React.FC<ButtonishProps> = ({ onClick }) => (
<Tooltip title={t("remove")}>
<IconButton {...{ onClick }}>
<RemoveIcon />
</IconButton>
</Tooltip>
);