first commit

This commit is contained in:
2025-10-19 13:31:11 +00:00
commit 8bfc183b66
1248 changed files with 195992 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
import { FileManager } from "../FileManager/FileManager.tsx";
import NavBarFrame, { AutoNavbarFrame } from "./NavBarFrame.tsx";
export { AutoNavbarFrame, FileManager, NavBarFrame };

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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",
}));

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 122 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 122 KiB