first commit
This commit is contained in:
118
src/component/FileManager/ContextMenu/CascadingMenu.tsx
Executable file
118
src/component/FileManager/ContextMenu/CascadingMenu.tsx
Executable file
@@ -0,0 +1,118 @@
|
||||
import { Box, ListItemIcon, ListItemText, Menu, MenuItemProps, styled, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { bindFocus, bindHover } from "material-ui-popup-state";
|
||||
import { bindMenu, bindTrigger, PopupState, usePopupState } from "material-ui-popup-state/hooks";
|
||||
import { createContext, useCallback, useContext, useMemo } from "react";
|
||||
import CaretRight from "../../Icons/CaretRight.tsx";
|
||||
import { SquareMenuItem } from "./ContextMenu.tsx";
|
||||
import HoverMenu from "./HoverMenu.tsx";
|
||||
|
||||
export const CascadingContext = createContext<{
|
||||
parentPopupState?: PopupState;
|
||||
rootPopupState?: PopupState;
|
||||
}>({});
|
||||
|
||||
export const SquareHoverMenu = styled(HoverMenu)(() => ({
|
||||
"& .MuiPaper-root": {
|
||||
minWidth: "200px",
|
||||
},
|
||||
}));
|
||||
|
||||
export const SquareMenu = styled(Menu)(() => ({
|
||||
"& .MuiPaper-root": {
|
||||
minWidth: "200px",
|
||||
},
|
||||
}));
|
||||
|
||||
export interface CascadingMenuItem {
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function CascadingMenuItem({ onClick, ...props }: CascadingMenuItem) {
|
||||
const { rootPopupState } = useContext(CascadingContext);
|
||||
if (!rootPopupState) throw new Error("must be used inside a CascadingMenu");
|
||||
const handleClick = useCallback(
|
||||
(event: React.MouseEvent<HTMLElement>) => {
|
||||
rootPopupState.close();
|
||||
if (onClick) onClick(event);
|
||||
},
|
||||
[rootPopupState, onClick],
|
||||
);
|
||||
|
||||
return <SquareMenuItem {...props} onClick={handleClick} />;
|
||||
}
|
||||
|
||||
export interface CascadingMenuProps {
|
||||
popupState: PopupState;
|
||||
isMobile?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function CascadingMenu({ popupState, isMobile, ...props }: CascadingMenuProps) {
|
||||
const { rootPopupState } = useContext(CascadingContext);
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
rootPopupState: rootPopupState || popupState,
|
||||
parentPopupState: popupState,
|
||||
}),
|
||||
[rootPopupState, popupState],
|
||||
);
|
||||
|
||||
const MenuComponent = isMobile ? SquareMenu : SquareHoverMenu;
|
||||
|
||||
return (
|
||||
<CascadingContext.Provider value={context}>
|
||||
<MenuComponent {...props} {...bindMenu(popupState)} />
|
||||
</CascadingContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export interface CascadingSubmenu {
|
||||
title: string;
|
||||
icon?: JSX.Element;
|
||||
popupId: string;
|
||||
menuItemProps?: MenuItemProps;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function CascadingSubmenu({ title, popupId, menuItemProps, icon, ...props }: CascadingSubmenu) {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const { parentPopupState } = useContext(CascadingContext);
|
||||
const popupState = usePopupState({
|
||||
popupId,
|
||||
variant: "popover",
|
||||
parentPopupState,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<SquareMenuItem
|
||||
dense
|
||||
{...(isMobile ? bindTrigger(popupState) : bindHover(popupState))}
|
||||
{...(isMobile ? {} : bindFocus(popupState))}
|
||||
{...menuItemProps}
|
||||
>
|
||||
{icon && <ListItemIcon>{icon}</ListItemIcon>}
|
||||
<ListItemText
|
||||
primary={title}
|
||||
slotProps={{
|
||||
primary: { variant: "body2" },
|
||||
}}
|
||||
/>
|
||||
<Box color="text.secondary" fontSize={"body2"} sx={{ display: "flex" }}>
|
||||
<CaretRight fontSize={"inherit"} />
|
||||
</Box>
|
||||
</SquareMenuItem>
|
||||
<CascadingMenu
|
||||
{...props}
|
||||
isMobile={isMobile}
|
||||
anchorOrigin={{ vertical: isMobile ? "bottom" : "top", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "left" }}
|
||||
popupState={popupState}
|
||||
MenuListProps={{
|
||||
dense: true,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
409
src/component/FileManager/ContextMenu/ContextMenu.tsx
Executable file
409
src/component/FileManager/ContextMenu/ContextMenu.tsx
Executable file
@@ -0,0 +1,409 @@
|
||||
import { Box, Divider, ListItemIcon, ListItemText, Menu, MenuItem, styled, Typography, useTheme } from "@mui/material";
|
||||
import { useCallback, 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 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]);
|
||||
|
||||
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;
|
||||
23
src/component/FileManager/ContextMenu/HoverMenu.tsx
Executable file
23
src/component/FileManager/ContextMenu/HoverMenu.tsx
Executable file
@@ -0,0 +1,23 @@
|
||||
import * as React from "react";
|
||||
import { Menu, type MenuProps } from "@mui/material";
|
||||
|
||||
const HoverMenu: React.ComponentType<MenuProps> = React.forwardRef(function HoverMenu(props: MenuProps, ref): any {
|
||||
return (
|
||||
<Menu
|
||||
{...props}
|
||||
ref={ref}
|
||||
style={{ pointerEvents: "none", ...props.style }}
|
||||
slotProps={{
|
||||
...props.slotProps,
|
||||
paper: {
|
||||
...props.slotProps?.paper,
|
||||
style: {
|
||||
pointerEvents: "auto",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default HoverMenu;
|
||||
122
src/component/FileManager/ContextMenu/MoreMenuItems.tsx
Executable file
122
src/component/FileManager/ContextMenu/MoreMenuItems.tsx
Executable file
@@ -0,0 +1,122 @@
|
||||
import { ListItemIcon, ListItemText } from "@mui/material";
|
||||
import { useCallback, useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { closeContextMenu } from "../../../redux/fileManagerSlice.ts";
|
||||
import {
|
||||
setCreateArchiveDialog,
|
||||
setDirectLinkManagementDialog,
|
||||
setManageShareDialog,
|
||||
setVersionControlDialog,
|
||||
} from "../../../redux/globalStateSlice.ts";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import { resetThumbnails } from "../../../redux/thunks/file.ts";
|
||||
import Archive from "../../Icons/Archive.tsx";
|
||||
import BranchForkLink from "../../Icons/BranchForkLink.tsx";
|
||||
import HistoryOutlined from "../../Icons/HistoryOutlined.tsx";
|
||||
import ImageArrowCounterclockwise from "../../Icons/ImageAarowCounterclockwise.tsx";
|
||||
import LinkSetting from "../../Icons/LinkSetting.tsx";
|
||||
import { CascadingContext, CascadingMenuItem } from "./CascadingMenu.tsx";
|
||||
import { SubMenuItemsProps } from "./OrganizeMenuItems.tsx";
|
||||
|
||||
const MoreMenuItems = ({ displayOpt, targets }: SubMenuItemsProps) => {
|
||||
const { rootPopupState } = useContext(CascadingContext);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(
|
||||
(f: () => any) => () => {
|
||||
f();
|
||||
if (rootPopupState) {
|
||||
rootPopupState.close();
|
||||
}
|
||||
dispatch(
|
||||
closeContextMenu({
|
||||
index: 0,
|
||||
value: undefined,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[dispatch, targets],
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{displayOpt.showVersionControl && (
|
||||
<CascadingMenuItem
|
||||
onClick={onClick(() =>
|
||||
dispatch(
|
||||
setVersionControlDialog({
|
||||
open: true,
|
||||
file: targets[0],
|
||||
}),
|
||||
),
|
||||
)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<HistoryOutlined fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("application:fileManager.manageVersions")}</ListItemText>
|
||||
</CascadingMenuItem>
|
||||
)}
|
||||
{displayOpt.showManageShares && (
|
||||
<CascadingMenuItem
|
||||
onClick={onClick(() =>
|
||||
dispatch(
|
||||
setManageShareDialog({
|
||||
open: true,
|
||||
file: targets[0],
|
||||
}),
|
||||
),
|
||||
)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<BranchForkLink fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("application:fileManager.manageShares")}</ListItemText>
|
||||
</CascadingMenuItem>
|
||||
)}
|
||||
{displayOpt.showDirectLinkManagement && (
|
||||
<CascadingMenuItem
|
||||
onClick={onClick(() =>
|
||||
dispatch(
|
||||
setDirectLinkManagementDialog({
|
||||
open: true,
|
||||
file: targets[0],
|
||||
}),
|
||||
),
|
||||
)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<LinkSetting fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("application:fileManager.manageDirectLinks")}</ListItemText>
|
||||
</CascadingMenuItem>
|
||||
)}
|
||||
{displayOpt.showCreateArchive && (
|
||||
<CascadingMenuItem
|
||||
onClick={onClick(() =>
|
||||
dispatch(
|
||||
setCreateArchiveDialog({
|
||||
open: true,
|
||||
files: targets,
|
||||
}),
|
||||
),
|
||||
)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Archive fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("application:fileManager.createArchive")}</ListItemText>
|
||||
</CascadingMenuItem>
|
||||
)}
|
||||
{displayOpt.showResetThumb && (
|
||||
<CascadingMenuItem onClick={onClick(() => dispatch(resetThumbnails(targets)))}>
|
||||
<ListItemIcon>
|
||||
<ImageArrowCounterclockwise fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("application:fileManager.resetThumbnail")}</ListItemText>
|
||||
</CascadingMenuItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MoreMenuItems;
|
||||
99
src/component/FileManager/ContextMenu/NewFileTemplateMenuItems.tsx
Executable file
99
src/component/FileManager/ContextMenu/NewFileTemplateMenuItems.tsx
Executable file
@@ -0,0 +1,99 @@
|
||||
import React, { useCallback, useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import { SubMenuItemsProps } from "./OrganizeMenuItems.tsx";
|
||||
import { ViewersByID } from "../../../redux/siteConfigSlice.ts";
|
||||
import { ListItemIcon, ListItemText } from "@mui/material";
|
||||
import { ViewerIcon } from "../Dialogs/OpenWith.tsx";
|
||||
import { SquareMenuItem } from "./ContextMenu.tsx";
|
||||
import { CascadingContext, CascadingMenuItem, CascadingSubmenu } from "./CascadingMenu.tsx";
|
||||
import { NewFileTemplate, Viewer } from "../../../api/explorer.ts";
|
||||
import { createNew } from "../../../redux/thunks/file.ts";
|
||||
import { CreateNewDialogType } from "../../../redux/globalStateSlice.ts";
|
||||
|
||||
interface MultiTemplatesMenuItemsProps {
|
||||
viewer: Viewer;
|
||||
}
|
||||
|
||||
const MultiTemplatesMenuItems = ({ viewer }: MultiTemplatesMenuItemsProps) => {
|
||||
const { rootPopupState } = useContext(CascadingContext);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(
|
||||
(f: NewFileTemplate) => () => {
|
||||
if (rootPopupState) {
|
||||
rootPopupState.close();
|
||||
}
|
||||
|
||||
dispatch(createNew(0, CreateNewDialogType.file, viewer, f));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{viewer.templates?.map((template) => (
|
||||
<CascadingMenuItem key={template.ext} onClick={onClick(template)}>
|
||||
<ListItemText>
|
||||
{t("fileManager.newDocumentType", {
|
||||
display_name: t(template.display_name),
|
||||
ext: template.ext,
|
||||
})}
|
||||
</ListItemText>
|
||||
</CascadingMenuItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const NewFileTemplateMenuItems = (props: SubMenuItemsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onClick = useCallback(
|
||||
(viewer: Viewer, template: NewFileTemplate) => () => {
|
||||
dispatch(createNew(0, CreateNewDialogType.file, viewer, template));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.values(ViewersByID)
|
||||
.filter((viewer) => viewer.templates)
|
||||
.map((viewer) => {
|
||||
if (!viewer.templates) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (viewer.templates.length == 1) {
|
||||
return (
|
||||
<SquareMenuItem key={viewer.id} onClick={onClick(viewer, viewer.templates[0])}>
|
||||
<ListItemIcon>
|
||||
<ViewerIcon size={20} viewer={viewer} py={0} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{t("fileManager.newDocumentType", {
|
||||
display_name: t(viewer.templates[0].display_name),
|
||||
ext: viewer.templates[0].ext,
|
||||
})}
|
||||
</ListItemText>
|
||||
</SquareMenuItem>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<CascadingSubmenu
|
||||
popupId={viewer.id}
|
||||
title={t(viewer.display_name)}
|
||||
icon={<ViewerIcon size={20} viewer={viewer} py={0} />}
|
||||
>
|
||||
<MultiTemplatesMenuItems viewer={viewer} />
|
||||
</CascadingSubmenu>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewFileTemplateMenuItems;
|
||||
65
src/component/FileManager/ContextMenu/OpenWithMenuItems.tsx
Executable file
65
src/component/FileManager/ContextMenu/OpenWithMenuItems.tsx
Executable file
@@ -0,0 +1,65 @@
|
||||
import React, { useCallback, useContext, useMemo } from "react";
|
||||
import { CascadingContext, CascadingMenuItem } from "./CascadingMenu.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import { closeContextMenu } from "../../../redux/fileManagerSlice.ts";
|
||||
import { ListItemIcon, ListItemText } from "@mui/material";
|
||||
import { SubMenuItemsProps } from "./OrganizeMenuItems.tsx";
|
||||
import { fileExtension } from "../../../util";
|
||||
import { Viewers } from "../../../redux/siteConfigSlice.ts";
|
||||
import { Viewer } from "../../../api/explorer.ts";
|
||||
import { ViewerIcon } from "../Dialogs/OpenWith.tsx";
|
||||
import { openViewer, openViewers } from "../../../redux/thunks/viewer.ts";
|
||||
|
||||
const OpenWithMenuItems = ({ targets }: SubMenuItemsProps) => {
|
||||
const { rootPopupState } = useContext(CascadingContext);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(
|
||||
(v: Viewer) => () => {
|
||||
dispatch(openViewer(targets[0], v, targets[0].size));
|
||||
if (rootPopupState) {
|
||||
rootPopupState.close();
|
||||
}
|
||||
dispatch(
|
||||
closeContextMenu({
|
||||
index: 0,
|
||||
value: undefined,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[dispatch, targets],
|
||||
);
|
||||
|
||||
const openSelector = useCallback(() => {
|
||||
dispatch(openViewers(0, targets[0], targets[0].size, undefined, true));
|
||||
}, [targets]);
|
||||
|
||||
const viewers = useMemo(() => {
|
||||
if (targets.length == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const firstFileSuffix = fileExtension(targets[0].name);
|
||||
return Viewers[firstFileSuffix ?? ""];
|
||||
}, [targets]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{viewers.map((viewer) => (
|
||||
<CascadingMenuItem key={viewer.id} onClick={onClick(viewer)}>
|
||||
<ListItemIcon>
|
||||
<ViewerIcon size={20} viewer={viewer} py={0} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t(viewer.display_name)}</ListItemText>
|
||||
</CascadingMenuItem>
|
||||
))}
|
||||
<CascadingMenuItem onClick={openSelector}>
|
||||
<ListItemIcon></ListItemIcon>
|
||||
<ListItemText>{t("fileManager.selectApplications")}</ListItemText>
|
||||
</CascadingMenuItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenWithMenuItems;
|
||||
103
src/component/FileManager/ContextMenu/OrganizeMenuItems.tsx
Executable file
103
src/component/FileManager/ContextMenu/OrganizeMenuItems.tsx
Executable file
@@ -0,0 +1,103 @@
|
||||
import { DisplayOption } from "./useActionDisplayOpt.ts";
|
||||
import { FileResponse } from "../../../api/explorer.ts";
|
||||
import { useCallback, useContext } from "react";
|
||||
import { CascadingContext, CascadingMenuItem } from "./CascadingMenu.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import { closeContextMenu } from "../../../redux/fileManagerSlice.ts";
|
||||
import { applyIconColor, dialogBasedMoveCopy } from "../../../redux/thunks/file.ts";
|
||||
import { ListItemIcon, ListItemText } from "@mui/material";
|
||||
import FolderArrowRightOutlined from "../../Icons/FolderArrowRightOutlined.tsx";
|
||||
import { setChangeIconDialog, setPinFileDialog } from "../../../redux/globalStateSlice.ts";
|
||||
import { getFileLinkedUri } from "../../../util";
|
||||
import PinOutlined from "../../Icons/PinOutlined.tsx";
|
||||
import EmojiEdit from "../../Icons/EmojiEdit.tsx";
|
||||
import FolderColorQuickAction from "../FileInfo/FolderColorQuickAction.tsx";
|
||||
import { DenseDivider } from "./ContextMenu.tsx";
|
||||
|
||||
export interface SubMenuItemsProps {
|
||||
displayOpt: DisplayOption;
|
||||
targets: FileResponse[];
|
||||
}
|
||||
const OrganizeMenuItems = ({ displayOpt, targets }: SubMenuItemsProps) => {
|
||||
const { rootPopupState } = useContext(CascadingContext);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(
|
||||
(f: () => any) => () => {
|
||||
f();
|
||||
if (rootPopupState) {
|
||||
rootPopupState.close();
|
||||
}
|
||||
dispatch(
|
||||
closeContextMenu({
|
||||
index: 0,
|
||||
value: undefined,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[dispatch, targets],
|
||||
);
|
||||
const showDivider =
|
||||
(displayOpt.showMove || displayOpt.showPin || displayOpt.showChangeIcon) && displayOpt.showChangeFolderColor;
|
||||
return (
|
||||
<>
|
||||
{displayOpt.showMove && (
|
||||
<CascadingMenuItem onClick={onClick(() => dispatch(dialogBasedMoveCopy(0, targets, false)))}>
|
||||
<ListItemIcon>
|
||||
<FolderArrowRightOutlined fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("application:fileManager.move")}</ListItemText>
|
||||
</CascadingMenuItem>
|
||||
)}
|
||||
{displayOpt.showPin && (
|
||||
<CascadingMenuItem
|
||||
onClick={onClick(() =>
|
||||
dispatch(
|
||||
setPinFileDialog({
|
||||
open: true,
|
||||
uri: getFileLinkedUri(targets[0]),
|
||||
}),
|
||||
),
|
||||
)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<PinOutlined fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("application:fileManager.pin")}</ListItemText>
|
||||
</CascadingMenuItem>
|
||||
)}
|
||||
{displayOpt.showChangeIcon && (
|
||||
<CascadingMenuItem
|
||||
onClick={onClick(() =>
|
||||
dispatch(
|
||||
setChangeIconDialog({
|
||||
open: true,
|
||||
file: targets,
|
||||
}),
|
||||
),
|
||||
)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<EmojiEdit fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("application:fileManager.customizeIcon")}</ListItemText>
|
||||
</CascadingMenuItem>
|
||||
)}
|
||||
{showDivider && <DenseDivider />}
|
||||
{displayOpt.showChangeFolderColor && (
|
||||
<FolderColorQuickAction
|
||||
file={targets[0]}
|
||||
onColorChange={(color) => onClick(() => dispatch(applyIconColor(0, targets, color, true)))()}
|
||||
sx={{
|
||||
maxWidth: "204px",
|
||||
margin: (theme) => `0 ${theme.spacing(0.5)}`,
|
||||
padding: (theme) => `${theme.spacing(0.5)} ${theme.spacing(1)}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrganizeMenuItems;
|
||||
179
src/component/FileManager/ContextMenu/TagMenuItems.tsx
Executable file
179
src/component/FileManager/ContextMenu/TagMenuItems.tsx
Executable file
@@ -0,0 +1,179 @@
|
||||
import { Box, IconButton, ListItemIcon, ListItemText } from "@mui/material";
|
||||
import React, { useCallback, useContext, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FileResponse, Metadata } from "../../../api/explorer.ts";
|
||||
import { closeContextMenu } from "../../../redux/fileManagerSlice.ts";
|
||||
import { setTagsDialog } from "../../../redux/globalStateSlice.ts";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import { patchFileMetadata } from "../../../redux/thunks/file.ts";
|
||||
import SessionManager, { UserSettings } from "../../../session";
|
||||
import { UsedTags } from "../../../session/utils.ts";
|
||||
import Checkmark from "../../Icons/Checkmark.tsx";
|
||||
import DeleteOutlined from "../../Icons/DeleteOutlined.tsx";
|
||||
import Tags from "../../Icons/Tags.tsx";
|
||||
import { getUniqueTagsFromFiles, Tag as TagItem } from "../Dialogs/Tags.tsx";
|
||||
import FileTag from "../Explorer/FileTag.tsx";
|
||||
import { FileManagerIndex } from "../FileManager.tsx";
|
||||
import { CascadingContext, CascadingMenuItem } from "./CascadingMenu.tsx";
|
||||
import { DenseDivider, SquareMenuItem } from "./ContextMenu.tsx";
|
||||
import { SubMenuItemsProps } from "./OrganizeMenuItems.tsx";
|
||||
|
||||
interface TagOption extends TagItem {
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const getTagOptions = (targets: FileResponse[]): TagOption[] => {
|
||||
const tags: {
|
||||
[key: string]: TagOption;
|
||||
} = {};
|
||||
getUniqueTagsFromFiles(targets).forEach((tag) => {
|
||||
tags[tag.key] = { ...tag, selected: true };
|
||||
});
|
||||
|
||||
const existing = SessionManager.get(UserSettings.UsedTags) as UsedTags;
|
||||
if (existing) {
|
||||
Object.keys(existing).forEach((key) => {
|
||||
if (!tags[key]) {
|
||||
tags[key] = { key, color: existing[key] ?? undefined, selected: false };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Object.values(tags);
|
||||
};
|
||||
|
||||
const TagMenuItems = ({ displayOpt, targets }: SubMenuItemsProps) => {
|
||||
const { rootPopupState } = useContext(CascadingContext);
|
||||
const [tags, setTags] = useState<TagOption[]>(getTagOptions(targets));
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(
|
||||
(f: () => any) => () => {
|
||||
f();
|
||||
if (rootPopupState) {
|
||||
rootPopupState.close();
|
||||
}
|
||||
dispatch(
|
||||
closeContextMenu({
|
||||
index: 0,
|
||||
value: undefined,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[dispatch, targets],
|
||||
);
|
||||
|
||||
const onTagChange = useCallback(
|
||||
async (tag: TagOption, selected: boolean) => {
|
||||
setTags((tags) =>
|
||||
tags.map((t) => {
|
||||
if (t.key == tag.key) {
|
||||
return { ...t, selected };
|
||||
}
|
||||
return t;
|
||||
}),
|
||||
);
|
||||
try {
|
||||
await dispatch(
|
||||
patchFileMetadata(FileManagerIndex.main, targets, [
|
||||
{
|
||||
key: Metadata.tag_prefix + tag.key,
|
||||
value: tag.color,
|
||||
remove: !selected,
|
||||
},
|
||||
]),
|
||||
);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
[targets, setTags],
|
||||
);
|
||||
|
||||
const onTagDelete = useCallback(
|
||||
(tag: TagOption, event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
|
||||
// Remove tag from session cache
|
||||
const existing = SessionManager.get(UserSettings.UsedTags) as UsedTags;
|
||||
if (existing && existing[tag.key] !== undefined) {
|
||||
delete existing[tag.key];
|
||||
SessionManager.set(UserSettings.UsedTags, existing);
|
||||
}
|
||||
|
||||
// Remove tag from local state
|
||||
setTags((tags) => tags.filter((t) => t.key !== tag.key));
|
||||
},
|
||||
[setTags],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CascadingMenuItem
|
||||
onClick={onClick(() =>
|
||||
dispatch(
|
||||
setTagsDialog({
|
||||
open: true,
|
||||
file: targets,
|
||||
}),
|
||||
),
|
||||
)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Tags fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("application:modals.manageTags")}</ListItemText>
|
||||
</CascadingMenuItem>
|
||||
{tags.length > 0 && <DenseDivider />}
|
||||
{tags.map((tag) => (
|
||||
<SquareMenuItem key={tag.key} onClick={() => onTagChange(tag, !tag.selected)}>
|
||||
{tag.selected && (
|
||||
<>
|
||||
<ListItemIcon>
|
||||
<Checkmark />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<FileTag disableClick spacing={1} label={tag.key} tagColor={tag.color} />
|
||||
</ListItemText>
|
||||
</>
|
||||
)}
|
||||
{!tag.selected && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
"&:hover .delete-button": {
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemText inset>
|
||||
<FileTag disableClick spacing={1} label={tag.key} tagColor={tag.color} />
|
||||
</ListItemText>
|
||||
<IconButton
|
||||
className="delete-button"
|
||||
size="small"
|
||||
onClick={(event) => onTagDelete(tag, event)}
|
||||
sx={{
|
||||
opacity: 0,
|
||||
transition: "opacity 0.2s",
|
||||
marginLeft: "auto",
|
||||
marginRight: 1,
|
||||
padding: "2px",
|
||||
"&:hover": {
|
||||
backgroundColor: "action.hover",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DeleteOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
</SquareMenuItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagMenuItems;
|
||||
325
src/component/FileManager/ContextMenu/useActionDisplayOpt.ts
Executable file
325
src/component/FileManager/ContextMenu/useActionDisplayOpt.ts
Executable file
@@ -0,0 +1,325 @@
|
||||
import { useMemo } from "react";
|
||||
import { FileResponse, FileType, Metadata, NavigatorCapability } from "../../../api/explorer.ts";
|
||||
import { GroupPermission } from "../../../api/user.ts";
|
||||
import { defaultPath } from "../../../hooks/useNavigation.tsx";
|
||||
import { ContextMenuTypes } from "../../../redux/fileManagerSlice.ts";
|
||||
import { Viewers, ViewersByID } from "../../../redux/siteConfigSlice.ts";
|
||||
import { ExpandedViewerSetting } from "../../../redux/thunks/viewer.ts";
|
||||
import SessionManager from "../../../session";
|
||||
import { fileExtension } from "../../../util";
|
||||
import Boolset from "../../../util/boolset.ts";
|
||||
import CrUri, { Filesystem } from "../../../util/uri.ts";
|
||||
import { FileManagerIndex } from "../FileManager.tsx";
|
||||
|
||||
const supportedArchiveTypes = ["zip", "gz", "xz", "tar", "rar", "7z", "bz2"];
|
||||
|
||||
export const canManageVersion = (file: FileResponse, bs: Boolset) => {
|
||||
return (
|
||||
file.type == FileType.file &&
|
||||
(!file.metadata || !file.metadata[Metadata.share_redirect]) &&
|
||||
bs.enabled(NavigatorCapability.version_control)
|
||||
);
|
||||
};
|
||||
|
||||
export const canShowInfo = (cap: Boolset) => {
|
||||
return cap.enabled(NavigatorCapability.info);
|
||||
};
|
||||
|
||||
export const canUpdate = (opt: DisplayOption) => {
|
||||
return !!(
|
||||
opt.allUpdatable &&
|
||||
opt.hasFile &&
|
||||
opt.orCapability?.enabled(NavigatorCapability.upload_file) &&
|
||||
opt.allUpdatable
|
||||
);
|
||||
};
|
||||
|
||||
export interface DisplayOption {
|
||||
allReadable: boolean;
|
||||
allUpdatable: boolean;
|
||||
|
||||
hasReadable?: boolean;
|
||||
hasUpdatable?: boolean;
|
||||
|
||||
hasTrashFile?: boolean;
|
||||
hasFile?: boolean;
|
||||
hasFolder?: boolean;
|
||||
hasOwned?: boolean;
|
||||
hasFailedThumb?: boolean;
|
||||
|
||||
showEnter?: boolean;
|
||||
showOpen?: boolean;
|
||||
showOpenWithCascading?: () => boolean;
|
||||
showOpenWith?: () => boolean;
|
||||
showDownload?: boolean;
|
||||
showGoToSharedLink?: boolean;
|
||||
showExtractArchive?: boolean;
|
||||
showTorrentRemoteDownload?: boolean;
|
||||
showGoToParent?: boolean;
|
||||
|
||||
showDelete?: boolean;
|
||||
showRestore?: boolean;
|
||||
showRename?: boolean;
|
||||
showPin?: boolean;
|
||||
showOrganize?: boolean;
|
||||
showCopy?: boolean;
|
||||
showShare?: boolean;
|
||||
showInfo?: boolean;
|
||||
showDirectLink?: boolean;
|
||||
|
||||
showMove?: boolean;
|
||||
showTags?: boolean;
|
||||
showChangeFolderColor?: boolean;
|
||||
showChangeIcon?: boolean;
|
||||
showCustomProps?: boolean;
|
||||
|
||||
showMore?: boolean;
|
||||
showVersionControl?: boolean;
|
||||
showDirectLinkManagement?: boolean;
|
||||
showManageShares?: boolean;
|
||||
showCreateArchive?: boolean;
|
||||
showResetThumb?: boolean;
|
||||
|
||||
andCapability?: Boolset;
|
||||
orCapability?: Boolset;
|
||||
|
||||
showCreateFolder?: boolean;
|
||||
showCreateFile?: boolean;
|
||||
showRefresh?: boolean;
|
||||
showNewFileFromTemplate?: boolean;
|
||||
showUpload?: boolean;
|
||||
showRemoteDownload?: boolean;
|
||||
}
|
||||
|
||||
const capabilityMap: { [key: string]: Boolset } = {};
|
||||
|
||||
export const getActionOpt = (
|
||||
targets: FileResponse[],
|
||||
viewerSetting?: ExpandedViewerSetting,
|
||||
type?: string,
|
||||
parent?: FileResponse,
|
||||
fmIndex: number = 0,
|
||||
): DisplayOption => {
|
||||
const currentUser = SessionManager.currentLoginOrNull();
|
||||
const currentUserAnonymous = SessionManager.currentUser();
|
||||
const groupBs = SessionManager.currentUserGroupPermission();
|
||||
const display: DisplayOption = {
|
||||
allReadable: true,
|
||||
allUpdatable: true,
|
||||
};
|
||||
if (type == ContextMenuTypes.empty || type == ContextMenuTypes.new) {
|
||||
display.showRefresh = type == ContextMenuTypes.empty;
|
||||
display.showRemoteDownload = groupBs.enabled(GroupPermission.remote_download) && !!currentUser;
|
||||
|
||||
if (!parent || parent.type != FileType.folder) {
|
||||
display.showRemoteDownload = display.showRemoteDownload && type == ContextMenuTypes.new;
|
||||
return display;
|
||||
}
|
||||
|
||||
const parentCap = new Boolset(parent.capability);
|
||||
display.showCreateFolder = parentCap.enabled(NavigatorCapability.create_file) && parent.owned;
|
||||
display.showCreateFile = display.showCreateFolder && fmIndex == FileManagerIndex.main;
|
||||
display.showUpload = display.showCreateFile;
|
||||
if (display.showCreateFile) {
|
||||
const allViewers = Object.entries(ViewersByID);
|
||||
for (let i = 0; i < allViewers.length; i++) {
|
||||
if (allViewers[i][1] && allViewers[i][1].templates) {
|
||||
display.showNewFileFromTemplate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return display;
|
||||
}
|
||||
|
||||
if (type == ContextMenuTypes.searchResult) {
|
||||
display.showGoToParent = true;
|
||||
}
|
||||
|
||||
const parentUrl = new CrUri(targets?.[0]?.path ?? defaultPath);
|
||||
targets.forEach((target) => {
|
||||
let readable = true;
|
||||
let updatable = target.owned && parentUrl.fs() != Filesystem.share;
|
||||
|
||||
if (display.allReadable && !readable) {
|
||||
display.allReadable = false;
|
||||
}
|
||||
if (display.allUpdatable && !updatable) {
|
||||
display.allUpdatable = false;
|
||||
}
|
||||
|
||||
if (!display.hasReadable && readable) {
|
||||
display.hasReadable = true;
|
||||
}
|
||||
if (!display.hasUpdatable && updatable) {
|
||||
display.hasUpdatable = true;
|
||||
}
|
||||
|
||||
if (target.metadata) {
|
||||
if (target.metadata[Metadata.restore_uri]) {
|
||||
display.hasTrashFile = true;
|
||||
}
|
||||
if (target.metadata[Metadata.thumbDisabled] !== undefined) {
|
||||
display.hasFailedThumb = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (target.type == FileType.file) {
|
||||
display.hasFile = true;
|
||||
}
|
||||
|
||||
if (target.type == FileType.folder) {
|
||||
display.hasFolder = true;
|
||||
}
|
||||
|
||||
if (target.owned) {
|
||||
display.hasOwned = true;
|
||||
}
|
||||
|
||||
if (target.capability) {
|
||||
let bs = capabilityMap[target.capability];
|
||||
if (!bs) {
|
||||
bs = new Boolset(target.capability);
|
||||
capabilityMap[target.capability] = bs;
|
||||
}
|
||||
|
||||
if (!display.andCapability) {
|
||||
display.andCapability = bs;
|
||||
}
|
||||
|
||||
display.andCapability = display.andCapability.and(bs);
|
||||
|
||||
if (!display.orCapability) {
|
||||
display.orCapability = bs;
|
||||
}
|
||||
display.orCapability = display.orCapability.or(bs);
|
||||
}
|
||||
});
|
||||
|
||||
const firstFileSuffix = fileExtension(targets[0]?.name ?? "");
|
||||
display.showPin = !display.hasTrashFile && targets.length == 1 && display.hasFolder;
|
||||
display.showDelete =
|
||||
display.hasUpdatable &&
|
||||
display.orCapability &&
|
||||
(display.orCapability.enabled(NavigatorCapability.soft_delete) ||
|
||||
display.orCapability.enabled(NavigatorCapability.delete_file));
|
||||
display.showRestore = display.andCapability?.enabled(NavigatorCapability.restore);
|
||||
display.showRename =
|
||||
targets.length == 1 &&
|
||||
display.allUpdatable &&
|
||||
display.orCapability &&
|
||||
display.orCapability.enabled(NavigatorCapability.rename_file);
|
||||
display.showCopy = display.hasUpdatable && !!display.orCapability;
|
||||
display.showShare =
|
||||
targets.length == 1 &&
|
||||
!!currentUser &&
|
||||
groupBs.enabled(GroupPermission.share) &&
|
||||
display.allUpdatable &&
|
||||
(targets[0].owned || groupBs.enabled(GroupPermission.is_admin)) &&
|
||||
display.orCapability &&
|
||||
display.orCapability.enabled(NavigatorCapability.share) &&
|
||||
(!targets[0].metadata ||
|
||||
(!targets[0].metadata[Metadata.share_redirect] && !targets[0].metadata[Metadata.restore_uri]));
|
||||
display.showMove = display.hasUpdatable && !!display.orCapability;
|
||||
display.showTags =
|
||||
display.hasUpdatable && display.orCapability && display.orCapability.enabled(NavigatorCapability.update_metadata);
|
||||
display.showChangeFolderColor =
|
||||
display.hasUpdatable &&
|
||||
!display.hasFile &&
|
||||
display.orCapability &&
|
||||
display.orCapability.enabled(NavigatorCapability.update_metadata);
|
||||
display.showChangeIcon =
|
||||
display.hasUpdatable && display.orCapability && display.orCapability.enabled(NavigatorCapability.update_metadata);
|
||||
display.showCustomProps = display.showChangeIcon;
|
||||
display.showDownload =
|
||||
display.hasReadable && display.orCapability && display.orCapability.enabled(NavigatorCapability.download_file);
|
||||
display.showDirectLink =
|
||||
(display.hasOwned || groupBs.enabled(GroupPermission.is_admin)) &&
|
||||
display.orCapability &&
|
||||
(currentUserAnonymous?.group?.direct_link_batch_size ?? 0) >= targets.length &&
|
||||
display.orCapability.enabled(NavigatorCapability.download_file);
|
||||
display.showDirectLinkManagement = display.showDirectLink && targets.length == 1 && display.hasFile;
|
||||
display.showOpen =
|
||||
targets.length == 1 &&
|
||||
display.hasFile &&
|
||||
display.showDownload &&
|
||||
!!viewerSetting &&
|
||||
!!firstFileSuffix &&
|
||||
!!viewerSetting?.[firstFileSuffix];
|
||||
display.showEnter =
|
||||
targets.length == 1 &&
|
||||
display.hasFolder &&
|
||||
display.orCapability?.enabled(NavigatorCapability.enter_folder) &&
|
||||
display.allReadable;
|
||||
display.showExtractArchive =
|
||||
targets.length == 1 &&
|
||||
display.hasFile &&
|
||||
display.showDownload &&
|
||||
!!currentUser &&
|
||||
groupBs.enabled(GroupPermission.archive_task) &&
|
||||
supportedArchiveTypes.includes(firstFileSuffix ?? "");
|
||||
display.showTorrentRemoteDownload =
|
||||
targets.length == 1 &&
|
||||
display.hasFile &&
|
||||
display.showDownload &&
|
||||
!!currentUser &&
|
||||
groupBs.enabled(GroupPermission.remote_download) &&
|
||||
firstFileSuffix == "torrent";
|
||||
|
||||
display.showOpenWithCascading = () => false;
|
||||
display.showOpenWith = () => targets.length == 1 && !!display.hasFile && !!display.showDownload;
|
||||
if (display.showOpen) {
|
||||
display.showOpenWithCascading = () =>
|
||||
!!(display.showOpen && viewerSetting && viewerSetting[firstFileSuffix ?? ""]?.length >= 1);
|
||||
display.showOpenWith = () =>
|
||||
!!(display.showOpen && viewerSetting && viewerSetting[firstFileSuffix ?? ""]?.length < 1);
|
||||
}
|
||||
display.showOrganize = display.showPin || display.showMove || display.showChangeFolderColor || display.showChangeIcon;
|
||||
display.showGoToSharedLink =
|
||||
targets.length == 1 && display.hasFile && targets[0].metadata && !!targets[0].metadata[Metadata.share_redirect];
|
||||
display.showInfo = targets.length == 1 && display.orCapability && canShowInfo(display.orCapability);
|
||||
display.showVersionControl =
|
||||
targets.length == 1 &&
|
||||
display.orCapability &&
|
||||
display.hasReadable &&
|
||||
canManageVersion(targets[0], display.orCapability);
|
||||
display.showManageShares =
|
||||
targets.length == 1 &&
|
||||
targets[0].shared &&
|
||||
display.orCapability &&
|
||||
!!currentUser &&
|
||||
groupBs.enabled(GroupPermission.share) &&
|
||||
display.orCapability.enabled(NavigatorCapability.share);
|
||||
display.showCreateArchive =
|
||||
display.hasReadable &&
|
||||
!!currentUser &&
|
||||
groupBs.enabled(GroupPermission.archive_task) &&
|
||||
display.orCapability &&
|
||||
display.orCapability.enabled(NavigatorCapability.download_file);
|
||||
display.showResetThumb =
|
||||
display.hasFile &&
|
||||
!display.hasFolder &&
|
||||
display.hasFailedThumb &&
|
||||
display.allUpdatable &&
|
||||
display.orCapability &&
|
||||
display.orCapability.enabled(NavigatorCapability.update_metadata);
|
||||
|
||||
display.showMore =
|
||||
display.showVersionControl ||
|
||||
display.showManageShares ||
|
||||
display.showCreateArchive ||
|
||||
display.showDirectLinkManagement ||
|
||||
display.showResetThumb;
|
||||
return display;
|
||||
};
|
||||
|
||||
const useActionDisplayOpt = (targets: FileResponse[], type?: string, parent?: FileResponse, fmIndex: number = 0) => {
|
||||
const opt = useMemo(() => {
|
||||
return getActionOpt(targets, Viewers, type, parent, fmIndex);
|
||||
}, [targets, type, parent, fmIndex]);
|
||||
|
||||
return opt;
|
||||
};
|
||||
|
||||
export default useActionDisplayOpt;
|
||||
Reference in New Issue
Block a user