mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-26 04:02:47 +00:00
418 lines
16 KiB
TypeScript
418 lines
16 KiB
TypeScript
import { Box, Divider, ListItemIcon, ListItemText, Menu, MenuItem, styled, Typography, useTheme } from "@mui/material";
|
|
import { useCallback, useEffect, useMemo } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { closeContextMenu } from "../../../redux/fileManagerSlice.ts";
|
|
import { CreateNewDialogType } from "../../../redux/globalStateSlice.ts";
|
|
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
|
import { downloadFiles } from "../../../redux/thunks/download.ts";
|
|
import {
|
|
batchGetDirectLinks,
|
|
createNew,
|
|
deleteFile,
|
|
dialogBasedMoveCopy,
|
|
enterFolder,
|
|
extractArchive,
|
|
goToParent,
|
|
goToSharedLink,
|
|
newRemoteDownload,
|
|
openShareDialog,
|
|
openSidebar,
|
|
renameFile,
|
|
restoreFile,
|
|
} from "../../../redux/thunks/file.ts";
|
|
import { refreshFileList, uploadClicked, uploadFromClipboard } from "../../../redux/thunks/filemanager.ts";
|
|
import { openViewers } from "../../../redux/thunks/viewer.ts";
|
|
import { primeThumbExtsCache } from "../../../redux/thunks/thumb.ts";
|
|
import AppFolder from "../../Icons/AppFolder.tsx";
|
|
import ArchiveArrow from "../../Icons/ArchiveArrow.tsx";
|
|
import ArrowSync from "../../Icons/ArrowSync.tsx";
|
|
import BinFullOutlined from "../../Icons/BinFullOutlined.tsx";
|
|
import Clipboard from "../../Icons/Clipboard.tsx";
|
|
import CloudDownloadOutlined from "../../Icons/CloudDownloadOutlined.tsx";
|
|
import CopyOutlined from "../../Icons/CopyOutlined.tsx";
|
|
import DeleteOutlined from "../../Icons/DeleteOutlined.tsx";
|
|
import Download from "../../Icons/Download.tsx";
|
|
import Enter from "../../Icons/Enter.tsx";
|
|
import FileAdd from "../../Icons/FileAdd.tsx";
|
|
import FolderAdd from "../../Icons/FolderAdd.tsx";
|
|
import FolderArrowUp from "../../Icons/FolderArrowUp.tsx";
|
|
import FolderLink from "../../Icons/FolderLink.tsx";
|
|
import FolderOutlined from "../../Icons/FolderOutlined.tsx";
|
|
import HistoryOutlined from "../../Icons/HistoryOutlined.tsx";
|
|
import Info from "../../Icons/Info.tsx";
|
|
import LinkOutlined from "../../Icons/LinkOutlined.tsx";
|
|
import Open from "../../Icons/Open.tsx";
|
|
import RenameOutlined from "../../Icons/RenameOutlined.tsx";
|
|
import ShareOutlined from "../../Icons/ShareOutlined.tsx";
|
|
import Tag from "../../Icons/Tag.tsx";
|
|
import Upload from "../../Icons/Upload.tsx";
|
|
import WrenchSettings from "../../Icons/WrenchSettings.tsx";
|
|
import { SelectType } from "../../Uploader/core";
|
|
import { CascadingSubmenu } from "./CascadingMenu.tsx";
|
|
import MoreMenuItems from "./MoreMenuItems.tsx";
|
|
import NewFileTemplateMenuItems from "./NewFileTemplateMenuItems.tsx";
|
|
import OpenWithMenuItems from "./OpenWithMenuItems.tsx";
|
|
import OrganizeMenuItems from "./OrganizeMenuItems.tsx";
|
|
import TagMenuItems from "./TagMenuItems.tsx";
|
|
import useActionDisplayOpt from "./useActionDisplayOpt.ts";
|
|
|
|
export const SquareMenu = styled(Menu)(() => ({
|
|
"& .MuiPaper-root": {
|
|
minWidth: "200px",
|
|
},
|
|
}));
|
|
|
|
export const SquareMenuItem = styled(MenuItem)<{ hoverColor?: string }>(({ theme, hoverColor }) => ({
|
|
"&:hover .MuiListItemIcon-root": {
|
|
color: hoverColor ?? theme.palette.primary.main,
|
|
},
|
|
}));
|
|
|
|
export const DenseDivider = styled(Divider)(() => ({
|
|
margin: "4px 0 !important",
|
|
}));
|
|
|
|
export const EmptyMenu = () => {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<Box sx={{ py: 0.5, px: 1, display: "flex", alignItems: "center" }} color={"text.secondary"}>
|
|
<Info sx={{ mr: 1 }} />
|
|
<Typography variant="body2">{t("fileManager.noActionsCanBeDone")}</Typography>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export interface ContextMenuProps {
|
|
fmIndex: number;
|
|
}
|
|
|
|
const ContextMenu = ({ fmIndex = 0 }: ContextMenuProps) => {
|
|
const { t } = useTranslation();
|
|
const dispatch = useAppDispatch();
|
|
const theme = useTheme();
|
|
const contextMenuOpen = useAppSelector((state) => state.fileManager[fmIndex].contextMenuOpen);
|
|
const contextMenuType = useAppSelector((state) => state.fileManager[fmIndex].contextMenuType);
|
|
const contextMenuPos = useAppSelector((state) => state.fileManager[fmIndex].contextMenuPos);
|
|
const selected = useAppSelector((state) => state.fileManager[fmIndex].selected);
|
|
const targetOverwrite = useAppSelector((state) => state.fileManager[fmIndex].contextMenuTargets);
|
|
|
|
const targets = useMemo(() => {
|
|
const targetsMap = targetOverwrite ?? selected;
|
|
return Object.keys(targetsMap).map((key) => targetsMap[key]);
|
|
}, [targetOverwrite, selected]);
|
|
|
|
const parent = useAppSelector((state) => state.fileManager[fmIndex].list?.parent);
|
|
|
|
const displayOpt = useActionDisplayOpt(targets, contextMenuType, parent, fmIndex);
|
|
const onClose = useCallback(() => {
|
|
dispatch(closeContextMenu({ index: fmIndex, value: undefined }));
|
|
}, [dispatch]);
|
|
|
|
// Ensure supported thumbnail extensions are primed when menu opens
|
|
useEffect(() => {
|
|
if (contextMenuOpen) {
|
|
dispatch(primeThumbExtsCache());
|
|
}
|
|
}, [contextMenuOpen, dispatch]);
|
|
|
|
const showOpenWithCascading = displayOpt.showOpenWithCascading && displayOpt.showOpenWithCascading();
|
|
const showOpenWith = displayOpt.showOpenWith && displayOpt.showOpenWith();
|
|
let part1 =
|
|
displayOpt.showOpen ||
|
|
showOpenWithCascading ||
|
|
showOpenWith ||
|
|
displayOpt.showEnter ||
|
|
displayOpt.showDownload ||
|
|
displayOpt.showRemoteDownload ||
|
|
displayOpt.showTorrentRemoteDownload ||
|
|
displayOpt.showExtractArchive ||
|
|
displayOpt.showUpload;
|
|
let part2 =
|
|
displayOpt.showCreateFolder ||
|
|
displayOpt.showCreateFile ||
|
|
displayOpt.showShare ||
|
|
displayOpt.showRename ||
|
|
displayOpt.showCopy ||
|
|
displayOpt.showDirectLink;
|
|
let part3 =
|
|
displayOpt.showTags || displayOpt.showOrganize || displayOpt.showMore || displayOpt.showNewFileFromTemplate;
|
|
let part4 = displayOpt.showInfo || displayOpt.showGoToParent || displayOpt.showGoToSharedLink;
|
|
let part5 = displayOpt.showRestore || displayOpt.showDelete || displayOpt.showRefresh;
|
|
const showDivider1 = part1 && part2;
|
|
const showDivider2 = part2 && part3;
|
|
const showDivider3 = part3 && part4;
|
|
const showDivider4 = part4 && part5;
|
|
|
|
const part1Elements = part1 ? (
|
|
<>
|
|
{displayOpt.showUpload && (
|
|
<SquareMenuItem onClick={() => dispatch(uploadClicked(0, SelectType.File))}>
|
|
<ListItemIcon>
|
|
<Upload fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.uploadFiles")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showEnter && (
|
|
<SquareMenuItem onClick={() => dispatch(enterFolder(0, targets[0]))}>
|
|
<ListItemIcon>
|
|
<Enter fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.enter")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showUpload && (
|
|
<SquareMenuItem onClick={() => dispatch(uploadClicked(0, SelectType.Directory))}>
|
|
<ListItemIcon>
|
|
<FolderArrowUp fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.uploadFolder")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showUpload && (
|
|
<SquareMenuItem onClick={() => dispatch(uploadFromClipboard(0))}>
|
|
<ListItemIcon>
|
|
<Clipboard fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:uploader.uploadFromClipboard")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showRemoteDownload && (
|
|
<SquareMenuItem onClick={() => dispatch(newRemoteDownload(0))}>
|
|
<ListItemIcon>
|
|
<CloudDownloadOutlined fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.newRemoteDownloads")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showOpen && (
|
|
<SquareMenuItem onClick={() => dispatch(openViewers(fmIndex, targets[0]))}>
|
|
<ListItemIcon>
|
|
<Open fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.open")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{showOpenWithCascading && (
|
|
<CascadingSubmenu
|
|
popupId={"openWith"}
|
|
title={t("application:fileManager.openWith")}
|
|
icon={<AppFolder fontSize="small" />}
|
|
>
|
|
<OpenWithMenuItems displayOpt={displayOpt} targets={targets} />
|
|
</CascadingSubmenu>
|
|
)}
|
|
{showOpenWith && (
|
|
<SquareMenuItem onClick={() => dispatch(openViewers(fmIndex, targets[0], targets[0].size, undefined, true))}>
|
|
<ListItemIcon>
|
|
<AppFolder fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.openWith")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showDownload && (
|
|
<SquareMenuItem onClick={() => dispatch(downloadFiles(fmIndex, targets))}>
|
|
<ListItemIcon>
|
|
<Download fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.download")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showExtractArchive && (
|
|
<SquareMenuItem onClick={() => dispatch(extractArchive(fmIndex, targets[0]))}>
|
|
<ListItemIcon>
|
|
<ArchiveArrow fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.extractArchive")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showTorrentRemoteDownload && (
|
|
<SquareMenuItem onClick={() => dispatch(newRemoteDownload(fmIndex, targets[0]))}>
|
|
<ListItemIcon>
|
|
<CloudDownloadOutlined fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.createRemoteDownloadForTorrent")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
</>
|
|
) : undefined;
|
|
|
|
const part2Elements = part2 ? (
|
|
<>
|
|
{displayOpt.showCreateFolder && (
|
|
<SquareMenuItem onClick={() => dispatch(createNew(fmIndex, CreateNewDialogType.folder))}>
|
|
<ListItemIcon>
|
|
<FolderAdd fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.newFolder")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showCreateFile && (
|
|
<SquareMenuItem onClick={() => dispatch(createNew(fmIndex, CreateNewDialogType.file))}>
|
|
<ListItemIcon>
|
|
<FileAdd fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.newFile")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showShare && (
|
|
<SquareMenuItem onClick={() => dispatch(openShareDialog(fmIndex, targets[0]))}>
|
|
<ListItemIcon>
|
|
<ShareOutlined fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.share")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showRename && (
|
|
<SquareMenuItem onClick={() => dispatch(renameFile(fmIndex, targets[0]))}>
|
|
<ListItemIcon>
|
|
<RenameOutlined fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.rename")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showCopy && (
|
|
<SquareMenuItem onClick={() => dispatch(dialogBasedMoveCopy(fmIndex, targets, true))}>
|
|
<ListItemIcon>
|
|
<CopyOutlined fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.copy")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showDirectLink && (
|
|
<SquareMenuItem onClick={() => dispatch(batchGetDirectLinks(fmIndex, targets))}>
|
|
<ListItemIcon>
|
|
<LinkOutlined fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.getSourceLink")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
</>
|
|
) : undefined;
|
|
|
|
const part3Elements = part3 ? (
|
|
<>
|
|
{displayOpt.showTags && (
|
|
<CascadingSubmenu popupId={"tags"} title={t("application:fileManager.tags")} icon={<Tag fontSize="small" />}>
|
|
<TagMenuItems displayOpt={displayOpt} targets={targets} />
|
|
</CascadingSubmenu>
|
|
)}
|
|
{displayOpt.showOrganize && (
|
|
<CascadingSubmenu
|
|
popupId={"organize"}
|
|
title={t("application:fileManager.organize")}
|
|
icon={<BinFullOutlined fontSize="small" />}
|
|
>
|
|
<OrganizeMenuItems displayOpt={displayOpt} targets={targets} />
|
|
</CascadingSubmenu>
|
|
)}
|
|
{displayOpt.showMore && (
|
|
<CascadingSubmenu
|
|
popupId={"more"}
|
|
title={t("application:fileManager.moreActions")}
|
|
icon={<WrenchSettings fontSize="small" />}
|
|
>
|
|
<MoreMenuItems displayOpt={displayOpt} targets={targets} />
|
|
</CascadingSubmenu>
|
|
)}
|
|
{displayOpt.showNewFileFromTemplate && <NewFileTemplateMenuItems displayOpt={displayOpt} targets={targets} />}
|
|
</>
|
|
) : undefined;
|
|
|
|
const part4Elements = part4 ? (
|
|
<>
|
|
{displayOpt.showGoToSharedLink && (
|
|
<SquareMenuItem onClick={() => dispatch(goToSharedLink(fmIndex, targets[0]))}>
|
|
<ListItemIcon>
|
|
<FolderLink fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.goToSharedLink")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showGoToParent && (
|
|
<SquareMenuItem onClick={() => dispatch(goToParent(0, targets[0]))}>
|
|
<ListItemIcon>
|
|
<FolderOutlined fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.openParentFolder")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showInfo && (
|
|
<SquareMenuItem onClick={() => dispatch(openSidebar(fmIndex, targets[0]))}>
|
|
<ListItemIcon>
|
|
<Info fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.viewDetails")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
</>
|
|
) : undefined;
|
|
|
|
const part5Elements = part5 ? (
|
|
<>
|
|
{displayOpt.showRestore && (
|
|
<SquareMenuItem onClick={() => dispatch(restoreFile(fmIndex, targets))}>
|
|
<ListItemIcon>
|
|
<HistoryOutlined fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.restore")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showDelete && (
|
|
<SquareMenuItem hoverColor={theme.palette.error.light} onClick={() => dispatch(deleteFile(fmIndex, targets))}>
|
|
<ListItemIcon>
|
|
<DeleteOutlined fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.delete")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
{displayOpt.showRefresh && (
|
|
<SquareMenuItem onClick={() => dispatch(refreshFileList(fmIndex))}>
|
|
<ListItemIcon>
|
|
<ArrowSync fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t("application:fileManager.refresh")}</ListItemText>
|
|
</SquareMenuItem>
|
|
)}
|
|
</>
|
|
) : undefined;
|
|
|
|
const allParts = [part1Elements, part2Elements, part3Elements, part4Elements, part5Elements].filter(
|
|
(p) => p != undefined,
|
|
);
|
|
|
|
return (
|
|
<SquareMenu
|
|
keepMounted
|
|
onClose={onClose}
|
|
disableAutoFocusItem
|
|
open={Boolean(contextMenuOpen)}
|
|
anchorReference="anchorPosition"
|
|
anchorPosition={{
|
|
top: contextMenuPos?.y ?? 0,
|
|
left: contextMenuPos?.x ?? 0,
|
|
}}
|
|
MenuListProps={{
|
|
dense: true,
|
|
}}
|
|
componentsProps={{
|
|
root: {
|
|
onContextMenu: (e) => {
|
|
e.preventDefault();
|
|
},
|
|
},
|
|
}}
|
|
>
|
|
{allParts.map((part, index) => (
|
|
<>
|
|
{part}
|
|
{index < allParts.length - 1 && <DenseDivider />}
|
|
</>
|
|
))}
|
|
{allParts.length == 0 && <EmptyMenu />}
|
|
</SquareMenu>
|
|
);
|
|
};
|
|
|
|
export default ContextMenu;
|