diff --git a/web/apps/photos/src/components/Upload.tsx b/web/apps/photos/src/components/Upload.tsx index 2f816ba191..2b68bc93d0 100644 --- a/web/apps/photos/src/components/Upload.tsx +++ b/web/apps/photos/src/components/Upload.tsx @@ -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; + /** + * 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 = ({ dragAndDropFiles, onRemotePull, onRemoteFilesPull, + onOpenCollectionSelector, + onCloseCollectionSelector, onUploadFile, onShowPlanSelector, - showSessionExpiredMessage, + onShowSessionExpiredDialog, ...props }) => { const { showMiniDialog, onGenericError } = useBaseContext(); @@ -570,7 +576,7 @@ export const Upload: React.FC = ({ }; } - props.onOpenCollectionSelector({ + onOpenCollectionSelector({ action: "upload", onSelectCollection: uploadFilesToExistingCollection, onCreateCollection: showNextModal, @@ -580,7 +586,7 @@ export const Upload: React.FC = ({ }, [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 = ({ const notifyUser = (e: unknown) => { switch (e instanceof Error && e.message) { case sessionExpiredErrorMessage: - showSessionExpiredMessage(); + onShowSessionExpiredDialog(); break; case subscriptionExpiredErrorMessage: showNotification({ diff --git a/web/apps/photos/src/components/pages/gallery/SelectedFileOptions.tsx b/web/apps/photos/src/components/pages/gallery/SelectedFileOptions.tsx deleted file mode 100644 index e69fa522d1..0000000000 --- a/web/apps/photos/src/components/pages/gallery/SelectedFileOptions.tsx +++ /dev/null @@ -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 ( - - - - - - {ownCount === count - ? t("selected_count", { selected: count }) - : t("selected_and_yours_count", { - selected: count, - yours: ownCount, - })} - - - {isInSearchMode ? ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : peopleMode ? ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : activeCollectionID == PseudoCollectionID.trash ? ( - <> - - - - - - - - - - - - ) : isUncategorizedCollection ? ( - <> - - - - - - - - - - - - - - - - - ) : isSharedIncomingCollection ? ( - - - - - - ) : isInHiddenSection ? ( - <> - - - - - - - - - - - - - - - - - - ) : ( - <> - - - - - - {!isFavoriteCollection && - activeCollectionID != - PseudoCollectionID.archiveItems && ( - - - - - - )} - - - - - - - - - - - {activeCollectionID == PseudoCollectionID.archiveItems && ( - - - - - - )} - {activeCollectionID === PseudoCollectionID.all && ( - - - - - - )} - {activeCollectionID !== PseudoCollectionID.all && - activeCollectionID != PseudoCollectionID.archiveItems && - !isFavoriteCollection && ( - <> - - - - - - - - - - - - - )} - - - - - - - - - - - - )} - - ); -}; - -export default SelectedFileOptions; diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 898932b45f..5fd15a1a81 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -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 ? ( ) : barMode == "hidden-albums" ? ( { 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} /> {selected.count > 0 ? ( ) : ( @@ -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 }} /> = ({ - downloadFilesHelper, count, clearSelection, + downloadFilesHelper, }) => ( collection.id === collectionID); -} - export async function downloadCollectionHelper( collectionID: number, setFilesDownloadProgressAttributes: SetFilesDownloadProgressAttributes, diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index cb1a4a03df..48354d8d7f 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -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)) { diff --git a/web/packages/new/photos/components/CollectionSelector.tsx b/web/packages/new/photos/components/CollectionSelector.tsx index 78e913b0b6..f95f42c049 100644 --- a/web/packages/new/photos/components/CollectionSelector.tsx +++ b/web/packages/new/photos/components/CollectionSelector.tsx @@ -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 = ({ 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); diff --git a/web/packages/new/photos/components/SelectedFileOptions.tsx b/web/packages/new/photos/components/SelectedFileOptions.tsx new file mode 100644 index 0000000000..28b2dcad8e --- /dev/null +++ b/web/packages/new/photos/components/SelectedFileOptions.tsx @@ -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 = ({ + 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 ( + + + + + + {selectedFileCount == selectedOwnFileCount + ? t("selected_count", { selected: selectedFileCount }) + : t("selected_and_yours_count", { + selected: selectedFileCount, + yours: selectedOwnFileCount, + })} + + + {isInSearchMode ? ( + <> + + + + + + + + ) : barMode == "people" ? ( + <> + + + + + + + ) : collectionSummary?.id == PseudoCollectionID.trash ? ( + <> + + + + ) : collectionSummary?.attributes.has("uncategorized") ? ( + <> + + + + + ) : collectionSummary?.attributes.has("sharedIncoming") ? ( + + ) : barMode == "hidden-albums" ? ( + <> + + + + + ) : ( + <> + {!isUserFavorites && + collectionSummary?.id != + PseudoCollectionID.archiveItems && ( + + )} + + + + {collectionSummary?.id === PseudoCollectionID.all ? ( + + ) : collectionSummary?.id == + PseudoCollectionID.archiveItems ? ( + + ) : ( + !isUserFavorites && ( + <> + + + + ) + )} + + + + )} + + ); +}; + +const DownloadButton: React.FC = ({ onClick }) => ( + + + + + +); + +const FavoriteButton: React.FC = ({ onClick }) => ( + + + + + +); + +const ArchiveButton: React.FC = ({ onClick }) => ( + + + + + +); + +const UnarchiveButton: React.FC = ({ onClick }) => ( + + + + + +); + +const HideButton: React.FC = ({ onClick }) => ( + + + + + +); + +const UnhideButton: React.FC = ({ onClick }) => ( + + + + + +); + +const DeleteButton: React.FC = ({ onClick }) => ( + + + + + +); + +const RestoreButton: React.FC = ({ onClick }) => ( + + + + + +); + +const DeletePermanentlyButton: React.FC = ({ onClick }) => ( + + + + + +); + +const FixTimeButton: React.FC = ({ onClick }) => ( + + + + + +); + +const AddToCollectionButton: React.FC = ({ onClick }) => ( + + + + + +); + +const MoveToCollectionButton: React.FC = ({ onClick }) => ( + + + + + +); + +const RemoveFromCollectionButton: React.FC = ({ onClick }) => ( + + + + + +);