first commit
This commit is contained in:
4
src/component/Frame/FrameManagerBundle.tsx
Executable file
4
src/component/Frame/FrameManagerBundle.tsx
Executable file
@@ -0,0 +1,4 @@
|
||||
import { FileManager } from "../FileManager/FileManager.tsx";
|
||||
import NavBarFrame, { AutoNavbarFrame } from "./NavBarFrame.tsx";
|
||||
|
||||
export { AutoNavbarFrame, FileManager, NavBarFrame };
|
||||
106
src/component/Frame/HeadlessFrame.tsx
Executable file
106
src/component/Frame/HeadlessFrame.tsx
Executable file
@@ -0,0 +1,106 @@
|
||||
import { Box, Container, Grid, Paper } from "@mui/material";
|
||||
import { Outlet, useNavigation } from "react-router-dom";
|
||||
import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts";
|
||||
import AutoHeight from "../Common/AutoHeight.tsx";
|
||||
import CircularProgress from "../Common/CircularProgress.tsx";
|
||||
import Logo from "../Common/Logo.tsx";
|
||||
import LanguageSwitcher from "../Common/LanguageSwitcher.tsx";
|
||||
import PoweredBy from "./PoweredBy.tsx";
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
pt: 7,
|
||||
pb: 9,
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const HeadlessFrame = () => {
|
||||
const loading = useAppSelector((state) => state.globalState.loading.headlessFrame);
|
||||
const { headless_footer, headless_bottom, sidebar_bottom } = useAppSelector(
|
||||
(state) => state.siteConfig.basic?.config?.custom_html ?? {},
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
let navigation = useNavigation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900],
|
||||
flexGrow: 1,
|
||||
height: "100vh",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<Container maxWidth={"xs"}>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
sx={{ minHeight: "100vh" }}
|
||||
>
|
||||
<Box sx={{ width: "100%", py: 2 }}>
|
||||
<Paper
|
||||
sx={{
|
||||
padding: (theme) => `${theme.spacing(2)} ${theme.spacing(3)} ${theme.spacing(3)}`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Logo
|
||||
sx={{
|
||||
maxWidth: "40%",
|
||||
maxHeight: "40px",
|
||||
}}
|
||||
/>
|
||||
{/* 语言切换按钮 */}
|
||||
<LanguageSwitcher />
|
||||
</Box>
|
||||
<AutoHeight>
|
||||
<div>
|
||||
<Box
|
||||
sx={{
|
||||
display: loading || navigation.state !== "idle" ? "none" : "block",
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
{headless_bottom && (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<div dangerouslySetInnerHTML={{ __html: headless_bottom }} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{(loading || navigation.state !== "idle") && <Loading />}
|
||||
</div>
|
||||
</AutoHeight>
|
||||
</Paper>
|
||||
</Box>
|
||||
<PoweredBy />
|
||||
{headless_footer && (
|
||||
<Box sx={{ width: "100%", mb: 2 }}>
|
||||
<div dangerouslySetInnerHTML={{ __html: headless_footer }} />
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeadlessFrame;
|
||||
93
src/component/Frame/NavBar/AppDrawer.tsx
Executable file
93
src/component/Frame/NavBar/AppDrawer.tsx
Executable file
@@ -0,0 +1,93 @@
|
||||
import { Box, Drawer, Popover, PopoverProps, Stack, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { useContext, useRef } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import SessionManager from "../../../session";
|
||||
import TreeNavigation from "../../FileManager/TreeView/TreeNavigation.tsx";
|
||||
import { PageVariant, PageVariantContext } from "../NavBarFrame.tsx";
|
||||
import DrawerHeader from "./DrawerHeader.tsx";
|
||||
import PageNavigation, { AdminPageNavigation } from "./PageNavigation.tsx";
|
||||
import StorageSummary from "./StorageSummary.tsx";
|
||||
|
||||
const DrawerContent = () => {
|
||||
const { sidebar_bottom } = useAppSelector((state) => state.siteConfig.basic?.config?.custom_html ?? {});
|
||||
const scrollRef = useRef<any>();
|
||||
const user = SessionManager.currentLoginOrNull();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const pageVariant = useContext(PageVariantContext);
|
||||
const isDashboard = pageVariant === PageVariant.dashboard;
|
||||
return (
|
||||
<>
|
||||
<DrawerHeader />
|
||||
<Stack
|
||||
direction={"column"}
|
||||
spacing={2}
|
||||
ref={scrollRef}
|
||||
sx={{
|
||||
px: 1,
|
||||
pb: 1,
|
||||
flexGrow: 1,
|
||||
mx: 1,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{!isDashboard && (
|
||||
<>
|
||||
<TreeNavigation scrollRef={scrollRef} hideWithDrawer={!isMobile} />
|
||||
<PageNavigation />
|
||||
{user && <StorageSummary />}
|
||||
</>
|
||||
)}
|
||||
{isDashboard && <AdminPageNavigation />}
|
||||
{sidebar_bottom && (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<div dangerouslySetInnerHTML={{ __html: sidebar_bottom }} />
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const DrawerPopover = (props: PopoverProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const open = useAppSelector((state) => state.globalState.drawerOpen);
|
||||
const drawerWidth = useAppSelector((state) => state.globalState.drawerWidth);
|
||||
return (
|
||||
<Popover {...props}>
|
||||
<Box sx={{ width: "70vw" }}>
|
||||
<DrawerContent />
|
||||
</Box>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const AppDrawer = () => {
|
||||
const theme = useTheme();
|
||||
const open = useAppSelector((state) => state.globalState.drawerOpen);
|
||||
const drawerWidth = useAppSelector((state) => state.globalState.drawerWidth);
|
||||
const appBarBg = theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900];
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
sx={{
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
display: "flex",
|
||||
"& .MuiDrawer-paper": {
|
||||
width: drawerWidth,
|
||||
boxSizing: "border-box",
|
||||
backgroundColor: appBarBg,
|
||||
borderRight: "initial",
|
||||
},
|
||||
}}
|
||||
variant="persistent"
|
||||
anchor="left"
|
||||
open={open}
|
||||
>
|
||||
<DrawerContent />
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppDrawer;
|
||||
97
src/component/Frame/NavBar/AppMain.tsx
Executable file
97
src/component/Frame/NavBar/AppMain.tsx
Executable file
@@ -0,0 +1,97 @@
|
||||
import { Box, styled, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { useContext, useEffect, useMemo, useState } from "react";
|
||||
import { Navigate, Outlet, useNavigation } from "react-router-dom";
|
||||
import { CSSTransition, SwitchTransition } from "react-transition-group";
|
||||
import { GroupPermission } from "../../../api/user.ts";
|
||||
import { useAppSelector } from "../../../redux/hooks.ts";
|
||||
import SessionManager from "../../../session";
|
||||
import { GroupBS } from "../../../session/utils.ts";
|
||||
import FacebookCircularProgress from "../../Common/CircularProgress.tsx";
|
||||
import { PageVariant, PageVariantContext } from "../NavBarFrame.tsx";
|
||||
import { DrawerHeaderContainer } from "./DrawerHeader.tsx";
|
||||
|
||||
const StyledLoadingContainer = styled(Box)(() => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}));
|
||||
|
||||
export const PageLoading = () => {
|
||||
return (
|
||||
<StyledLoadingContainer>
|
||||
<FacebookCircularProgress />
|
||||
</StyledLoadingContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const AppMain = () => {
|
||||
const open = useAppSelector((state) => state.globalState.drawerOpen);
|
||||
const drawerWidth = useAppSelector((state) => state.globalState.drawerWidth);
|
||||
const [innerHeight, setInnerHeight] = useState(window.innerHeight);
|
||||
let navigation = useNavigation();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const pageVariant = useContext(PageVariantContext);
|
||||
const isDashboard = pageVariant == PageVariant.dashboard;
|
||||
const user = SessionManager.currentLoginOrNull();
|
||||
const isAdmin = useMemo(() => {
|
||||
return GroupBS(user?.user).enabled(GroupPermission.is_admin);
|
||||
}, [user?.user?.group?.permission]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setInnerHeight(window.innerHeight);
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
flexGrow: 1,
|
||||
transition: theme.transitions.create("margin", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
marginRight: isMobile ? 0 : 2,
|
||||
marginLeft: isMobile ? 0 : `-${drawerWidth - 16}px`,
|
||||
...(open && {
|
||||
transition: theme.transitions.create("margin", {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
marginLeft: 0,
|
||||
}),
|
||||
height: isMobile ? "100%" : window.innerHeight,
|
||||
minHeight: window.innerHeight,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
})}
|
||||
component={"main"}
|
||||
>
|
||||
<DrawerHeaderContainer />
|
||||
<SwitchTransition>
|
||||
<CSSTransition
|
||||
addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}
|
||||
classNames="fade"
|
||||
key={navigation.state !== "idle" ? "loading" : "idle"}
|
||||
>
|
||||
{navigation.state !== "idle" ? (
|
||||
<PageLoading />
|
||||
) : isDashboard && !isAdmin ? (
|
||||
<Navigate to={"/home"} />
|
||||
) : (
|
||||
<Outlet />
|
||||
)}
|
||||
</CSSTransition>
|
||||
</SwitchTransition>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppMain;
|
||||
102
src/component/Frame/NavBar/DarkThemeSwitcher.tsx
Executable file
102
src/component/Frame/NavBar/DarkThemeSwitcher.tsx
Executable file
@@ -0,0 +1,102 @@
|
||||
import { IconButton, Popover, ToggleButton, ToggleButtonGroup, Tooltip } from "@mui/material";
|
||||
import DarkTheme from "../../Icons/DarkTheme.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import Sunny from "../../Icons/Sunny.tsx";
|
||||
import Moon from "../../Icons/Moon.tsx";
|
||||
import SunWithTime from "../../Icons/SunWithTime.tsx";
|
||||
import { setDarkMode } from "../../../redux/globalStateSlice.ts";
|
||||
import SessionManager, { UserSettings } from "../../../session";
|
||||
|
||||
interface SwitchPopoverProps {
|
||||
open?: boolean;
|
||||
anchorEl?: HTMLElement | null;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const SwitchPopover = ({ open, anchorEl, onClose }: SwitchPopoverProps) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const darkMode = useAppSelector((state) => state.globalState.darkMode);
|
||||
const currentMode = useMemo(() => {
|
||||
if (darkMode === undefined) {
|
||||
return "system";
|
||||
}
|
||||
return darkMode ? "dark" : "light";
|
||||
}, [darkMode]);
|
||||
const handleChange = (_event: React.MouseEvent<HTMLElement>, newMode: string) => {
|
||||
let newSetting: boolean | undefined;
|
||||
if (newMode === "light") {
|
||||
newSetting = false;
|
||||
} else if (newMode === "dark") {
|
||||
newSetting = true;
|
||||
}
|
||||
dispatch(setDarkMode(newSetting));
|
||||
SessionManager.set(UserSettings.PreferredDarkMode, newSetting);
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
const inner = (
|
||||
<ToggleButtonGroup
|
||||
color="primary"
|
||||
value={currentMode}
|
||||
exclusive
|
||||
onChange={handleChange}
|
||||
size={onClose ? undefined : "small"}
|
||||
aria-label="Platform"
|
||||
>
|
||||
<ToggleButton value="light">
|
||||
<Sunny fontSize="small" sx={{ mr: 1 }} />
|
||||
{t("navbar.toLightMode")}
|
||||
</ToggleButton>
|
||||
<ToggleButton value="system">
|
||||
<SunWithTime fontSize="small" sx={{ mr: 1 }} />
|
||||
{t("setting.syncWithSystem")}
|
||||
</ToggleButton>
|
||||
<ToggleButton value="dark">
|
||||
<Moon fontSize="small" sx={{ mr: 1 }} />
|
||||
{t("navbar.toDarkMode")}
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
);
|
||||
|
||||
return onClose ? (
|
||||
<Popover
|
||||
open={!!open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={onClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
{inner}
|
||||
</Popover>
|
||||
) : (
|
||||
inner
|
||||
);
|
||||
};
|
||||
|
||||
const DarkThemeSwitcher = () => {
|
||||
const { t } = useTranslation();
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={t("navbar.darkModeSwitch")}>
|
||||
<IconButton onClick={handleClick} size="large">
|
||||
<DarkTheme />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<SwitchPopover open={Boolean(anchorEl)} anchorEl={anchorEl} onClose={() => setAnchorEl(null)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DarkThemeSwitcher;
|
||||
54
src/component/Frame/NavBar/DrawerHeader.tsx
Executable file
54
src/component/Frame/NavBar/DrawerHeader.tsx
Executable file
@@ -0,0 +1,54 @@
|
||||
import { ChevronLeft } from "@mui/icons-material";
|
||||
import { Box, Fade, IconButton, styled, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { setDrawerOpen } from "../../../redux/globalStateSlice.ts";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import Logo from "../../Common/Logo.tsx";
|
||||
|
||||
export const DrawerHeaderContainer = styled("div")(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: theme.spacing(0, 1),
|
||||
// necessary for content to be below app bar
|
||||
...theme.mixins.toolbar,
|
||||
justifyContent: "flex-end",
|
||||
}));
|
||||
|
||||
const DrawerHeader = ({ disabled }: { disabled?: boolean }) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [showCollapse, setShowCollapse] = useState(false);
|
||||
|
||||
return (
|
||||
<DrawerHeaderContainer
|
||||
onMouseEnter={() => setShowCollapse(disabled ? false : true)}
|
||||
onMouseLeave={() => setShowCollapse(false)}
|
||||
>
|
||||
<Box sx={{ width: "100%", pl: 2 }}>
|
||||
<Logo
|
||||
sx={{
|
||||
height: "auto",
|
||||
maxWidth: 160,
|
||||
maxHeight: 35,
|
||||
width: "100%",
|
||||
objectPosition: "left",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{!isMobile && (
|
||||
<Box>
|
||||
<Fade in={showCollapse}>
|
||||
<IconButton onClick={() => dispatch(setDrawerOpen(false))}>
|
||||
<ChevronLeft />
|
||||
</IconButton>
|
||||
</Fade>
|
||||
</Box>
|
||||
)}
|
||||
</DrawerHeaderContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DrawerHeader;
|
||||
165
src/component/Frame/NavBar/FileSelectedActions.tsx
Executable file
165
src/component/Frame/NavBar/FileSelectedActions.tsx
Executable file
@@ -0,0 +1,165 @@
|
||||
import { Badge, Box, IconButton, Stack, styled, Tooltip, useMediaQuery, useTheme } from "@mui/material";
|
||||
import React, { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FileResponse } from "../../../api/explorer.ts";
|
||||
import { clearSelected, ContextMenuTypes } from "../../../redux/fileManagerSlice.ts";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import { downloadFiles } from "../../../redux/thunks/download.ts";
|
||||
import {
|
||||
deleteFile,
|
||||
dialogBasedMoveCopy,
|
||||
openFileContextMenu,
|
||||
openShareDialog,
|
||||
renameFile,
|
||||
} from "../../../redux/thunks/file.ts";
|
||||
import { openViewers } from "../../../redux/thunks/viewer.ts";
|
||||
import useActionDisplayOpt from "../../FileManager/ContextMenu/useActionDisplayOpt.ts";
|
||||
import { FileManagerIndex } from "../../FileManager/FileManager.tsx";
|
||||
import { ActionButton, ActionButtonGroup } from "../../FileManager/TopBar/TopActions.tsx";
|
||||
import CopyOutlined from "../../Icons/CopyOutlined.tsx";
|
||||
import DeleteOutlined from "../../Icons/DeleteOutlined.tsx";
|
||||
import Dismiss from "../../Icons/Dismiss.tsx";
|
||||
import Download from "../../Icons/Download.tsx";
|
||||
import FolderArrowRightOutlined from "../../Icons/FolderArrowRightOutlined.tsx";
|
||||
import MoreHorizontal from "../../Icons/MoreHorizontal.tsx";
|
||||
import Open from "../../Icons/Open.tsx";
|
||||
import RenameOutlined from "../../Icons/RenameOutlined.tsx";
|
||||
import ShareOutlined from "../../Icons/ShareOutlined.tsx";
|
||||
|
||||
export interface FileSelectedActionsProps {
|
||||
targets: FileResponse[];
|
||||
}
|
||||
|
||||
const StyledActionButton = styled(ActionButton)(({ theme }) => ({
|
||||
// disabled
|
||||
"&.MuiButtonBase-root.Mui-disabled": {
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
fontSize: theme.typography.body2.fontSize,
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledActionButtonGroup = styled(ActionButtonGroup)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.default,
|
||||
}));
|
||||
|
||||
const FileSelectedActions = forwardRef(({ targets }: FileSelectedActionsProps, ref: React.Ref<HTMLElement>) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down("md"));
|
||||
const { t } = useTranslation();
|
||||
const displayOpt = useActionDisplayOpt(targets, ContextMenuTypes.file);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Stack direction={"row"} spacing={1} sx={{ height: "100%" }}>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
clearSelected({
|
||||
index: FileManagerIndex.main,
|
||||
value: undefined,
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
<Badge badgeContent={targets.length} color={"primary"}>
|
||||
<Dismiss />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<IconButton onClick={(e) => dispatch(openFileContextMenu(FileManagerIndex.main, targets[0], false, e))}>
|
||||
<Badge badgeContent={targets.length} color={"primary"}>
|
||||
<MoreHorizontal />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box ref={ref} sx={{ height: "100%" }}>
|
||||
<Stack direction={"row"} spacing={1} sx={{ height: "100%" }}>
|
||||
<StyledActionButtonGroup variant="outlined">
|
||||
<ActionButton
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
clearSelected({
|
||||
index: FileManagerIndex.main,
|
||||
value: undefined,
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
<Dismiss fontSize={"small"} />
|
||||
</ActionButton>
|
||||
<StyledActionButton disabled sx={{ color: (theme) => theme.palette.text.primary }}>
|
||||
{t("application:navbar.objectsSelected", {
|
||||
num: targets.length,
|
||||
})}
|
||||
</StyledActionButton>
|
||||
</StyledActionButtonGroup>
|
||||
{!isTablet && (
|
||||
<StyledActionButtonGroup variant="outlined">
|
||||
{displayOpt.showOpen && (
|
||||
<Tooltip title={t("application:fileManager.open")}>
|
||||
<ActionButton onClick={() => dispatch(openViewers(0, targets[0]))}>
|
||||
<Open fontSize={"small"} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{displayOpt.showDownload && (
|
||||
<Tooltip title={t("application:fileManager.download")}>
|
||||
<ActionButton onClick={() => dispatch(downloadFiles(0, targets))}>
|
||||
<Download fontSize={"small"} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{displayOpt.showCopy && (
|
||||
<Tooltip title={t("application:fileManager.copy")}>
|
||||
<ActionButton onClick={() => dispatch(dialogBasedMoveCopy(0, targets, true))}>
|
||||
<CopyOutlined fontSize={"small"} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{displayOpt.showMove && (
|
||||
<Tooltip title={t("application:fileManager.move")}>
|
||||
<ActionButton onClick={() => dispatch(dialogBasedMoveCopy(0, targets, false))}>
|
||||
<FolderArrowRightOutlined fontSize={"small"} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{displayOpt.showRename && (
|
||||
<Tooltip title={t("application:fileManager.rename")}>
|
||||
<ActionButton onClick={() => dispatch(renameFile(0, targets[0]))}>
|
||||
<RenameOutlined fontSize={"small"} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{displayOpt.showShare && (
|
||||
<Tooltip title={t("application:fileManager.share")}>
|
||||
<ActionButton onClick={() => dispatch(openShareDialog(0, targets[0]))}>
|
||||
<ShareOutlined fontSize={"small"} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{displayOpt.showDelete && (
|
||||
<Tooltip title={t("application:fileManager.delete")}>
|
||||
<ActionButton onClick={() => dispatch(deleteFile(0, targets))}>
|
||||
<DeleteOutlined fontSize="small" />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</StyledActionButtonGroup>
|
||||
)}
|
||||
<StyledActionButtonGroup variant="outlined">
|
||||
<ActionButton onClick={(e) => dispatch(openFileContextMenu(FileManagerIndex.main, targets[0], false, e))}>
|
||||
<MoreHorizontal fontSize={"small"} />
|
||||
</ActionButton>
|
||||
</StyledActionButtonGroup>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
export default FileSelectedActions;
|
||||
32
src/component/Frame/NavBar/NavBarMainActions.tsx
Executable file
32
src/component/Frame/NavBar/NavBarMainActions.tsx
Executable file
@@ -0,0 +1,32 @@
|
||||
import { CSSTransition, SwitchTransition } from "react-transition-group";
|
||||
import { useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { useMemo } from "react";
|
||||
import { Box } from "@mui/material";
|
||||
import SearchBar from "./SearchBar.tsx";
|
||||
import FileSelectedActions from "./FileSelectedActions.tsx";
|
||||
import { FileManagerIndex } from "../../FileManager/FileManager.tsx";
|
||||
|
||||
const NavBarMainActions = () => {
|
||||
const selected = useAppSelector((state) => state.fileManager[FileManagerIndex.main].selected);
|
||||
const targets = useMemo(() => {
|
||||
return Object.keys(selected).map((key) => selected[key]);
|
||||
}, [selected]);
|
||||
return (
|
||||
<>
|
||||
<SwitchTransition>
|
||||
<CSSTransition
|
||||
addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}
|
||||
classNames="fade"
|
||||
key={`${targets.length > 0}`}
|
||||
>
|
||||
<Box sx={{ height: "100%" }}>
|
||||
{targets.length == 0 && <SearchBar />}
|
||||
{targets.length > 0 && <FileSelectedActions targets={targets} />}
|
||||
</Box>
|
||||
</CSSTransition>
|
||||
</SwitchTransition>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBarMainActions;
|
||||
38
src/component/Frame/NavBar/NavIconTransition.tsx
Executable file
38
src/component/Frame/NavBar/NavIconTransition.tsx
Executable file
@@ -0,0 +1,38 @@
|
||||
import { Box, Fade, SvgIconProps } from "@mui/material";
|
||||
import { TransitionGroup } from "react-transition-group";
|
||||
import "../../Common/FadeTransition.css";
|
||||
import SvgIcon from "@mui/material/SvgIcon/SvgIcon";
|
||||
|
||||
export interface NavIconTransitionProps {
|
||||
fileIcon: ((props: SvgIconProps) => JSX.Element)[] | (typeof SvgIcon)[];
|
||||
active?: boolean;
|
||||
[key: string]: any;
|
||||
iconProps?: SvgIconProps;
|
||||
}
|
||||
|
||||
const NavIconTransition = ({ fileIcon, active, iconProps, ...rest }: NavIconTransitionProps) => {
|
||||
const [Active, InActive] = fileIcon;
|
||||
return (
|
||||
<Box {...rest}>
|
||||
<TransitionGroup>
|
||||
{active && (
|
||||
<Fade key={"active"}>
|
||||
<span>
|
||||
<Active sx={{ position: "absolute" }} {...iconProps} />
|
||||
</span>
|
||||
</Fade>
|
||||
)}
|
||||
{!active && (
|
||||
<Fade key={"inactive"}>
|
||||
<span>
|
||||
<InActive sx={{ position: "absolute" }} key={"inactive"} {...iconProps} />
|
||||
</span>
|
||||
</Fade>
|
||||
)}
|
||||
<InActive key={"3"} sx={{ visibility: "hidden" }} {...iconProps} />
|
||||
</TransitionGroup>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavIconTransition;
|
||||
305
src/component/Frame/NavBar/PageNavigation.tsx
Executable file
305
src/component/Frame/NavBar/PageNavigation.tsx
Executable file
@@ -0,0 +1,305 @@
|
||||
import { Icon as Iconify } from "@iconify/react";
|
||||
import { Box, SvgIconProps, useTheme } from "@mui/material";
|
||||
import SvgIcon from "@mui/material/SvgIcon/SvgIcon";
|
||||
import { memo, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { GroupPermission } from "../../../api/user.ts";
|
||||
import { useAppSelector } from "../../../redux/hooks.ts";
|
||||
import SessionManager from "../../../session";
|
||||
import { GroupBS } from "../../../session/utils.ts";
|
||||
import ProDialog from "../../Admin/Common/ProDialog.tsx";
|
||||
import BoxMultiple from "../../Icons/BoxMultiple.tsx";
|
||||
import BoxMultipleFilled from "../../Icons/BoxMultipleFilled.tsx";
|
||||
import CloudDownload from "../../Icons/CloudDownload.tsx";
|
||||
import CloudDownloadOutlined from "../../Icons/CloudDownloadOutlined.tsx";
|
||||
import CubeSync from "../../Icons/CubeSync.tsx";
|
||||
import CubeSyncFilled from "../../Icons/CubeSyncFilled.tsx";
|
||||
import CubeTree from "../../Icons/CubeTree.tsx";
|
||||
import CubeTreeFilled from "../../Icons/CubeTreeFilled.tsx";
|
||||
import DataHistogram from "../../Icons/DataHistogram.tsx";
|
||||
import DataHistogramFilled from "../../Icons/DataHistogramFilled.tsx";
|
||||
import Folder from "../../Icons/Folder.tsx";
|
||||
import FolderOutlined from "../../Icons/FolderOutlined.tsx";
|
||||
import HomeOutlined from "../../Icons/HomeOutlined.tsx";
|
||||
import Payment from "../../Icons/Payment.tsx";
|
||||
import PaymentFilled from "../../Icons/PaymentFilled.tsx";
|
||||
import People from "../../Icons/People.tsx";
|
||||
import PeopleFilled from "../../Icons/PeopleFilled.tsx";
|
||||
import Person from "../../Icons/Person.tsx";
|
||||
import PersonOutlined from "../../Icons/PersonOutlined.tsx";
|
||||
import PhoneLaptop from "../../Icons/PhoneLaptop.tsx";
|
||||
import PhoneLaptopOutlined from "../../Icons/PhoneLaptopOutlined.tsx";
|
||||
import SendLogging from "../../Icons/SendLogging.tsx";
|
||||
import SendLoggingFilled from "../../Icons/SendLoggingFilled.tsx";
|
||||
import Server from "../../Icons/Server.tsx";
|
||||
import ServerFilled from "../../Icons/ServerFilled.tsx";
|
||||
import Setting from "../../Icons/Setting.tsx";
|
||||
import SettingsOutlined from "../../Icons/SettingsOutlined.tsx";
|
||||
import ShareAndroid from "../../Icons/ShareAndroid.tsx";
|
||||
import ShareOutlined from "../../Icons/ShareOutlined.tsx";
|
||||
import Storage from "../../Icons/Storage.tsx";
|
||||
import StorageOutlined from "../../Icons/StorageOutlined.tsx";
|
||||
import Warning from "../../Icons/Warning.tsx";
|
||||
import WarningOutlined from "../../Icons/WarningOutlined.tsx";
|
||||
import WrenchSettings from "../../Icons/WrenchSettings.tsx";
|
||||
import { ProChip } from "../../Pages/Setting/SettingForm.tsx";
|
||||
import NavIconTransition from "./NavIconTransition.tsx";
|
||||
import SideNavItem from "./SideNavItem.tsx";
|
||||
|
||||
export interface NavigationItem {
|
||||
label: string;
|
||||
icon?: ((props: SvgIconProps) => JSX.Element)[] | (typeof SvgIcon)[];
|
||||
iconifyName?: string;
|
||||
path: string;
|
||||
pro?: boolean;
|
||||
}
|
||||
|
||||
let NavigationItems: NavigationItem[];
|
||||
NavigationItems = [
|
||||
{
|
||||
label: "navbar.myShare",
|
||||
icon: [ShareAndroid, ShareOutlined],
|
||||
path: "/shares",
|
||||
},
|
||||
];
|
||||
|
||||
const ConnectNavigationItem: NavigationItem = {
|
||||
label: "navbar.connect",
|
||||
icon: [PhoneLaptop, PhoneLaptopOutlined],
|
||||
path: "/connect",
|
||||
};
|
||||
|
||||
const TaskNavigationItem: NavigationItem = {
|
||||
label: "navbar.taskQueue",
|
||||
icon: [CubeSyncFilled, CubeSync],
|
||||
path: "/tasks",
|
||||
};
|
||||
|
||||
const RemoteDownloadNavigationItem: NavigationItem = {
|
||||
label: "navbar.remoteDownload",
|
||||
icon: [CloudDownload, CloudDownloadOutlined],
|
||||
path: "/downloads",
|
||||
};
|
||||
|
||||
export const SideNavItemComponent = ({ item }: { item: NavigationItem }) => {
|
||||
const { t } = useTranslation("application");
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const theme = useTheme();
|
||||
const [proOpen, setProOpen] = useState(false);
|
||||
const active = useMemo(() => {
|
||||
return location.pathname == item.path || location.pathname.startsWith(item.path + "/");
|
||||
}, [location.pathname, item.path]);
|
||||
return (
|
||||
<>
|
||||
{item.pro && <ProDialog open={proOpen} onClose={() => setProOpen(false)} />}
|
||||
<SideNavItem
|
||||
key={item.label}
|
||||
onClick={() =>
|
||||
item.pro ? setProOpen(true) : item.iconifyName ? window.open(item.path, "_blank") : navigate(item.path)
|
||||
}
|
||||
label={
|
||||
item.pro ? (
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
{t(item.label)}
|
||||
<ProChip
|
||||
sx={{
|
||||
height: "16px",
|
||||
fontSize: (t) => t.typography.caption.fontSize,
|
||||
}}
|
||||
label="Pro"
|
||||
color="primary"
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
t(item.label)
|
||||
)
|
||||
}
|
||||
active={active}
|
||||
icon={
|
||||
!item.icon ? (
|
||||
<Box
|
||||
sx={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
}}
|
||||
>
|
||||
<Iconify
|
||||
icon={item.iconifyName ?? ""}
|
||||
height={20}
|
||||
style={{
|
||||
color: theme.palette.action.active,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<NavIconTransition
|
||||
sx={{ px: 0, py: 0, pr: "14px", height: "20px" }}
|
||||
iconProps={{ fontSize: "small", color: "action" }}
|
||||
fileIcon={item.icon}
|
||||
active={active}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
let AdminNavigationItems: NavigationItem[];
|
||||
AdminNavigationItems = [
|
||||
{
|
||||
label: "dashboard:nav.summary",
|
||||
icon: [DataHistogramFilled, DataHistogram],
|
||||
path: "/admin/home",
|
||||
},
|
||||
{
|
||||
label: "dashboard:nav.settings",
|
||||
icon: [Setting, SettingsOutlined],
|
||||
path: "/admin/settings",
|
||||
},
|
||||
{
|
||||
label: "dashboard:nav.fileSystem",
|
||||
icon: [CubeTreeFilled, CubeTree],
|
||||
path: "/admin/filesystem",
|
||||
},
|
||||
{
|
||||
label: "dashboard:nav.storagePolicy",
|
||||
icon: [Storage, StorageOutlined],
|
||||
path: "/admin/policy",
|
||||
},
|
||||
{
|
||||
label: "dashboard:nav.nodes",
|
||||
icon: [ServerFilled, Server],
|
||||
path: "/admin/node",
|
||||
},
|
||||
{
|
||||
label: "dashboard:nav.groups",
|
||||
icon: [PeopleFilled, People],
|
||||
path: "/admin/group",
|
||||
},
|
||||
{
|
||||
label: "dashboard:nav.users",
|
||||
icon: [Person, PersonOutlined],
|
||||
path: "/admin/user",
|
||||
},
|
||||
{
|
||||
label: "dashboard:nav.files",
|
||||
icon: [Folder, FolderOutlined],
|
||||
path: "/admin/file",
|
||||
},
|
||||
{
|
||||
label: "dashboard:nav.entities",
|
||||
icon: [BoxMultipleFilled, BoxMultiple],
|
||||
path: "/admin/blob",
|
||||
},
|
||||
{
|
||||
label: "dashboard:nav.shares",
|
||||
icon: [ShareAndroid, ShareOutlined],
|
||||
path: "/admin/share",
|
||||
},
|
||||
{
|
||||
label: "dashboard:nav.tasks",
|
||||
icon: [CubeSyncFilled, CubeSync],
|
||||
path: "/admin/task",
|
||||
},
|
||||
{
|
||||
label: "dashboard:vas.orders",
|
||||
icon: [PaymentFilled, Payment],
|
||||
path: "/admin/payment",
|
||||
pro: true,
|
||||
},
|
||||
{
|
||||
label: "dashboard:nav.events",
|
||||
icon: [SendLoggingFilled, SendLogging],
|
||||
path: "/admin/event",
|
||||
pro: true,
|
||||
},
|
||||
{
|
||||
label: "dashboard:nav.abuseReport",
|
||||
icon: [Warning, WarningOutlined],
|
||||
path: "/admin/abuse",
|
||||
pro: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const AdminPageNavigation = memo(() => {
|
||||
return (
|
||||
<>
|
||||
<SideNavItemComponent key={AdminNavigationItems[0].label} item={AdminNavigationItems[0]} />
|
||||
<Box>
|
||||
{AdminNavigationItems.slice(1).map((item) => (
|
||||
<SideNavItemComponent key={item.label} item={item} />
|
||||
))}
|
||||
</Box>
|
||||
<SideNavItemComponent
|
||||
item={{
|
||||
label: "navbar.backToHomepage",
|
||||
icon: [HomeOutlined, HomeOutlined],
|
||||
path: "/home",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const PageNavigation = () => {
|
||||
const shopNavEnabled = useAppSelector((state) => state.siteConfig.basic.config.shop_nav_enabled);
|
||||
const appPromotionEnabled = useAppSelector((state) => state.siteConfig.basic.config.app_promotion);
|
||||
const user = SessionManager.currentLoginOrNull();
|
||||
const isAdmin = useMemo(() => {
|
||||
return GroupBS(user?.user).enabled(GroupPermission.is_admin);
|
||||
}, [user?.user?.group?.permission]);
|
||||
const remoteDownloadEnabled = useMemo(() => {
|
||||
return GroupBS(user?.user).enabled(GroupPermission.remote_download);
|
||||
}, [user?.user?.group?.permission]);
|
||||
const connectEnabled = useMemo(() => {
|
||||
return GroupBS(user?.user).enabled(GroupPermission.webdav) || appPromotionEnabled;
|
||||
}, [user?.user?.group?.permission, appPromotionEnabled]);
|
||||
const isLogin = !!user;
|
||||
const customNavItems = useAppSelector((state) => state.siteConfig.basic.config.custom_nav_items);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLogin && (
|
||||
<Box>
|
||||
<>
|
||||
{NavigationItems.map((item) => (
|
||||
<SideNavItemComponent key={item.label} item={item} />
|
||||
))}
|
||||
{connectEnabled && <SideNavItemComponent item={ConnectNavigationItem} />}
|
||||
<SideNavItemComponent item={TaskNavigationItem} />
|
||||
{remoteDownloadEnabled && <SideNavItemComponent item={RemoteDownloadNavigationItem} />}
|
||||
</>
|
||||
</Box>
|
||||
)}
|
||||
{customNavItems && customNavItems.length > 0 && (
|
||||
<Box>
|
||||
{customNavItems.map((item) => (
|
||||
<SideNavItemComponent
|
||||
key={item.name}
|
||||
item={{
|
||||
label: item.name,
|
||||
iconifyName: item.icon,
|
||||
path: item.url,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
{isLogin && isAdmin && (
|
||||
<SideNavItemComponent
|
||||
item={{
|
||||
label: "navbar.dashboard",
|
||||
icon: [WrenchSettings, WrenchSettings],
|
||||
path: "/admin/home",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageNavigation;
|
||||
79
src/component/Frame/NavBar/SearchBar.tsx
Executable file
79
src/component/Frame/NavBar/SearchBar.tsx
Executable file
@@ -0,0 +1,79 @@
|
||||
import { alpha, Button, IconButton, styled, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { setSearchPopup } from "../../../redux/globalStateSlice.ts";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import Search from "../../Icons/Search.tsx";
|
||||
|
||||
export const KeyIndicator = styled("code")(({ theme }) => ({
|
||||
backgroundColor: theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900],
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
boxShadow:
|
||||
theme.palette.mode === "light"
|
||||
? "0 1px 1px rgba(0, 0, 0, 0.2), 0 2px 0 0 rgba(255, 255, 255, 0.7) inset"
|
||||
: "0 1px 1px rgba(0, 0, 0, 0.2), 0 2px 0 0 #3d3e42 inset",
|
||||
padding: theme.spacing(0, 0.5),
|
||||
borderRadius: 4,
|
||||
}));
|
||||
|
||||
const SearchButton = styled(Button)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.default,
|
||||
color: theme.palette.text.disabled,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
pl: 2,
|
||||
pr: 8,
|
||||
" :hover": {
|
||||
border: `1px solid ${theme.palette.primary.main}`,
|
||||
backgroundColor: alpha(theme.palette.primary.main, 0.04),
|
||||
},
|
||||
}));
|
||||
|
||||
const SearchBar = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const { t } = useTranslation();
|
||||
useHotkeys(
|
||||
"/",
|
||||
() => {
|
||||
dispatch(setSearchPopup(true));
|
||||
},
|
||||
{ preventDefault: true },
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<IconButton onClick={() => dispatch(setSearchPopup(true))}>
|
||||
<Search />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchButton
|
||||
sx={(theme) => ({
|
||||
backgroundColor: theme.palette.background.default,
|
||||
color: theme.palette.text.disabled,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
pl: 2,
|
||||
pr: 8,
|
||||
height: "100%",
|
||||
})}
|
||||
onClick={() => dispatch(setSearchPopup(true))}
|
||||
variant={"outlined"}
|
||||
startIcon={<Search color={"primary"} />}
|
||||
>
|
||||
<Trans
|
||||
ns={"application"}
|
||||
i18nKey={"navbar.searchPlaceholder"}
|
||||
components={[
|
||||
<KeyIndicator sx={{ mx: 0.5 }}>
|
||||
<Typography variant={"body2"} />
|
||||
</KeyIndicator>,
|
||||
]}
|
||||
/>
|
||||
</SearchButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBar;
|
||||
77
src/component/Frame/NavBar/SideNavItem.tsx
Executable file
77
src/component/Frame/NavBar/SideNavItem.tsx
Executable file
@@ -0,0 +1,77 @@
|
||||
import { Box, ButtonBase, darken, lighten, styled } from "@mui/material";
|
||||
import * as React from "react";
|
||||
import { NoWrapTypography } from "../../Common/StyledComponents.tsx";
|
||||
|
||||
const StyledButtonBase = styled(ButtonBase)<{
|
||||
active?: boolean;
|
||||
}>(({ theme, active }) => ({
|
||||
borderRadius: "90px",
|
||||
display: "flex",
|
||||
justifyContent: "left",
|
||||
alignItems: "initial",
|
||||
width: "100%",
|
||||
backgroundColor: active
|
||||
? `${
|
||||
theme.palette.mode == "light"
|
||||
? lighten(theme.palette.primary.main, 0.7)
|
||||
: darken(theme.palette.primary.main, 0.7)
|
||||
}!important`
|
||||
: "initial",
|
||||
transition:
|
||||
"background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,border 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms",
|
||||
}));
|
||||
|
||||
export interface SideNavItemBaseProps {
|
||||
active?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
export const SideNavItemBase = React.forwardRef(
|
||||
({ active, ...rest }: SideNavItemBaseProps, ref: React.Ref<HTMLElement>) => {
|
||||
return <StyledButtonBase active={active} {...rest} ref={ref} />;
|
||||
},
|
||||
);
|
||||
|
||||
const StyledSideNavItem = styled(SideNavItemBase)<{ level?: number }>(({ theme, level }) => ({
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
padding: "4px",
|
||||
paddingLeft: `${28 + (level ?? 0) * 16}px`,
|
||||
height: "32px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}));
|
||||
|
||||
export interface SideNavItemProps extends SideNavItemBaseProps {
|
||||
icon?: React.ReactNode;
|
||||
label?: string | React.ReactNode;
|
||||
level?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const SideNavItem = React.forwardRef(
|
||||
({ icon, label, level, sx, ...rest }: SideNavItemProps, ref: React.Ref<HTMLElement>) => {
|
||||
return (
|
||||
<StyledSideNavItem
|
||||
level={level}
|
||||
sx={{
|
||||
...sx,
|
||||
}}
|
||||
{...rest}
|
||||
ref={ref}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 20,
|
||||
mr: "14px",
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
<NoWrapTypography variant={"body2"}>{label}</NoWrapTypography>
|
||||
</StyledSideNavItem>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default SideNavItem;
|
||||
78
src/component/Frame/NavBar/SplitHandle.tsx
Executable file
78
src/component/Frame/NavBar/SplitHandle.tsx
Executable file
@@ -0,0 +1,78 @@
|
||||
import { Box, Fade } from "@mui/material";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { setDrawerWidth } from "../../../redux/globalStateSlice.ts";
|
||||
import SessionManager, { UserSettings } from "../../../session";
|
||||
|
||||
export interface SplitHandleProps {}
|
||||
|
||||
const minDrawerWidth = 236;
|
||||
|
||||
const SplitHandle = (_props: SplitHandleProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [moving, setMoving] = useState(false);
|
||||
const [cursor, setCursor] = useState(0);
|
||||
const finalWidth = useRef(0);
|
||||
|
||||
const drawerWidth = useAppSelector((state) => state.globalState.drawerWidth);
|
||||
const drawerOpen = useAppSelector((state) => state.globalState.drawerOpen);
|
||||
|
||||
useEffect(() => {
|
||||
setCursor(drawerWidth - 4);
|
||||
finalWidth.current = drawerWidth - 4;
|
||||
}, []);
|
||||
|
||||
const handler = () => {
|
||||
setMoving(true);
|
||||
document.body.style.userSelect = "none";
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
const newWidth = e.clientX - document.body.offsetLeft;
|
||||
const cappedWidth = Math.max(Math.min(newWidth, window.innerWidth / 2), minDrawerWidth);
|
||||
setCursor(cappedWidth);
|
||||
finalWidth.current = cappedWidth;
|
||||
}
|
||||
function onMouseUp() {
|
||||
document.body.removeEventListener("mousemove", onMouseMove);
|
||||
setMoving(false);
|
||||
dispatch(setDrawerWidth(finalWidth.current + 4));
|
||||
SessionManager.set(UserSettings.DrawerWidth, finalWidth.current + 4);
|
||||
document.body.style.userSelect = "initial";
|
||||
}
|
||||
|
||||
document.body.addEventListener("mousemove", onMouseMove);
|
||||
document.body.addEventListener("mouseup", onMouseUp, { once: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{drawerOpen && (
|
||||
<Box
|
||||
onMouseDown={handler}
|
||||
sx={{
|
||||
cursor: "ew-resize",
|
||||
height: "100%",
|
||||
position: "fixed",
|
||||
width: 8,
|
||||
left: cursor,
|
||||
zIndex: (theme) => theme.zIndex.drawer + 2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Fade in={moving} unmountOnExit>
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
position: "fixed",
|
||||
width: 8,
|
||||
left: cursor,
|
||||
bgcolor: "divider",
|
||||
zIndex: (theme) => theme.zIndex.drawer + 1,
|
||||
}}
|
||||
/>
|
||||
</Fade>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SplitHandle;
|
||||
67
src/component/Frame/NavBar/StorageSummary.tsx
Executable file
67
src/component/Frame/NavBar/StorageSummary.tsx
Executable file
@@ -0,0 +1,67 @@
|
||||
import { LinearProgress, linearProgressClasses, Skeleton, styled, Typography } from "@mui/material";
|
||||
import { memo, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { updateUserCapacity } from "../../../redux/thunks/filemanager.ts";
|
||||
import { sizeToString } from "../../../util";
|
||||
import { RadiusFrame } from "../RadiusFrame.tsx";
|
||||
|
||||
const StyledBox = styled(RadiusFrame)(({ theme }) => ({
|
||||
padding: theme.spacing(1, 2, 1, 2),
|
||||
margin: theme.spacing(0, 2, 0, 2),
|
||||
}));
|
||||
|
||||
const StorageHeaderContainer = styled("div")(() => ({
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}));
|
||||
|
||||
const BorderLinearProgress = styled(LinearProgress)<{ warning: boolean }>(({ theme, warning }) => ({
|
||||
height: 8,
|
||||
borderRadius: 5,
|
||||
[`&.${linearProgressClasses.colorPrimary}`]: {
|
||||
backgroundColor: theme.palette.grey[theme.palette.mode === "light" ? 200 : 800],
|
||||
},
|
||||
[`& .${linearProgressClasses.bar}`]: {
|
||||
borderRadius: 5,
|
||||
backgroundColor: warning ? theme.palette.warning.main : theme.palette.primary.main,
|
||||
},
|
||||
marginTop: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StorageSummary = memo(() => {
|
||||
const { t } = useTranslation("application");
|
||||
const dispatch = useAppDispatch();
|
||||
const capacity = useAppSelector((state) => state.fileManager[0].capacity);
|
||||
useEffect(() => {
|
||||
if (!capacity) {
|
||||
dispatch(updateUserCapacity(0));
|
||||
return;
|
||||
}
|
||||
}, [capacity]);
|
||||
return (
|
||||
<StyledBox withBorder>
|
||||
<StorageHeaderContainer>
|
||||
<Typography variant={"subtitle2"}>{t("application:navbar.storage")}</Typography>
|
||||
</StorageHeaderContainer>
|
||||
{capacity && (
|
||||
<BorderLinearProgress
|
||||
warning={capacity.used > capacity.total}
|
||||
variant="determinate"
|
||||
value={Math.min(100, (capacity.used / capacity.total) * 100)}
|
||||
/>
|
||||
)}
|
||||
{!capacity && <Skeleton sx={{ mt: 1, height: 8 }} variant="rounded" />}
|
||||
<Typography variant={"caption"} color={"text.secondary"}>
|
||||
{capacity ? (
|
||||
`${sizeToString(capacity.used)} / ${sizeToString(capacity.total)}`
|
||||
) : (
|
||||
<Skeleton sx={{ width: "50%" }} variant="text" />
|
||||
)}
|
||||
</Typography>
|
||||
</StyledBox>
|
||||
);
|
||||
});
|
||||
|
||||
export default StorageSummary;
|
||||
146
src/component/Frame/NavBar/TopAppBar.tsx
Executable file
146
src/component/Frame/NavBar/TopAppBar.tsx
Executable file
@@ -0,0 +1,146 @@
|
||||
import { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar";
|
||||
import { AppBar, Box, Collapse, IconButton, Stack, Toolbar, Tooltip, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { Menu } from "@mui/icons-material";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
|
||||
import { setDrawerOpen, setMobileDrawerOpen } from "../../../redux/globalStateSlice.ts";
|
||||
import NewButton from "../../FileManager/NewButton.tsx";
|
||||
import UserAction from "./UserAction.tsx";
|
||||
import Setting from "../../Icons/Setting.tsx";
|
||||
import DarkThemeSwitcher from "./DarkThemeSwitcher.tsx";
|
||||
import NavBarMainActions from "./NavBarMainActions.tsx";
|
||||
import MusicPlayer from "../../Viewers/MusicPlayer/MusicPlayer.tsx";
|
||||
import { TaskListIconButton } from "../../Uploader/TaskListIconButton.tsx";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SessionManager from "../../../session";
|
||||
import { useContext, useState } from "react";
|
||||
import { DrawerPopover } from "./AppDrawer.tsx";
|
||||
import { PageVariant, PageVariantContext } from "../NavBarFrame.tsx";
|
||||
|
||||
interface AppBarProps extends MuiAppBarProps {
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
const TopAppBar = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const theme = useTheme();
|
||||
const pageVariant = useContext(PageVariantContext);
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const isMainPage = pageVariant == PageVariant.default;
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const open = useAppSelector((state) => state.globalState.drawerOpen);
|
||||
const mobileDrawerOpen = useAppSelector((state) => state.globalState.mobileDrawerOpen);
|
||||
const drawerWidth = useAppSelector((state) => state.globalState.drawerWidth);
|
||||
const musicPlayer = useAppSelector((state) => state.globalState.musicPlayer);
|
||||
const [mobileMenuAnchor, setMobileMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
|
||||
const appBarBg = theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900];
|
||||
const isLogin = !!SessionManager.currentLoginOrNull();
|
||||
|
||||
const onMobileMenuClicked = (e: React.MouseEvent<HTMLElement>) => {
|
||||
setMobileMenuAnchor(e.currentTarget);
|
||||
dispatch(setMobileDrawerOpen(true));
|
||||
};
|
||||
|
||||
const onCloseMobileMenu = () => {
|
||||
dispatch(setMobileDrawerOpen(false));
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
return (
|
||||
<AppBar
|
||||
elevation={0}
|
||||
enableColorOnDark
|
||||
sx={(theme) => ({
|
||||
transition: theme.transitions.create(["margin", "width"], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
backgroundColor: appBarBg,
|
||||
color: theme.palette.getContrastText(appBarBg),
|
||||
...(open &&
|
||||
!isMobile && {
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
marginLeft: `${drawerWidth}px`,
|
||||
transition: theme.transitions.create(["margin", "width"], {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
}),
|
||||
})}
|
||||
position="fixed"
|
||||
>
|
||||
<Toolbar
|
||||
sx={{
|
||||
"&.MuiToolbar-root.MuiToolbar-gutters": {
|
||||
paddingLeft: open && !isMobile ? theme.spacing(0) : theme.spacing(isMobile ? 2 : 3),
|
||||
transition: theme.transitions.create("padding", {
|
||||
easing: theme.transitions.easing.easeInOut,
|
||||
duration: theme.transitions.duration.standard,
|
||||
}),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Collapse orientation={"horizontal"} in={!open || isMobile}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
// @ts-ignore
|
||||
onClick={isMobile ? onMobileMenuClicked : () => dispatch(setDrawerOpen(true))}
|
||||
edge="start"
|
||||
sx={{
|
||||
mr: isMobile ? 1 : 2,
|
||||
ml: isMobile ? -1 : -1.5,
|
||||
}}
|
||||
>
|
||||
<Menu />
|
||||
</IconButton>
|
||||
</Collapse>
|
||||
{isMobile && (
|
||||
<>
|
||||
<DrawerPopover open={!!mobileDrawerOpen} anchorEl={mobileMenuAnchor} onClose={onCloseMobileMenu} />
|
||||
</>
|
||||
)}
|
||||
{!isMobile && isMainPage && (
|
||||
<Stack direction={"row"} spacing={1} sx={{ height: 42 }}>
|
||||
<NewButton />
|
||||
<NavBarMainActions />
|
||||
</Stack>
|
||||
)}
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Stack
|
||||
direction={"row"}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
}}
|
||||
spacing={isMobile ? 1 : 0}
|
||||
>
|
||||
{!isMobile && <TaskListIconButton />}
|
||||
{musicPlayer && <MusicPlayer />}
|
||||
{!isMobile ? (
|
||||
<>
|
||||
<DarkThemeSwitcher />
|
||||
{isLogin && (
|
||||
<Tooltip title={t("navbar.setting")}>
|
||||
<IconButton size="large" onClick={() => navigate("/settings")}>
|
||||
<Setting />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<UserAction />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{isMainPage && <NavBarMainActions />}
|
||||
{isMainPage && <NewButton />}
|
||||
<UserAction />
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopAppBar;
|
||||
173
src/component/Frame/NavBar/UserAction.tsx
Executable file
173
src/component/Frame/NavBar/UserAction.tsx
Executable file
@@ -0,0 +1,173 @@
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
IconButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
MenuList,
|
||||
Popover,
|
||||
PopoverProps,
|
||||
styled,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { bindPopover } from "material-ui-popup-state";
|
||||
import { bindTrigger, usePopupState } from "material-ui-popup-state/hooks";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { GroupPermission } from "../../../api/user.ts";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import { signout } from "../../../redux/thunks/session.ts";
|
||||
import SessionManager, { Session } from "../../../session";
|
||||
import { GroupBS } from "../../../session/utils.ts";
|
||||
import UserAvatar from "../../Common/User/UserAvatar.tsx";
|
||||
import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx";
|
||||
import HomeOutlined from "../../Icons/HomeOutlined.tsx";
|
||||
import Person from "../../Icons/Person.tsx";
|
||||
import SettingsOutlined from "../../Icons/SettingsOutlined.tsx";
|
||||
import SignOut from "../../Icons/SignOut.tsx";
|
||||
import WrenchSettings from "../../Icons/WrenchSettings.tsx";
|
||||
|
||||
const StyledTypography = styled(Typography)(() => ({
|
||||
lineHeight: 1,
|
||||
}));
|
||||
|
||||
const UserPopover = ({ open, onClose, ...rest }: PopoverProps) => {
|
||||
const user = SessionManager.currentUser();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAdmin = useMemo(() => {
|
||||
return GroupBS(user).enabled(GroupPermission.is_admin);
|
||||
}, [user.group?.permission]);
|
||||
|
||||
const signWithHint = (email: string) => {
|
||||
navigate("/session?phase=email&email=" + encodeURIComponent(email));
|
||||
};
|
||||
|
||||
const signOut = useCallback(() => {
|
||||
dispatch(signout());
|
||||
onClose && onClose({}, "backdropClick");
|
||||
}, []);
|
||||
|
||||
const openMyProfile = useCallback(() => {
|
||||
navigate(`/profile/${user?.id}`);
|
||||
onClose && onClose({}, "backdropClick");
|
||||
}, [user?.id]);
|
||||
|
||||
const openSetting = useCallback(() => {
|
||||
navigate(`/settings`);
|
||||
onClose && onClose({}, "backdropClick");
|
||||
}, [user?.id]);
|
||||
|
||||
const openDashboard = useCallback(() => {
|
||||
navigate(`/admin/home`);
|
||||
onClose && onClose({}, "backdropClick");
|
||||
}, [user?.id]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
{...rest}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
px: "12px",
|
||||
py: "10px",
|
||||
minWidth: "230px",
|
||||
display: "flex",
|
||||
maxWidth: "300px",
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
gap: "4px",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<StyledTypography paragraph={false} variant={"body2"} fontWeight={600}>
|
||||
{user.nickname}
|
||||
</StyledTypography>
|
||||
<StyledTypography variant={"body2"} paragraph={false} color={"textSecondary"} sx={{ ml: 1 }}>
|
||||
{user.group?.name}
|
||||
</StyledTypography>
|
||||
</Box>
|
||||
<StyledTypography variant={"caption"} color={"textSecondary"}>
|
||||
{user.email}
|
||||
</StyledTypography>
|
||||
</Box>
|
||||
<Divider />
|
||||
<MenuList dense sx={{ mx: 0.5 }}>
|
||||
{isAdmin && (
|
||||
<SquareMenuItem onClick={openDashboard}>
|
||||
<ListItemIcon>
|
||||
<WrenchSettings fontSize={"small"} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("navbar.dashboard")}</ListItemText>
|
||||
</SquareMenuItem>
|
||||
)}
|
||||
{isMobile && (
|
||||
<SquareMenuItem onClick={openSetting}>
|
||||
<ListItemIcon>
|
||||
<SettingsOutlined fontSize={"small"} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("navbar.setting")}</ListItemText>
|
||||
</SquareMenuItem>
|
||||
)}
|
||||
<SquareMenuItem onClick={openMyProfile}>
|
||||
<ListItemIcon>
|
||||
<HomeOutlined fontSize={"small"} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("navbar.myProfile")}</ListItemText>
|
||||
</SquareMenuItem>
|
||||
<SquareMenuItem onClick={signOut}>
|
||||
<ListItemIcon>
|
||||
<SignOut />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("login.logout")}</ListItemText>
|
||||
</SquareMenuItem>
|
||||
</MenuList>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const UserAction = () => {
|
||||
const navigate = useNavigate();
|
||||
const [current, setCurrent] = useState<Session>();
|
||||
const popupState = usePopupState({ variant: "popover", popupId: "user" });
|
||||
useEffect(() => {
|
||||
try {
|
||||
const session = SessionManager.currentLogin();
|
||||
if (session) {
|
||||
setCurrent(session);
|
||||
}
|
||||
} catch (e) {}
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<IconButton size={current ? "large" : undefined} {...(current ? bindTrigger(popupState) : {})}>
|
||||
{!current && <Person onClick={() => navigate("/session")} />}
|
||||
{current && <UserAvatar sx={{ width: 30, height: 30 }} user={current.user} />}
|
||||
</IconButton>
|
||||
<UserPopover {...bindPopover(popupState)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserAction;
|
||||
71
src/component/Frame/NavBarFrame.tsx
Executable file
71
src/component/Frame/NavBarFrame.tsx
Executable file
@@ -0,0 +1,71 @@
|
||||
import { Box, useMediaQuery, useTheme } from "@mui/material";
|
||||
import { createContext, useEffect } from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { setMobileDrawerOpen } from "../../redux/globalStateSlice.ts";
|
||||
import { useAppDispatch } from "../../redux/hooks.ts";
|
||||
import ContextMenu from "../FileManager/ContextMenu/ContextMenu.tsx";
|
||||
import Dialogs from "../FileManager/Dialogs/Dialogs.tsx";
|
||||
import DragLayer from "../FileManager/Dnd/DragLayer.tsx";
|
||||
import { FileManagerIndex } from "../FileManager/FileManager.tsx";
|
||||
import SearchPopup from "../FileManager/Search/SearchPopup.tsx";
|
||||
import Uploader from "../Uploader/Uploader.tsx";
|
||||
import AppDrawer from "./NavBar/AppDrawer.tsx";
|
||||
import Main from "./NavBar/AppMain.tsx";
|
||||
import SplitHandle from "./NavBar/SplitHandle.tsx";
|
||||
import TopAppBar from "./NavBar/TopAppBar.tsx";
|
||||
|
||||
export enum PageVariant {
|
||||
default,
|
||||
dashboard,
|
||||
}
|
||||
|
||||
export interface NavBarFrameProps {
|
||||
variant?: PageVariant;
|
||||
}
|
||||
|
||||
export const PageVariantContext = createContext(PageVariant.default);
|
||||
|
||||
export const AutoNavbarFrame = () => {
|
||||
const path = useLocation().pathname;
|
||||
return <NavBarFrame variant={path.startsWith("/admin") ? PageVariant.dashboard : PageVariant.default} />;
|
||||
};
|
||||
|
||||
const NavBarFrame = ({ variant }: NavBarFrameProps) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useAppDispatch();
|
||||
const isTouch = useMediaQuery("(pointer: coarse)");
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
dispatch(setMobileDrawerOpen(false));
|
||||
}
|
||||
}, [location]);
|
||||
return (
|
||||
<PageVariantContext.Provider value={variant ?? PageVariant.default}>
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: (theme) => (theme.palette.mode === "light" ? theme.palette.grey[100] : theme.palette.grey[900]),
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
{!isMobile && variant != PageVariant.dashboard && !isTouch && <DragLayer />}
|
||||
{!isMobile && !isTouch && <SplitHandle />}
|
||||
<TopAppBar />
|
||||
{!isMobile && <AppDrawer />}
|
||||
{variant != PageVariant.dashboard && <ContextMenu fmIndex={FileManagerIndex.main} />}
|
||||
<Dialogs />
|
||||
<Uploader />
|
||||
{variant != PageVariant.dashboard && <SearchPopup />}
|
||||
<Main />
|
||||
</DndProvider>
|
||||
</Box>
|
||||
</PageVariantContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBarFrame;
|
||||
56
src/component/Frame/PoweredBy.tsx
Executable file
56
src/component/Frame/PoweredBy.tsx
Executable file
@@ -0,0 +1,56 @@
|
||||
import { Box, BoxProps, Typography, useTheme } from "@mui/material";
|
||||
import LogoIcon from "./assets/logo.svg";
|
||||
import LogoIconDark from "./assets/logo_light.svg";
|
||||
|
||||
export interface PoweredByProps extends BoxProps {}
|
||||
|
||||
const PoweredBy = ({ ...rest }: PoweredByProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box {...rest}>
|
||||
<Box
|
||||
component="a"
|
||||
marginBottom={2}
|
||||
href="https://cloudreve.org"
|
||||
target="_blank"
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
textDecoration: "none",
|
||||
"& img": {
|
||||
filter: "grayscale(100%)",
|
||||
opacity: 0.3,
|
||||
},
|
||||
"&:hover": {
|
||||
"& img": {
|
||||
filter: "grayscale(0%)",
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: (theme) => theme.palette.action.disabled,
|
||||
}}
|
||||
fontWeight={500}
|
||||
>
|
||||
Powered by
|
||||
</Typography>
|
||||
<Box
|
||||
component="img"
|
||||
alt="Cloudreve"
|
||||
sx={{
|
||||
height: 20,
|
||||
}}
|
||||
src={theme.palette.mode === "dark" ? LogoIconDark : LogoIcon}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PoweredBy;
|
||||
10
src/component/Frame/RadiusFrame.tsx
Executable file
10
src/component/Frame/RadiusFrame.tsx
Executable file
@@ -0,0 +1,10 @@
|
||||
import { Box, styled } from "@mui/material";
|
||||
|
||||
export const RadiusFrame = styled(Box)<{
|
||||
withBorder?: boolean;
|
||||
square?: boolean;
|
||||
}>(({ theme, withBorder, square }) => ({
|
||||
borderRadius: square ? 0 : theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
border: withBorder ? `1px solid ${theme.palette.divider}` : "initial",
|
||||
}));
|
||||
54
src/component/Frame/assets/logo.svg
Executable file
54
src/component/Frame/assets/logo.svg
Executable file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 122 KiB |
54
src/component/Frame/assets/logo_light.svg
Executable file
54
src/component/Frame/assets/logo_light.svg
Executable file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 122 KiB |
Reference in New Issue
Block a user