This commit is contained in:
2024-02-25 08:27:01 +08:00
commit 20c1fa08dc
279 changed files with 78489 additions and 0 deletions

233
src/Admin.js Normal file
View File

@@ -0,0 +1,233 @@
import React, { useEffect, useState } from "react";
import { CssBaseline, makeStyles } from "@material-ui/core";
import AlertBar from "./component/Common/Snackbar";
import Dashboard from "./component/Admin/Dashboard";
import { useHistory } from "react-router";
import Auth from "./middleware/Auth";
import { Route, Switch } from "react-router-dom";
import { ThemeProvider } from "@material-ui/styles";
import createTheme from "@material-ui/core/styles/createMuiTheme";
import { zhCN } from "@material-ui/core/locale";
import Index from "./component/Admin/Index";
import SiteInformation from "./component/Admin/Setting/SiteInformation";
import Access from "./component/Admin/Setting/Access";
import Mail from "./component/Admin/Setting/Mail";
import UploadDownload from "./component/Admin/Setting/UploadDownload";
import VAS from "./component/Admin/Setting/VAS";
import Theme from "./component/Admin/Setting/Theme";
import ImageSetting from "./component/Admin/Setting/Image";
import Policy from "./component/Admin/Policy/Policy";
import AddPolicy from "./component/Admin/Policy/AddPolicy";
import EditPolicyPreload from "./component/Admin/Policy/EditPolicy";
import Group from "./component/Admin/Group/Group";
import GroupForm from "./component/Admin/Group/GroupForm";
import EditGroupPreload from "./component/Admin/Group/EditGroup";
import User from "./component/Admin/User/User";
import UserForm from "./component/Admin/User/UserForm";
import EditUserPreload from "./component/Admin/User/EditUser";
import File from "./component/Admin/File/File";
import Share from "./component/Admin/Share/Share";
import Order from "./component/Admin/Order/Order";
import Download from "./component/Admin/Task/Download";
import Task from "./component/Admin/Task/Task";
import Import from "./component/Admin/File/Import";
import ReportList from "./component/Admin/Report/ReportList";
import Captcha from "./component/Admin/Setting/Captcha";
import Node from "./component/Admin/Node/Node";
import AddNode from "./component/Admin/Node/AddNode";
import EditNode from "./component/Admin/Node/EditNode";
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
},
content: {
flexGrow: 1,
padding: 0,
minWidth: 0,
},
toolbar: theme.mixins.toolbar,
}));
const theme = createTheme(
{
palette: {
background: {},
},
shape:{
borderRadius:12,
},
overrides: {
MuiButton: {
root: {
textTransform: "none",
},
},
MuiTab: {
root: {
textTransform: "none",
},
},
},
},
zhCN
);
export default function Admin() {
const classes = useStyles();
const history = useHistory();
const [show, setShow] = useState(false);
useEffect(() => {
const user = Auth.GetUser();
if (user && user.group) {
if (user.group.id !== 1) {
history.push("/home");
return;
}
setShow(true);
return;
}
history.push("/login");
// eslint-disable-next-line
}, []);
return (
<React.Fragment>
<ThemeProvider theme={theme}>
<div className={classes.root}>
<CssBaseline />
<AlertBar />
{show && (
<Dashboard
content={(path) => (
<Switch>
<Route path={`${path}/home`} exact>
<Index />
</Route>
<Route path={`${path}/basic`}>
<SiteInformation />
</Route>
<Route path={`${path}/access`}>
<Access />
</Route>
<Route path={`${path}/mail`}>
<Mail />
</Route>
<Route path={`${path}/upload`}>
<UploadDownload />
</Route>
<Route path={`${path}/vas`}>
<VAS />
</Route>
<Route path={`${path}/theme`}>
<Theme />
</Route>
<Route path={`${path}/image`}>
<ImageSetting />
</Route>
<Route path={`${path}/captcha`}>
<Captcha />
</Route>
<Route path={`${path}/policy`} exact>
<Policy />
</Route>
<Route
path={`${path}/policy/add/:type`}
exact
>
<AddPolicy />
</Route>
<Route
path={`${path}/policy/edit/:mode/:id`}
exact
>
<EditPolicyPreload />
</Route>
<Route path={`${path}/group`} exact>
<Group />
</Route>
<Route path={`${path}/group/add`} exact>
<GroupForm />
</Route>
<Route
path={`${path}/group/edit/:id`}
exact
>
<EditGroupPreload />
</Route>
<Route path={`${path}/user`} exact>
<User />
</Route>
<Route path={`${path}/user/add`} exact>
<UserForm />
</Route>
<Route path={`${path}/user/edit/:id`} exact>
<EditUserPreload />
</Route>
<Route path={`${path}/file`} exact>
<File />
</Route>
<Route path={`${path}/file/import`} exact>
<Import />
</Route>
<Route path={`${path}/share`} exact>
<Share />
</Route>
<Route path={`${path}/order`} exact>
<Order />
</Route>
<Route path={`${path}/download`} exact>
<Download />
</Route>
<Route path={`${path}/task`} exact>
<Task />
</Route>
<Route path={`${path}/report`} exact>
<ReportList />
</Route>
<Route path={`${path}/node`} exact>
<Node />
</Route>
<Route path={`${path}/node/add`} exact>
<AddNode />
</Route>
<Route path={`${path}/node/edit/:id`} exact>
<EditNode />
</Route>
</Switch>
)}
/>
)}
</div>
</ThemeProvider>
</React.Fragment>
);
}

280
src/App.js Normal file
View File

@@ -0,0 +1,280 @@
import React, { Suspense } from "react";
import AuthRoute from "./middleware/AuthRoute";
import NoAuthRoute from "./middleware/NoAuthRoute";
import Navbar from "./component/Navbar/Navbar.js";
import useMediaQuery from "@material-ui/core/useMediaQuery";
import AlertBar from "./component/Common/Snackbar";
import { createMuiTheme, lighten } from "@material-ui/core/styles";
import { useSelector } from "react-redux";
import { Redirect, Route, Switch, useRouteMatch } from "react-router-dom";
import Auth from "./middleware/Auth";
import { CssBaseline, makeStyles, ThemeProvider } from "@material-ui/core";
import PageLoading from "./component/Placeholder/PageLoading.js";
import { changeThemeColor } from "./utils";
import NotFound from "./component/Share/NotFound";
// Lazy loads
import LoginForm from "./component/Login/LoginForm";
import FileManager from "./component/FileManager/FileManager.js";
import VideoPreview from "./component/Viewer/Video.js";
import SearchResult from "./component/Share/SearchResult";
import MyShare from "./component/Share/MyShare";
import Download from "./component/Download/Download";
import SharePreload from "./component/Share/SharePreload";
import DocViewer from "./component/Viewer/Doc";
import TextViewer from "./component/Viewer/Text";
import Quota from "./component/VAS/Quota";
import BuyQuota from "./component/VAS/BuyQuota";
import WebDAV from "./component/Setting/WebDAV";
import Tasks from "./component/Setting/Tasks";
import Profile from "./component/Setting/Profile";
import UserSetting from "./component/Setting/UserSetting";
import QQCallback from "./component/Login/QQ";
import Register from "./component/Login/Register";
import Activation from "./component/Login/Activication";
import ResetForm from "./component/Login/ResetForm";
import Reset from "./component/Login/Reset";
import CodeViewer from "./component/Viewer/Code";
import SiteNotice from "./component/Modals/SiteNotice";
import MusicPlayer from "./component/FileManager/MusicPlayer";
import EpubViewer from "./component/Viewer/Epub";
import { useTranslation } from "react-i18next";
const PDFViewer = React.lazy(() =>
import(/* webpackChunkName: "pdf" */ "./component/Viewer/PDF")
);
export default function App() {
const themeConfig = useSelector((state) => state.siteConfig.theme);
const isLogin = useSelector((state) => state.viewUpdate.isLogin);
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
const { t } = useTranslation();
const theme = React.useMemo(() => {
themeConfig.palette.type = prefersDarkMode ? "dark" : "light";
const prefer = Auth.GetPreference("theme_mode");
if (prefer) {
themeConfig.palette.type = prefer;
}
const theme = createMuiTheme({
...themeConfig,
palette: {
...themeConfig.palette,
primary: {
...themeConfig.palette.primary,
main:
themeConfig.palette.type === "dark"
? lighten(themeConfig.palette.primary.main, 0.3)
: themeConfig.palette.primary.main,
},
},
shape: {
...themeConfig.shape,
borderRadius: 12,
},
overrides: {
MuiButton: {
root: {
textTransform: "none",
},
},
MuiTab: {
root: {
textTransform: "none",
},
},
},
});
changeThemeColor(
themeConfig.palette.type === "dark"
? theme.palette.background.default
: theme.palette.primary.main
);
return theme;
}, [prefersDarkMode, themeConfig]);
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
},
content: {
flexGrow: 1,
padding: theme.spacing(0),
minWidth: 0,
},
toolbar: theme.mixins.toolbar,
}));
const classes = useStyles();
const { path } = useRouteMatch();
return (
<React.Fragment>
<ThemeProvider theme={theme}>
<div className={classes.root} id="container">
<CssBaseline />
<AlertBar />
<Navbar />
<main className={classes.content}>
<div className={classes.toolbar} />
<Switch>
<AuthRoute exact path={path} isLogin={isLogin}>
<Redirect
to={{
pathname: "/home",
}}
/>
</AuthRoute>
<AuthRoute path={`${path}home`} isLogin={isLogin}>
<>
<SiteNotice />
<FileManager />
</>
</AuthRoute>
<AuthRoute path={`${path}video`} isLogin={isLogin}>
<VideoPreview />
</AuthRoute>
<AuthRoute path={`${path}text`} isLogin={isLogin}>
<TextViewer />
</AuthRoute>
<AuthRoute path={`${path}doc`} isLogin={isLogin}>
<DocViewer />
</AuthRoute>
<AuthRoute path={`${path}pdf`} isLogin={isLogin}>
<Suspense fallback={<PageLoading />}>
<PDFViewer />
</Suspense>
</AuthRoute>
<AuthRoute path={`${path}code`} isLogin={isLogin}>
<CodeViewer />
</AuthRoute>
<AuthRoute path={`${path}epub`} isLogin={isLogin}>
<EpubViewer />
</AuthRoute>
<AuthRoute path={`${path}aria2`} isLogin={isLogin}>
<Download />
</AuthRoute>
<AuthRoute path={`${path}shares`} isLogin={isLogin}>
<MyShare />
</AuthRoute>
<Route path={`${path}search`} isLogin={isLogin}>
<SearchResult />
</Route>
<AuthRoute path={`${path}quota`} isLogin={isLogin}>
<Quota />
</AuthRoute>
<AuthRoute path={`${path}buy`} isLogin={isLogin}>
<BuyQuota />
</AuthRoute>
<AuthRoute
path={`${path}setting`}
isLogin={isLogin}
>
<UserSetting />
</AuthRoute>
<AuthRoute
path={`${path}profile/:id`}
isLogin={isLogin}
>
<Profile />
</AuthRoute>
<AuthRoute
path={`${path}connect`}
isLogin={isLogin}
>
<WebDAV />
</AuthRoute>
<AuthRoute path={`${path}tasks`} isLogin={isLogin}>
<Tasks />
</AuthRoute>
<NoAuthRoute
exact
path={`${path}login`}
isLogin={isLogin}
>
<LoginForm />
</NoAuthRoute>
<NoAuthRoute
exact
path={`${path}signup`}
isLogin={isLogin}
>
<Register />
</NoAuthRoute>
<Route path={`${path}activate`} exact>
<Activation />
</Route>
<Route path={`${path}reset`} exact>
<ResetForm />
</Route>
<Route path={`${path}forget`} exact>
<Reset />
</Route>
<Route path={`${path}login/qq`}>
<QQCallback />
</Route>
<Route exact path={`${path}s/:id`}>
<SharePreload />
</Route>
<Route path={`${path}s/:id/video(/)*`}>
<VideoPreview />
</Route>
<Route path={`${path}s/:id/doc(/)*`}>
<DocViewer />
</Route>
<Route path={`${path}s/:id/text(/)*`}>
<TextViewer />
</Route>
<Route path={`${path}s/:id/pdf(/)*`}>
<Suspense fallback={<PageLoading />}>
<PDFViewer />
</Suspense>
</Route>
<Route path={`${path}s/:id/code(/)*`}>
<CodeViewer />
</Route>
<Route path={`${path}s/:id/epub(/)*`}>
<EpubViewer />
</Route>
<Route path="*">
<NotFound
msg={t("pageNotFound", { ns: "common" })}
/>
</Route>
</Switch>
</main>
<MusicPlayer />
</div>
</ThemeProvider>
</React.Fragment>
);
}

9
src/App.test.js Normal file
View File

@@ -0,0 +1,9 @@
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
it("renders without crashing", () => {
const div = document.createElement("div");
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

View File

@@ -0,0 +1,77 @@
import React, { useEffect, useState } from "react";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import InputAdornment from "@material-ui/core/InputAdornment";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import FormHelperText from "@material-ui/core/FormHelperText";
export default function DomainInput({ onChange, value, required, label }) {
const [domain, setDomain] = useState("");
const [protocol, setProtocol] = useState("https://");
const [error, setError] = useState();
useState(() => {
value = value ? value : "";
if (value.startsWith("https://")) {
setDomain(value.replace("https://", ""));
setProtocol("https://");
} else {
if (value !== "") {
setDomain(value.replace("http://", ""));
setProtocol("http://");
}
}
}, [value]);
useEffect(() => {
if (protocol === "http://" && window.location.protocol === "https:") {
setError(
"您当前站点启用了 HTTPS ,此处选择 HTTP 可能会导致无法连接。"
);
} else {
setError("");
}
}, [protocol]);
return (
<FormControl>
<InputLabel htmlFor="component-helper">{label}</InputLabel>
<Input
error={error !== ""}
value={domain}
onChange={(e) => {
setDomain(e.target.value);
onChange({
target: {
value: protocol + e.target.value,
},
});
}}
required={required}
startAdornment={
<InputAdornment position="start">
<Select
value={protocol}
onChange={(e) => {
setProtocol(e.target.value);
onChange({
target: {
value: e.target.value + domain,
},
});
}}
>
<MenuItem value={"http://"}>http://</MenuItem>
<MenuItem value={"https://"}>https://</MenuItem>
</Select>
</InputAdornment>
}
/>
{error !== "" && (
<FormHelperText error={error !== ""}>{error}</FormHelperText>
)}
</FormControl>
);
}

View File

@@ -0,0 +1,94 @@
import React, { useCallback } from "react";
import Autocomplete from "@material-ui/lab/Autocomplete";
import TextField from "@material-ui/core/TextField";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Typography from "@material-ui/core/Typography";
import { toggleSnackbar } from "../../../redux/explorer";
export default function FileSelector({ onChange, value, label }) {
const [selectValue, setSelectValue] = React.useState(
value.map((v) => {
return {
ID: v,
Name: "文件ID " + v,
};
})
);
const [loading, setLoading] = React.useState(false);
const [inputValue, setInputValue] = React.useState("");
const [options, setOptions] = React.useState([]);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
React.useEffect(() => {
let active = true;
if (
inputValue === "" ||
selectValue.findIndex((v) => v.ID.toString() === inputValue) >= 0
) {
setOptions([]);
return;
}
setLoading(true);
API.post("/admin/file/list", {
page: 1,
page_size: 10,
order_by: "id desc",
conditions: {
id: inputValue,
},
searches: {},
})
.then((response) => {
if (active) {
let newOptions = [];
newOptions = [...newOptions, ...response.data.items];
setOptions(newOptions);
}
setLoading(false);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
setLoading(false);
});
return () => {
active = false;
};
}, [selectValue, inputValue]);
return (
<Autocomplete
multiple
style={{ width: 300 }}
options={options}
getOptionLabel={(option) =>
typeof option === "string" ? option : option.Name
}
filterOptions={(x) => x}
loading={loading}
autoComplete
includeInputInList
filterSelectedOptions
value={selectValue}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
onChange={(event, newValue) => {
setSelectValue(newValue);
onChange(JSON.stringify(newValue.map((v) => v.ID)));
}}
renderInput={(params) => (
<TextField {...params} label={label} type={"number"} />
)}
renderOption={(option) => (
<Typography noWrap>{option.Name}</Typography>
)}
/>
);
}

View File

@@ -0,0 +1,95 @@
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../redux/explorer";
import InputLabel from "@material-ui/core/InputLabel";
import Select from "@material-ui/core/Select";
import Input from "@material-ui/core/Input";
import Chip from "@material-ui/core/Chip";
import MenuItem from "@material-ui/core/MenuItem";
import { getSelectItemStyles } from "../../../utils";
import FormHelperText from "@material-ui/core/FormHelperText";
import { FormControl } from "@material-ui/core";
import API from "../../../middleware/Api";
import { useTheme } from "@material-ui/core/styles";
export default function PolicySelector({
onChange,
value,
label,
helperText,
filter,
}) {
const [policies, setPolicies] = useState({});
const theme = useTheme();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/policy/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
conditions: {},
})
.then((response) => {
const res = {};
let data = response.data.items;
if (filter) {
data = data.filter(filter);
}
data.forEach((v) => {
res[v.ID] = v.Name;
});
setPolicies(res);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
return (
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">{label}</InputLabel>
<Select
labelId="demo-mutiple-chip-label"
id="demo-mutiple-chip"
multiple
value={value}
onChange={onChange}
input={<Input id="select-multiple-chip" />}
renderValue={(selected) => (
<div>
{selected.map((value) => (
<Chip
style={{
margin: 2,
}}
key={value}
size={"small"}
label={policies[value]}
/>
))}
</div>
)}
>
{Object.keys(policies).map((pid) => (
<MenuItem
key={pid}
value={pid}
style={getSelectItemStyles(pid, value, theme)}
>
{policies[pid]}
</MenuItem>
))}
</Select>
<FormHelperText id="component-helper-text">
{helperText}
</FormHelperText>
</FormControl>
);
}

View File

@@ -0,0 +1,101 @@
import React, { useCallback, useEffect, useState } from "react";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import InputAdornment from "@material-ui/core/InputAdornment";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../redux/explorer";
import FormHelperText from "@material-ui/core/FormHelperText";
const unitTransform = (v) => {
if (!v || v.toString() === "0") {
return [0, 1024 * 1024];
}
for (let i = 4; i >= 0; i--) {
const base = Math.pow(1024, i);
if (v % base === 0) {
return [v / base, base];
}
}
};
export default function SizeInput({
onChange,
min,
value,
required,
label,
max,
suffix,
}) {
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const [unit, setUnit] = useState(1);
const [val, setVal] = useState(value);
const [err, setError] = useState("");
useEffect(() => {
onChange({
target: {
value: (val * unit).toString(),
},
});
if (val * unit > max || val * unit < min) {
setError("不符合尺寸限制");
} else {
setError("");
}
}, [val, unit, max, min]);
useEffect(() => {
const res = unitTransform(value);
setUnit(res[1]);
setVal(res[0]);
}, []);
return (
<FormControl error={err !== ""}>
<InputLabel htmlFor="component-helper">{label}</InputLabel>
<Input
style={{ width: 200 }}
value={val}
type={"number"}
inputProps={{ step: 1 }}
onChange={(e) => setVal(e.target.value)}
required={required}
endAdornment={
<InputAdornment position="end">
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={unit}
onChange={(e) => setUnit(e.target.value)}
>
<MenuItem value={1}>B{suffix && suffix}</MenuItem>
<MenuItem value={1024}>
KB{suffix && suffix}
</MenuItem>
<MenuItem value={1024 * 1024}>
MB{suffix && suffix}
</MenuItem>
<MenuItem value={1024 * 1024 * 1024}>
GB{suffix && suffix}
</MenuItem>
<MenuItem value={1024 * 1024 * 1024 * 1024}>
TB{suffix && suffix}
</MenuItem>
</Select>
</InputAdornment>
}
/>
{err !== "" && <FormHelperText>{err}</FormHelperText>}
</FormControl>
);
}

View File

@@ -0,0 +1,477 @@
import React, { useCallback, useEffect, useState } from "react";
import clsx from "clsx";
import { lighten, makeStyles, useTheme } from "@material-ui/core/styles";
import Drawer from "@material-ui/core/Drawer";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import List from "@material-ui/core/List";
import Typography from "@material-ui/core/Typography";
import Divider from "@material-ui/core/Divider";
import IconButton from "@material-ui/core/IconButton";
import MenuIcon from "@material-ui/icons/Menu";
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import UserAvatar from "../Navbar/UserAvatar";
import {
Assignment,
AttachMoney,
Category,
CloudDownload,
Contactless,
Contacts,
Group,
Home,
Image,
InsertDriveFile,
Language,
ListAlt,
Mail,
Palette,
Person,
Report,
Settings,
SettingsEthernet,
Share,
ShoppingCart,
Storage,
} from "@material-ui/icons";
import { withStyles } from "@material-ui/core";
import MuiExpansionPanel from "@material-ui/core/ExpansionPanel";
import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary";
import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails";
import { useHistory, useLocation } from "react-router";
import { useRouteMatch } from "react-router-dom";
import { useDispatch } from "react-redux";
import { changeSubTitle } from "../../redux/viewUpdate/action";
import pathHelper from "../../utils/page";
import { useTranslation } from "react-i18next";
const ExpansionPanel = withStyles({
root: {
maxWidth: "100%",
boxShadow: "none",
"&:not(:last-child)": {
borderBottom: 0,
},
"&:before": {
display: "none",
},
"&$expanded": { margin: 0 },
},
expanded: {},
})(MuiExpansionPanel);
const ExpansionPanelSummary = withStyles({
root: {
minHeight: 0,
padding: 0,
"&$expanded": {
minHeight: 0,
},
},
content: {
maxWidth: "100%",
margin: 0,
display: "block",
"&$expanded": {
margin: "0",
},
},
expanded: {},
})(MuiExpansionPanelSummary);
const ExpansionPanelDetails = withStyles((theme) => ({
root: {
display: "block",
padding: theme.spacing(0),
},
}))(MuiExpansionPanelDetails);
const drawerWidth = 240;
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
width: "100%",
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
appBarShift: {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
menuButton: {
marginRight: 36,
},
hide: {
display: "none",
},
drawer: {
width: drawerWidth,
flexShrink: 0,
whiteSpace: "nowrap",
},
drawerOpen: {
width: drawerWidth,
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
drawerClose: {
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: "hidden",
width: theme.spacing(7) + 1,
[theme.breakpoints.up("sm")]: {
width: theme.spacing(9) + 1,
},
},
title: {
flexGrow: 1,
},
toolbar: {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: theme.spacing(0, 1),
...theme.mixins.toolbar,
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
},
sub: {
paddingLeft: 36,
color: theme.palette.text.secondary,
},
subMenu: {
backgroundColor: theme.palette.background.default,
paddingTop: 0,
paddingBottom: 0,
},
active: {
backgroundColor: lighten(theme.palette.primary.main, 0.8),
color: theme.palette.primary.main,
"&:hover": {
backgroundColor: lighten(theme.palette.primary.main, 0.7),
},
},
activeText: {
fontWeight: 500,
},
activeIcon: {
color: theme.palette.primary.main,
},
}));
const items = [
{
title: "nav.summary",
icon: <Home />,
path: "home",
},
{
title: "nav.settings",
icon: <Settings />,
sub: [
{
title: "nav.basicSetting",
path: "basic",
icon: <Language />,
},
{
title: "nav.publicAccess",
path: "access",
icon: <Contacts />,
},
{
title: "nav.email",
path: "mail",
icon: <Mail />,
},
{
title: "nav.transportation",
path: "upload",
icon: <SettingsEthernet />,
},
{
title: "vas.vas",
path: "vas",
icon: <AttachMoney />,
},
{
title: "nav.appearance",
path: "theme",
icon: <Palette />,
},
{
title: "nav.image",
path: "image",
icon: <Image />,
},
{
title: "nav.captcha",
path: "captcha",
icon: <Category />,
},
],
},
{
title: "nav.storagePolicy",
icon: <Storage />,
path: "policy",
},
{
title: "nav.nodes",
icon: <Contactless />,
path: "node",
},
{
title: "nav.groups",
icon: <Group />,
path: "group",
},
{
title: "nav.users",
icon: <Person />,
path: "user",
},
{
title: "nav.files",
icon: <InsertDriveFile />,
path: "file",
},
{
title: "nav.shares",
icon: <Share />,
path: "share",
},
{
title: "vas.reports",
icon: <Report />,
path: "report",
},
{
title: "vas.orders",
icon: <ShoppingCart />,
path: "order",
},
{
title: "nav.tasks",
icon: <Assignment />,
sub: [
{
title: "nav.remoteDownload",
path: "download",
icon: <CloudDownload />,
},
{
title: "nav.generalTasks",
path: "task",
icon: <ListAlt />,
},
],
},
];
export default function Dashboard({ content }) {
const { t } = useTranslation("dashboard");
const classes = useStyles();
const theme = useTheme();
const [open, setOpen] = useState(!pathHelper.isMobile());
const [menuOpen, setMenuOpen] = useState(null);
const history = useHistory();
const location = useLocation();
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
const dispatch = useDispatch();
const SetSubTitle = useCallback(
(title) => dispatch(changeSubTitle(title)),
[dispatch]
);
useEffect(() => {
SetSubTitle(t("nav.title"));
}, []);
useEffect(() => {
return () => {
SetSubTitle();
};
}, []);
const { path } = useRouteMatch();
return (
<div className={classes.root}>
<AppBar
position="fixed"
className={clsx(classes.appBar, {
[classes.appBarShift]: open,
})}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
className={clsx(classes.menuButton, {
[classes.hide]: open,
})}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title} noWrap>
{t("nav.dashboard")}
</Typography>
<UserAvatar />
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
className={clsx(classes.drawer, {
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
}),
}}
>
<div className={classes.toolbar}>
<IconButton onClick={handleDrawerClose}>
{theme.direction === "rtl" ? (
<ChevronRightIcon />
) : (
<ChevronLeftIcon />
)}
</IconButton>
</div>
<Divider />
<List className={classes.noPadding}>
{items.map((item) => {
if (item.path !== undefined) {
return (
<ListItem
onClick={() =>
history.push("/admin/" + item.path)
}
button
className={clsx({
[classes.active]: location.pathname.startsWith(
"/admin/" + item.path
),
})}
key={item.title}
>
<ListItemIcon
className={clsx({
[classes.activeIcon]: location.pathname.startsWith(
"/admin/" + item.path
),
})}
>
{item.icon}
</ListItemIcon>
<ListItemText
className={clsx({
[classes.activeText]: location.pathname.startsWith(
"/admin/" + item.path
),
})}
primary={t(item.title)}
/>
</ListItem>
);
}
return (
// eslint-disable-next-line react/jsx-key
<ExpansionPanel
key={item.title}
square
expanded={menuOpen === item.title}
onChange={(event, isExpanded) => {
setMenuOpen(isExpanded ? item.title : null);
}}
>
<ExpansionPanelSummary
aria-controls="panel1d-content"
id="panel1d-header"
>
<ListItem button key={item.title}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={t(item.title)} />
</ListItem>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<List className={classes.subMenu}>
{item.sub.map((sub) => (
<ListItem
onClick={() =>
history.push(
"/admin/" + sub.path
)
}
className={clsx({
[classes.sub]: open,
[classes.active]: location.pathname.startsWith(
"/admin/" + sub.path
),
})}
button
key={sub.title}
>
<ListItemIcon
className={clsx({
[classes.activeIcon]: location.pathname.startsWith(
"/admin/" + sub.path
),
})}
>
{sub.icon}
</ListItemIcon>
<ListItemText
primary={t(sub.title)}
/>
</ListItem>
))}
</List>
</ExpansionPanelDetails>
</ExpansionPanel>
);
})}
</List>
</Drawer>
<main className={classes.content}>
<div className={classes.toolbar} />
{content(path)}
</main>
</div>
);
}

View File

@@ -0,0 +1,268 @@
import React, { useEffect, useState } from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import FormControl from "@material-ui/core/FormControl";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Switch from "@material-ui/core/Switch";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles(() => ({
formContainer: {
margin: "8px 0 8px 0",
},
}));
const defaultGroup = {
name: "",
group_id: 2,
time: "",
price: "",
score: "",
des: "",
highlight: false,
};
const groupEditToForm = (target) => {
return {
...target,
time: (target.time / 86400).toString(),
price: (target.price / 100).toString(),
score: target.score.toString(),
des: target.des.join("\n"),
};
};
export default function AddGroup({ open, onClose, onSubmit, groupEdit }) {
const { t } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tCommon } = useTranslation("common");
const classes = useStyles();
const [groups, setGroups] = useState([]);
const [group, setGroup] = useState(defaultGroup);
useEffect(() => {
if (groupEdit) {
setGroup(groupEditToForm(groupEdit));
} else {
setGroup(defaultGroup);
}
}, [groupEdit]);
useEffect(() => {
if (open && groups.length === 0) {
API.get("/admin/groups")
.then((response) => {
setGroups(response.data);
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
.catch(() => {});
}
// eslint-disable-next-line
}, [open]);
const handleChange = (name) => (event) => {
setGroup({
...group,
[name]: event.target.value,
});
};
const handleCheckChange = (name) => (event) => {
setGroup({
...group,
[name]: event.target.checked,
});
};
const submit = (e) => {
e.preventDefault();
const groupCopy = { ...group };
groupCopy.time = parseInt(groupCopy.time) * 86400;
groupCopy.price = parseInt(groupCopy.price) * 100;
groupCopy.score = parseInt(groupCopy.score);
groupCopy.id = groupEdit ? groupEdit.id : new Date().valueOf();
groupCopy.des = groupCopy.des.split("\n");
onSubmit(groupCopy, groupEdit !== null);
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth={"xs"}
scroll={"paper"}
>
<form onSubmit={submit}>
<DialogTitle id="alert-dialog-title">
{groupEdit ? t("editMembership") : t("addMembership")}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("name")}
</InputLabel>
<Input
value={group.name}
onChange={handleChange("name")}
required
/>
<FormHelperText id="component-helper-text">
{t("productNameDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("group")}
</InputLabel>
<Select
value={group.group_id}
onChange={handleChange("group_id")}
required
>
{groups.map((v) => {
if (v.ID !== 3) {
return (
<MenuItem value={v.ID}>
{v.Name}
</MenuItem>
);
}
return null;
})}
</Select>
<FormHelperText id="component-helper-text">
{t("groupDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("durationDay")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={group.time}
onChange={handleChange("time")}
required
/>
<FormHelperText id="component-helper-text">
{t("durationGroupDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("priceYuan")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0.01,
step: 0.01,
}}
value={group.price}
onChange={handleChange("price")}
required
/>
<FormHelperText id="component-helper-text">
{t("groupPriceDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("priceCredits")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={group.score}
onChange={handleChange("score")}
required
/>
<FormHelperText id="component-helper-text">
{t("priceCreditsDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("productDescription")}
</InputLabel>
<Input
value={group.des}
onChange={handleChange("des")}
multiline
rowsMax={10}
required
/>
<FormHelperText id="component-helper-text">
{t("productDescriptionDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={group.highlight}
onChange={handleCheckChange(
"highlight"
)}
/>
}
label={t("highlight")}
/>
<FormHelperText id="component-helper-text">
{t("highlightDes")}
</FormHelperText>
</FormControl>
</div>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
{tCommon("cancel")}
</Button>
<Button type={"submit"} color="primary">
{tCommon("ok")}
</Button>
</DialogActions>
</form>
</Dialog>
);
}

View File

@@ -0,0 +1,193 @@
import React, { useEffect, useState } from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import FormControl from "@material-ui/core/FormControl";
import SizeInput from "../Common/SizeInput";
import { makeStyles } from "@material-ui/core/styles";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles(() => ({
formContainer: {
margin: "8px 0 8px 0",
},
}));
const packEditToForm = (target) => {
return {
...target,
size: target.size.toString(),
time: (target.time / 86400).toString(),
price: (target.price / 100).toString(),
score: target.score.toString(),
};
};
const defaultPack = {
name: "",
size: "1073741824",
time: "",
price: "",
score: "",
};
export default function AddPack({ open, onClose, onSubmit, packEdit }) {
const { t } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tCommon } = useTranslation("common");
const classes = useStyles();
const [pack, setPack] = useState(defaultPack);
useEffect(() => {
if (packEdit) {
setPack(packEditToForm(packEdit));
} else {
setPack(defaultPack);
}
}, [packEdit]);
const handleChange = (name) => (event) => {
setPack({
...pack,
[name]: event.target.value,
});
};
const submit = (e) => {
e.preventDefault();
const packCopy = { ...pack };
packCopy.size = parseInt(packCopy.size);
packCopy.time = parseInt(packCopy.time) * 86400;
packCopy.price = parseInt(packCopy.price) * 100;
packCopy.score = parseInt(packCopy.score);
packCopy.id = packEdit ? packEdit.id : new Date().valueOf();
onSubmit(packCopy, packEdit !== null);
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth={"xs"}
>
<form onSubmit={submit}>
<DialogTitle id="alert-dialog-title">
{packEdit ? t("editStoragePack") : t("addStoragePack")}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("name")}
</InputLabel>
<Input
value={pack.name}
onChange={handleChange("name")}
required
/>
<FormHelperText id="component-helper-text">
{t("productNameDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<SizeInput
value={pack.size}
onChange={handleChange("size")}
min={1}
label={t("size")}
max={9223372036854775807}
required
/>
<FormHelperText id="component-helper-text">
{t("packSizeDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("durationDay")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={pack.time}
onChange={handleChange("time")}
required
/>
<FormHelperText id="component-helper-text">
{t("durationDayDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("priceYuan")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0.01,
step: 0.01,
}}
value={pack.price}
onChange={handleChange("price")}
required
/>
<FormHelperText id="component-helper-text">
{t("packPriceDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("priceCredits")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={pack.score}
onChange={handleChange("score")}
required
/>
<FormHelperText id="component-helper-text">
{t("priceCreditsDes")}
</FormHelperText>
</FormControl>
</div>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
{tCommon("cancel")}
</Button>
<Button type={"submit"} color="primary">
{tCommon("ok")}
</Button>
</DialogActions>
</form>
</Dialog>
);
}

View File

@@ -0,0 +1,145 @@
import React from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import Typography from "@material-ui/core/Typography";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import Grid from "@material-ui/core/Grid";
import Card from "@material-ui/core/Card";
import CardActionArea from "@material-ui/core/CardActionArea";
import { makeStyles } from "@material-ui/core/styles";
import CardMedia from "@material-ui/core/CardMedia";
import CardContent from "@material-ui/core/CardContent";
import { useHistory } from "react-router";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
cardContainer: {
display: "flex",
},
cover: {
width: 100,
height: 60,
},
card: {},
content: {
flex: "1 0 auto",
},
bg: {
backgroundColor: theme.palette.background.default,
padding: "24px 24px",
},
dialogFooter: {
justifyContent: "space-between",
},
}));
const policies = [
{
name: "local",
img: "local.png",
path: "/admin/policy/add/local",
},
{
name: "remote",
img: "remote.png",
path: "/admin/policy/add/remote",
},
{
name: "qiniu",
img: "qiniu.png",
path: "/admin/policy/add/qiniu",
},
{
name: "oss",
img: "oss.png",
path: "/admin/policy/add/oss",
},
{
name: "upyun",
img: "upyun.png",
path: "/admin/policy/add/upyun",
},
{
name: "cos",
img: "cos.png",
path: "/admin/policy/add/cos",
},
{
name: "onedrive",
img: "onedrive.png",
path: "/admin/policy/add/onedrive",
},
{
name: "s3",
img: "s3.png",
path: "/admin/policy/add/s3",
},
];
export default function AddPolicy({ open, onClose }) {
const { t } = useTranslation("dashboard", { keyPrefix: "policy" });
const { t: tCommon } = useTranslation("common");
const classes = useStyles();
const location = useHistory();
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth={"sm"}
fullWidth
>
<DialogTitle id="alert-dialog-title">
{t("selectAStorageProvider")}
</DialogTitle>
<DialogContent dividers className={classes.bg}>
<Grid container spacing={2}>
{policies.map((v, index) => (
<Grid key={index} item sm={12} md={6}>
<Card className={classes.card}>
<CardActionArea
onClick={() => {
location.push(v.path);
onClose();
}}
className={classes.cardContainer}
>
<CardMedia
className={classes.cover}
image={"/static/img/" + v.img}
/>
<CardContent className={classes.content}>
<Typography
variant="subtitle1"
color="textSecondary"
>
{t(v.name)}
</Typography>
</CardContent>
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
</DialogContent>
<DialogActions className={classes.dialogFooter}>
<Button
onClick={() =>
window.open(t("comparesStoragePoliciesLink"))
}
color="primary"
>
{t("comparesStoragePolicies")}
</Button>
<Button onClick={onClose} color="primary">
{tCommon("cancel")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,183 @@
import React, { useCallback, useState } from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import FormControl from "@material-ui/core/FormControl";
import { makeStyles } from "@material-ui/core/styles";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles(() => ({
formContainer: {
margin: "8px 0 8px 0",
},
}));
export default function AddRedeem({ open, onClose, products, onSuccess }) {
const { t } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tCommon } = useTranslation("common");
const { t: tApp } = useTranslation();
const classes = useStyles();
const [input, setInput] = useState({
num: 1,
id: 0,
time: 1,
});
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const handleChange = (name) => (event) => {
setInput({
...input,
[name]: event.target.value,
});
};
const submit = (e) => {
e.preventDefault();
setLoading(true);
input.num = parseInt(input.num);
input.id = parseInt(input.id);
input.time = parseInt(input.time);
input.type = 2;
for (let i = 0; i < products.length; i++) {
if (products[i].id === input.id) {
if (products[i].group_id !== undefined) {
input.type = 1;
} else {
input.type = 0;
}
break;
}
}
API.post("/admin/redeem", input)
.then((response) => {
onSuccess(response.data);
onClose();
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth={"xs"}
>
<form onSubmit={submit}>
<DialogTitle id="alert-dialog-title">
{t("generateGiftCode")}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("numberOfCodes")}
</InputLabel>
<Input
type={"number"}
inputProps={{
step: 1,
min: 1,
max: 100,
}}
value={input.num}
onChange={handleChange("num")}
required
/>
<FormHelperText id="component-helper-text">
{t("numberOfCodesDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("linkedProduct")}
</InputLabel>
<Select
value={input.id}
onChange={(e) => {
handleChange("id")(e);
}}
>
{products.map((v) => (
<MenuItem
key={v.id}
value={v.id}
data-type={"1"}
>
{v.name}
</MenuItem>
))}
<MenuItem value={0}>
{tApp("vas.credits")}
</MenuItem>
</Select>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("productQyt")}
</InputLabel>
<Input
type={"number"}
inputProps={{
step: 1,
min: 1,
}}
value={input.time}
onChange={handleChange("time")}
required
/>
<FormHelperText id="component-helper-text">
{t("productQytDes")}
</FormHelperText>
</FormControl>
</div>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
disabled={loading}
onClick={onClose}
color="default"
>
{tCommon("cancel")}
</Button>
<Button disabled={loading} type={"submit"} color="primary">
{tCommon("ok")}
</Button>
</DialogActions>
</form>
</Dialog>
);
}

View File

@@ -0,0 +1,33 @@
import React from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import Typography from "@material-ui/core/Typography";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import { useTranslation } from "react-i18next";
export default function AlertDialog({ title, msg, open, onClose }) {
const { t } = useTranslation("common");
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{title}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<Typography>{msg}</Typography>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
{t("ok")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,355 @@
import React, { useCallback, useState } from "react";
import DialogContent from "@material-ui/core/DialogContent";
import { CompactPicker } from "react-color";
import Typography from "@material-ui/core/Typography";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import Grid from "@material-ui/core/Grid";
import TextField from "@material-ui/core/TextField";
import { createMuiTheme, makeStyles } from "@material-ui/core/styles";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import IconButton from "@material-ui/core/IconButton";
import { Add, Menu } from "@material-ui/icons";
import { ThemeProvider } from "@material-ui/styles";
import Fab from "@material-ui/core/Fab";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
picker: {
"& div": {
boxShadow: "none !important",
},
marginTop: theme.spacing(1),
},
"@global": {
".compact-picker:parent ": {
boxShadow: "none !important",
},
},
statusBar: {
height: 24,
width: "100%",
},
fab: {
textAlign: "right",
},
}));
export default function CreateTheme({ open, onClose, onSubmit }) {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const { t: tGlobal } = useTranslation("common");
const classes = useStyles();
const [theme, setTheme] = useState({
palette: {
primary: {
main: "#3f51b5",
contrastText: "#fff",
},
secondary: {
main: "#d81b60",
contrastText: "#fff",
},
},
});
const subTheme = useCallback(() => {
try {
return createMuiTheme(theme);
} catch (e) {
return createMuiTheme({});
}
}, [theme]);
return (
<Dialog open={open} onClose={onClose} fullWidth maxWidth={"md"}>
<DialogContent>
<Grid container>
<Grid spacing={2} md={8} xs={12} container>
<Grid md={6} xs={12} item>
<Typography variant="h6" gutterBottom>
{t("primaryColor")}
</Typography>
<TextField
value={theme.palette.primary.main}
onChange={(e) => {
setTheme({
...theme,
palette: {
...theme.palette,
primary: {
...theme.palette.primary,
main: e.target.value,
},
},
});
}}
fullWidth
/>
<div className={classes.picker}>
<CompactPicker
colors={[
"#4D4D4D",
"#999999",
"#FFFFFF",
"#f44336",
"#ff9800",
"#ffeb3b",
"#cddc39",
"#A4DD00",
"#00bcd4",
"#03a9f4",
"#AEA1FF",
"#FDA1FF",
"#333333",
"#808080",
"#cccccc",
"#ff5722",
"#ffc107",
"#FCC400",
"#8bc34a",
"#4caf50",
"#009688",
"#2196f3",
"#3f51b5",
"#e91e63",
"#000000",
"#666666",
"#B3B3B3",
"#9F0500",
"#C45100",
"#FB9E00",
"#808900",
"#194D33",
"#0C797D",
"#0062B1",
"#673ab7",
"#9c27b0",
]}
color={theme.palette.primary.main}
onChangeComplete={(c) => {
setTheme({
...theme,
palette: {
...theme.palette,
primary: {
...theme.palette.primary,
main: c.hex,
},
},
});
}}
/>
</div>
</Grid>
<Grid md={6} xs={12} item>
<Typography variant="h6" gutterBottom>
{t("secondaryColor")}
</Typography>
<TextField
value={theme.palette.secondary.main}
onChange={(e) => {
setTheme({
...theme,
palette: {
...theme.palette,
secondary: {
...theme.palette.secondary,
main: e.target.value,
},
},
});
}}
fullWidth
/>
<div className={classes.picker}>
<CompactPicker
colors={[
"#4D4D4D",
"#999999",
"#FFFFFF",
"#ff1744",
"#ff3d00",
"#ffeb3b",
"#cddc39",
"#A4DD00",
"#00bcd4",
"#00e5ff",
"#AEA1FF",
"#FDA1FF",
"#333333",
"#808080",
"#cccccc",
"#ff5722",
"#ffea00",
"#ffc400",
"#c6ff00",
"#00e676",
"#76ff03",
"#00b0ff",
"#2979ff",
"#f50057",
"#000000",
"#666666",
"#B3B3B3",
"#9F0500",
"#C45100",
"#FB9E00",
"#808900",
"#1de9b6",
"#0C797D",
"#3d5afe",
"#651fff",
"#d500f9",
]}
color={theme.palette.secondary.main}
onChangeComplete={(c) => {
setTheme({
...theme,
palette: {
...theme.palette,
secondary: {
...theme.palette.secondary,
main: c.hex,
},
},
});
}}
/>
</div>
</Grid>
<Grid md={6} xs={12} item>
<Typography variant="h6" gutterBottom>
{t("primaryColorText")}
</Typography>
<TextField
value={theme.palette.primary.contrastText}
onChange={(e) => {
setTheme({
...theme,
palette: {
...theme.palette,
primary: {
...theme.palette.primary,
contrastText: e.target.value,
},
},
});
}}
fullWidth
/>
<div className={classes.picker}>
<CompactPicker
color={theme.palette.primary.contrastText}
onChangeComplete={(c) => {
setTheme({
...theme,
palette: {
...theme.palette,
primary: {
...theme.palette.primary,
contrastText: c.hex,
},
},
});
}}
/>
</div>
</Grid>
<Grid md={6} xs={12} item>
<Typography variant="h6" gutterBottom>
{t("secondaryColorText")}
</Typography>
<TextField
value={theme.palette.secondary.contrastText}
onChange={(e) => {
setTheme({
...theme,
palette: {
...theme.palette,
secondary: {
...theme.palette.secondary,
contrastText: e.target.value,
},
},
});
}}
fullWidth
/>
<div className={classes.picker}>
<CompactPicker
color={theme.palette.secondary.contrastText}
onChangeComplete={(c) => {
setTheme({
...theme,
palette: {
...theme.palette,
secondary: {
...theme.palette.secondary,
contrastText: c.hex,
},
},
});
}}
/>
</div>
</Grid>
</Grid>
<Grid spacing={2} md={4} xs={12}>
<ThemeProvider theme={subTheme()}>
<div
className={classes.statusBar}
style={{
backgroundColor: subTheme().palette.primary
.dark,
}}
/>
<AppBar position="static">
<Toolbar>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="menu"
>
<Menu />
</IconButton>
<Typography
variant="h6"
className={classes.title}
>
Color
</Typography>
</Toolbar>
</AppBar>
<div style={{ padding: 16 }}>
<TextField
fullWidth
color={"secondary"}
label={"Text input"}
/>
<div
className={classes.fab}
style={{ paddingTop: 64 }}
>
<Fab color="secondary" aria-label="add">
<Add />
</Fab>
</div>
</div>
</ThemeProvider>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
{tGlobal("cancel")}
</Button>
<Button onClick={() => onSubmit(theme)} color="primary">
{tGlobal("ok")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,134 @@
import React, { useCallback, useEffect, useState } from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import TextField from "@material-ui/core/TextField";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
export default function FileFilter({ setFilter, setSearch, open, onClose }) {
const { t } = useTranslation("dashboard", { keyPrefix: "file" });
const { t: tDashboard } = useTranslation("dashboard");
const { t: tCommon } = useTranslation("common");
const [input, setInput] = useState({
policy_id: "all",
user_id: "",
});
const [policies, setPolicies] = useState([]);
const [keywords, setKeywords] = useState("");
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const handleChange = (name) => (event) => {
setInput({ ...input, [name]: event.target.value });
};
useEffect(() => {
API.post("/admin/policy/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
conditions: {},
})
.then((response) => {
setPolicies(response.data.items);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
const submit = () => {
const res = {};
Object.keys(input).forEach((v) => {
if (input[v] !== "all" && input[v] !== "") {
res[v] = input[v];
}
});
setFilter(res);
if (keywords !== "") {
setSearch({
name: keywords,
});
} else {
setSearch({});
}
onClose();
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
fullWidth
maxWidth={"xs"}
>
<DialogTitle id="alert-dialog-title">
{tDashboard("user.filterCondition")}
</DialogTitle>
<DialogContent>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">
{t("storagePolicy")}
</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={input.policy_id}
onChange={handleChange("policy_id")}
>
<MenuItem value={"all"}>
{tDashboard("user.all")}
</MenuItem>
{policies.map((v) => {
return (
<MenuItem key={v.ID} value={v.ID.toString()}>
{v.Name}
</MenuItem>
);
})}
</Select>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<TextField
value={input.user_id}
onChange={handleChange("user_id")}
id="standard-basic"
label={t("uploaderID")}
/>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<TextField
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
id="standard-basic"
label={t("searchFileName")}
/>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
{tCommon("cancel")}
</Button>
<Button onClick={submit} color="primary">
{tDashboard("user.apply")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,180 @@
import React from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import TableBody from "@material-ui/core/TableBody";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import { useTranslation } from "react-i18next";
const magicVars = [
{
value: "{randomkey16}",
des: "16digitsRandomString",
example: "N6IimT5XZP324ACK",
fileOnly: false,
},
{
value: "{randomkey8}",
des: "8digitsRandomString",
example: "gWz78q30",
fileOnly: false,
},
{
value: "{timestamp}",
des: "secondTimestamp",
example: "1582692933",
fileOnly: false,
},
{
value: "{timestamp_nano}",
des: "nanoTimestamp",
example: "1582692933231834600",
fileOnly: false,
},
{
value: "{uid}",
des: "uid",
example: "1",
fileOnly: false,
},
{
value: "{originname}",
des: "originalFileName",
example: "MyPico.mp4",
fileOnly: true,
},
{
value: "{originname_without_ext}",
des: "originFileNameNoext",
example: "MyPico",
fileOnly: true,
},
{
value: "{ext}",
des: "extension",
example: ".jpg",
fileOnly: true,
},
{
value: "{uuid}",
des: "uuidV4",
example: "31f0a770-659d-45bf-a5a9-166c06f33281",
fileOnly: true,
},
{
value: "{date}",
des: "date",
example: "20060102",
fileOnly: false,
},
{
value: "{datetime}",
des: "dateAndTime",
example: "20060102150405",
fileOnly: false,
},
{
value: "{year}",
des: "year",
example: "2006",
fileOnly: false,
},
{
value: "{month}",
des: "month",
example: "01",
fileOnly: false,
},
{
value: "{day}",
des: "day",
example: "02",
fileOnly: false,
},
{
value: "{hour}",
des: "hour",
example: "15",
fileOnly: false,
},
{
value: "{minute}",
des: "minute",
example: "04",
fileOnly: false,
},
{
value: "{second}",
des: "second",
example: "05",
fileOnly: false,
},
];
export default function MagicVar({ isFile, open, onClose, isSlave }) {
const { t } = useTranslation("dashboard", { keyPrefix: "policy.magicVar" });
const { t: tCommon } = useTranslation("common");
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{isFile ? t("fileNameMagicVar") : t("pathMagicVar")}
</DialogTitle>
<DialogContent>
<TableContainer>
<Table size="small" aria-label="a dense table">
<TableHead>
<TableRow>
<TableCell>{t("variable")}</TableCell>
<TableCell>{t("description")}</TableCell>
<TableCell>{t("example")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{magicVars.map((m) => {
if (!m.fileOnly || isFile) {
return (
<TableRow>
<TableCell
component="th"
scope="row"
>
{m.value}
</TableCell>
<TableCell>{t(m.des)}</TableCell>
<TableCell>{m.example}</TableCell>
</TableRow>
);
}
})}
{!isFile && (
<TableRow>
<TableCell component="th" scope="row">
{"{path}"}
</TableCell>
<TableCell>{t("userUploadPath")}</TableCell>
<TableCell>/MyFile/Documents/</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
{tCommon("close")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,103 @@
import React, { useState } from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import TextField from "@material-ui/core/TextField";
import { useTranslation } from "react-i18next";
export default function ShareFilter({ setFilter, setSearch, open, onClose }) {
const { t } = useTranslation("dashboard", { keyPrefix: "share" });
const { t: tDashboard } = useTranslation("dashboard");
const { t: tCommon } = useTranslation("common");
const [input, setInput] = useState({
is_dir: "all",
user_id: "",
});
const [keywords, setKeywords] = useState("");
const handleChange = (name) => (event) => {
setInput({ ...input, [name]: event.target.value });
};
const submit = () => {
const res = {};
Object.keys(input).forEach((v) => {
if (input[v] !== "all" && input[v] !== "") {
res[v] = input[v];
}
});
setFilter(res);
if (keywords !== "") {
setSearch({
source_name: keywords,
});
} else {
setSearch({});
}
onClose();
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
fullWidth
maxWidth={"xs"}
>
<DialogTitle id="alert-dialog-title">
{tDashboard("user.filterCondition")}
</DialogTitle>
<DialogContent>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">
{t("srcType")}
</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={input.is_dir}
onChange={handleChange("is_dir")}
>
<MenuItem value={"all"}>
{tDashboard("user.all")}
</MenuItem>
<MenuItem value={"1"}>{t("folder")}</MenuItem>
<MenuItem value={"0"}>{t("file")}</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<TextField
value={input.user_id}
onChange={handleChange("user_id")}
id="standard-basic"
label={tDashboard("file.uploaderID")}
/>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<TextField
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
id="standard-basic"
label={tDashboard("file.searchFileName")}
/>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
{tCommon("cancel")}
</Button>
<Button onClick={submit} color="primary">
{tCommon("ok")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,139 @@
import React, { useCallback, useEffect, useState } from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import TextField from "@material-ui/core/TextField";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
export default function UserFilter({ setFilter, setSearch, open, onClose }) {
const { t } = useTranslation("dashboard", { keyPrefix: "user" });
const { t: tCommon } = useTranslation("common");
const [input, setInput] = useState({
group_id: "all",
status: "all",
});
const [groups, setGroups] = useState([]);
const [keywords, setKeywords] = useState("");
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const handleChange = (name) => (event) => {
setInput({ ...input, [name]: event.target.value });
};
useEffect(() => {
API.get("/admin/groups")
.then((response) => {
setGroups(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
const submit = () => {
const res = {};
Object.keys(input).forEach((v) => {
if (input[v] !== "all") {
res[v] = input[v];
}
});
setFilter(res);
if (keywords !== "") {
setSearch({
nick: keywords,
email: keywords,
});
} else {
setSearch({});
}
onClose();
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
fullWidth
maxWidth={"xs"}
>
<DialogTitle id="alert-dialog-title">
{t("filterCondition")}
</DialogTitle>
<DialogContent>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">
{t("group")}
</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={input.group_id}
onChange={handleChange("group_id")}
>
<MenuItem value={"all"}>{t("all")}</MenuItem>
{groups.map((v) => {
if (v.ID === 3) {
return null;
}
return (
<MenuItem key={v.ID} value={v.ID.toString()}>
{v.Name}
</MenuItem>
);
})}
</Select>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<InputLabel id="demo-simple-select-label">
{t("userStatus")}
</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={input.status}
onChange={handleChange("status")}
>
<MenuItem value={"all"}>{t("all")}</MenuItem>
<MenuItem value={"0"}>{t("active")}</MenuItem>
<MenuItem value={"1"}>{t("notActivated")}</MenuItem>
<MenuItem value={"2"}>{t("banned")}</MenuItem>
<MenuItem value={"3"}>{t("bannedBySys")}</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<TextField
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
id="standard-basic"
label={t("searchNickUserName")}
/>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
{tCommon("cancel")}
</Button>
<Button onClick={submit} color="primary">
{t("apply")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,502 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import { sizeToString } from "../../../utils";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import { useHistory } from "react-router";
import IconButton from "@material-ui/core/IconButton";
import { Delete, DeleteForever, FilterList, LinkOff } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Checkbox from "@material-ui/core/Checkbox";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { lighten } from "@material-ui/core";
import Link from "@material-ui/core/Link";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import Badge from "@material-ui/core/Badge";
import FileFilter from "../Dialogs/FileFilter";
import { formatLocalTime } from "../../../utils/datetime";
import { toggleSnackbar } from "../../../redux/explorer";
import Chip from "@material-ui/core/Chip";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
disabledBadge: {
marginLeft: theme.spacing(1),
height: 18,
},
}));
export default function File() {
const { t } = useTranslation("dashboard", { keyPrefix: "file" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [files, setFiles] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [users, setUsers] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const history = useHistory();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/file/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setFiles(response.data.items);
setTotal(response.data.total);
setSelected([]);
setUsers(response.data.users);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deleteFile = (id, unlink = false) => {
setLoading(true);
API.post("/admin/file/delete", { id: [id], unlink })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("deleteAsync"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch =
(force, unlink = false) =>
() => {
setLoading(true);
API.post("/admin/file/delete", { id: selected, force, unlink })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("deleteAsync"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = files.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<FileFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<Button
color={"primary"}
onClick={() => history.push("/admin/file/import")}
variant={"contained"}
style={{
alignSelf: "center",
}}
>
{t("import")}
</Button>
<div className={classes.headerRight}>
<Tooltip title={tDashboard("user.filter")}>
<IconButton
style={{ marginRight: 8 }}
onClick={() => setFilterDialog(true)}
>
<Badge
color="secondary"
variant="dot"
invisible={
Object.keys(search).length === 0 &&
Object.keys(filter).length === 0
}
>
<FilterList />
</Badge>
</IconButton>
</Tooltip>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
{tDashboard("user.selectedObjects", {
num: selected.length,
})}
</Typography>
<Tooltip title={tDashboard("policy.delete")}>
<IconButton
onClick={deleteBatch(false)}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
<Tooltip title={t("forceDelete")}>
<IconButton
onClick={deleteBatch(true)}
disabled={loading}
aria-label="delete"
>
<DeleteForever />
</IconButton>
</Tooltip>
<Tooltip title={tDashboard("file.unlink")}>
<IconButton
disabled={loading}
onClick={deleteBatch(true, true)}
size={"small"}
>
<LinkOff />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < files.length
}
checked={
files.length > 0 &&
selected.length === files.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 59 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 250 }}>
<TableSortLabel
active={orderBy[0] === "name"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"name",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("name")}
{orderBy[0] === "name" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell
align={"right"}
style={{ minWidth: 70 }}
>
<TableSortLabel
active={orderBy[0] === "size"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"size",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("size")}
{orderBy[0] === "size" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 120 }}>
{t("uploader")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{t("createdAt")}
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{tDashboard("policy.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{files.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell>
<Link
target={"_blank"}
color="inherit"
href={
"/api/v3/admin/file/preview/" +
row.ID
}
>
{row.Name}
{row.UploadSessionID && (
<Chip
className={
classes.disabledBadge
}
size="small"
label={t("uploading")}
/>
)}
</Link>
</TableCell>
<TableCell align={"right"}>
{sizeToString(row.Size)}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" + row.UserID
}
>
{users[row.UserID]
? users[row.UserID].Nick
: t("unknownUploader")}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(
row.CreatedAt,
"YYYY-MM-DD H:mm:ss"
)}
</TableCell>
<TableCell>
<Tooltip
title={tDashboard("policy.delete")}
>
<IconButton
disabled={loading}
onClick={() =>
deleteFile(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
<Tooltip
title={tDashboard("file.unlink")}
>
<IconButton
disabled={loading}
onClick={() =>
deleteFile(row.ID, true)
}
size={"small"}
>
<LinkOff />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,481 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Alert from "@material-ui/lab/Alert";
import Fade from "@material-ui/core/Fade";
import Paper from "@material-ui/core/Paper";
import Popper from "@material-ui/core/Popper";
import InputAdornment from "@material-ui/core/InputAdornment";
import Chip from "@material-ui/core/Chip";
import { Dialog } from "@material-ui/core";
import DialogTitle from "@material-ui/core/DialogTitle";
import PathSelector from "../../FileManager/PathSelector";
import DialogActions from "@material-ui/core/DialogActions";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
userSelect: {
width: 400,
borderRadius: 0,
},
}));
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value]);
return debouncedValue;
}
export default function Import() {
const { t } = useTranslation("dashboard", { keyPrefix: "file" });
const { t: tCommon } = useTranslation("common");
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
policy: 1,
userInput: "",
src: "",
dst: "",
recursive: true,
});
const [anchorEl, setAnchorEl] = useState(null);
const [policies, setPolicies] = useState({});
const [users, setUsers] = useState([]);
const [user, setUser] = useState(null);
const [selectRemote, setSelectRemote] = useState(false);
const [selectLocal, setSelectLocal] = useState(false);
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const handleCheckChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.checked,
});
};
const history = useHistory();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const submit = (e) => {
e.preventDefault();
if (user === null) {
ToggleSnackbar("top", "right", t("selectTargetUser"), "warning");
return;
}
setLoading(true);
API.post("/admin/task/import", {
uid: user.ID,
policy_id: parseInt(options.policy),
src: options.src,
dst: options.dst,
recursive: options.recursive,
})
.then(() => {
setLoading(false);
history.push("/admin/file");
ToggleSnackbar(
"top",
"right",
t("importTaskCreated"),
"success"
);
})
.catch((error) => {
setLoading(false);
ToggleSnackbar("top", "right", error.message, "error");
});
};
const debouncedSearchTerm = useDebounce(options.userInput, 500);
useEffect(() => {
if (debouncedSearchTerm !== "") {
API.post("/admin/user/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
searches: {
nick: debouncedSearchTerm,
email: debouncedSearchTerm,
},
})
.then((response) => {
setUsers(response.data.items);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}
}, [debouncedSearchTerm]);
useEffect(() => {
API.post("/admin/policy/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
conditions: {},
})
.then((response) => {
const res = {};
response.data.items.forEach((v) => {
res[v.ID] = v;
});
setPolicies(res);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const selectUser = (u) => {
setOptions({
...options,
userInput: "",
});
setUser(u);
};
const setMoveTarget = (setter) => (folder) => {
const path =
folder.path === "/"
? folder.path + folder.name
: folder.path + "/" + folder.name;
setter(path === "//" ? "/" : path);
};
const openPathSelector = (isSrcSelect) => {
if (isSrcSelect) {
if (
!policies[options.policy] ||
policies[options.policy].Type === "local" ||
policies[options.policy].Type === "remote"
) {
ToggleSnackbar(
"top",
"right",
t("manuallyPathOnly"),
"warning"
);
return;
}
setSelectRemote(true);
} else {
if (user === null) {
ToggleSnackbar(
"top",
"right",
t("selectTargetUser"),
"warning"
);
return;
}
setSelectLocal(true);
}
};
return (
<div>
<Dialog
open={selectRemote}
onClose={() => setSelectRemote(false)}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">
{t("selectFolder")}
</DialogTitle>
<PathSelector
presentPath="/"
api={"/admin/file/folders/policy/" + options.policy}
selected={[]}
onSelect={setMoveTarget((p) =>
setOptions({
...options,
src: p,
})
)}
/>
<DialogActions>
<Button
onClick={() => setSelectRemote(false)}
color="primary"
>
{tCommon("ok")}
</Button>
</DialogActions>
</Dialog>
<Dialog
open={selectLocal}
onClose={() => setSelectLocal(false)}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">
{t("selectFolder")}
</DialogTitle>
<PathSelector
presentPath="/"
api={
"/admin/file/folders/user/" +
(user === null ? 0 : user.ID)
}
selected={[]}
onSelect={setMoveTarget((p) =>
setOptions({
...options,
dst: p,
})
)}
/>
<DialogActions>
<Button
onClick={() => setSelectLocal(false)}
color="primary"
>
{tCommon("ok")}
</Button>
</DialogActions>
</Dialog>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("importExternalFolder")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Alert severity="info">
{t("importExternalFolderDes")}
</Alert>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("storagePolicy")}
</InputLabel>
<Select
labelId="demo-mutiple-chip-label"
id="demo-mutiple-chip"
value={options.policy}
onChange={handleChange("policy")}
input={<Input id="select-multiple-chip" />}
>
{Object.keys(policies).map((pid) => (
<MenuItem key={pid} value={pid}>
{policies[pid].Name}
</MenuItem>
))}
</Select>
<FormHelperText id="component-helper-text">
{t("storagePolicyDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("targetUser")}
</InputLabel>
<Input
value={options.userInput}
onChange={(e) => {
handleChange("userInput")(e);
setAnchorEl(e.currentTarget);
}}
startAdornment={
user !== null && (
<InputAdornment position="start">
<Chip
size="small"
onDelete={() => {
setUser(null);
}}
label={user.Nick}
/>
</InputAdornment>
)
}
disabled={user !== null}
/>
<Popper
open={
options.userInput !== "" &&
users.length > 0
}
anchorEl={anchorEl}
placement={"bottom"}
transition
>
{({ TransitionProps }) => (
<Fade
{...TransitionProps}
timeout={350}
>
<Paper
className={classes.userSelect}
>
{users.map((u) => (
<MenuItem
key={u.ID}
onClick={() =>
selectUser(u)
}
>
{u.Nick}{" "}
{"<" + u.Email + ">"}
</MenuItem>
))}
</Paper>
</Fade>
)}
</Popper>
<FormHelperText id="component-helper-text">
{t("targetUserDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("srcFolderPath")}
</InputLabel>
<Input
value={options.src}
onChange={(e) => {
handleChange("src")(e);
setAnchorEl(e.currentTarget);
}}
required
endAdornment={
<Button
onClick={() =>
openPathSelector(true)
}
>
{t("select")}
</Button>
}
/>
<FormHelperText id="component-helper-text">
{t("selectSrcDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("dstFolderPath")}
</InputLabel>
<Input
value={options.dst}
onChange={(e) => {
handleChange("dst")(e);
setAnchorEl(e.currentTarget);
}}
required
endAdornment={
<Button
onClick={() =>
openPathSelector(false)
}
>
{t("select")}
</Button>
}
/>
<FormHelperText id="component-helper-text">
{t("dstFolderPathDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={options.recursive}
onChange={handleCheckChange(
"recursive"
)}
/>
}
label={t("recursivelyImport")}
/>
<FormHelperText id="component-helper-text">
{t("recursivelyImportDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("createImportTask")}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,108 @@
import React, { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import GroupForm from "./GroupForm";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
export default function EditGroupPreload() {
const { t } = useTranslation("dashboard", { keyPrefix: "group" });
const [group, setGroup] = useState({});
const { id } = useParams();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
setGroup({});
API.get("/admin/group/" + id)
.then((response) => {
// 布尔值转换
["ShareEnabled", "WebDAVEnabled"].forEach((v) => {
response.data[v] = response.data[v] ? "true" : "false";
});
[
"archive_download",
"archive_task",
"relocate",
"one_time_download",
"share_download",
"webdav_proxy",
"share_free",
"aria2",
"redirected_source",
"advance_delete",
"select_node",
].forEach((v) => {
if (response.data.OptionsSerialized[v] !== undefined) {
response.data.OptionsSerialized[v] = response.data
.OptionsSerialized[v]
? "true"
: "false";
}
});
// 整型转换
["MaxStorage", "SpeedLimit"].forEach((v) => {
response.data[v] = response.data[v].toString();
});
[
"compress_size",
"decompress_size",
"source_batch",
"aria2_batch",
].forEach((v) => {
if (response.data.OptionsSerialized[v] !== undefined) {
response.data.OptionsSerialized[v] =
response.data.OptionsSerialized[v].toString();
}
});
response.data.PolicyList = response.data.PolicyList.map((v) => {
return v.toString();
});
response.data.OptionsSerialized.available_nodes = response.data
.OptionsSerialized.available_nodes
? response.data.OptionsSerialized.available_nodes.map(
(v) => {
return v.toString();
}
)
: [];
// JSON转换
if (
response.data.OptionsSerialized.aria2_options === undefined
) {
response.data.OptionsSerialized.aria2_options = "{}";
} else {
try {
response.data.OptionsSerialized.aria2_options =
JSON.stringify(
response.data.OptionsSerialized.aria2_options
);
} catch (e) {
ToggleSnackbar(
"top",
"right",
t("aria2FormatError"),
"warning"
);
return;
}
}
setGroup(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, [id]);
return <div>{group.ID !== undefined && <GroupForm group={group} />}</div>;
}

View File

@@ -0,0 +1,252 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import { sizeToString } from "../../../utils";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import { useHistory, useLocation } from "react-router";
import IconButton from "@material-ui/core/IconButton";
import { Delete, Edit } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
}));
const columns = [
{ id: "#", minWidth: 50 },
{ id: "name", minWidth: 170 },
{ id: "type", label: "存储策略", minWidth: 170 },
{
id: "count",
minWidth: 50,
align: "right",
},
{
id: "size",
minWidth: 100,
align: "right",
},
{
id: "action",
minWidth: 170,
align: "right",
},
];
function useQuery() {
return new URLSearchParams(useLocation().search);
}
export default function Group() {
const { t } = useTranslation("dashboard", { keyPrefix: "group" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [groups, setGroups] = useState([]);
const [statics, setStatics] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [policies, setPolicies] = React.useState({});
const location = useLocation();
const history = useHistory();
const query = useQuery();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/group/list", {
page: page,
page_size: pageSize,
order_by: "id desc",
})
.then((response) => {
setGroups(response.data.items);
setStatics(response.data.statics);
setTotal(response.data.total);
setPolicies(response.data.policies);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
if (query.get("code") === "0") {
ToggleSnackbar("top", "right", "授权成功", "success");
} else if (query.get("msg") && query.get("msg") !== "") {
ToggleSnackbar(
"top",
"right",
query.get("msg") + ", " + query.get("err"),
"warning"
);
}
}, [location]);
useEffect(() => {
loadList();
}, [page, pageSize]);
const deletePolicy = (id) => {
API.delete("/admin/group/" + id)
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("deleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
return (
<div>
<div className={classes.header}>
<Button
color={"primary"}
onClick={() => history.push("/admin/group/add")}
variant={"contained"}
>
{t("new")}
</Button>
<div className={classes.headerRight}>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
{columns.map((column) => (
<TableCell
key={column.id}
align={column.align}
style={{ minWidth: column.minWidth }}
>
{t(column.id)}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{groups.map((row) => (
<TableRow hover key={row.ID}>
<TableCell>{row.ID}</TableCell>
<TableCell>{row.Name}</TableCell>
<TableCell>
{row.PolicyList !== null &&
row.PolicyList.map((pid, key) => {
let res = "";
if (policies[pid]) {
res += policies[pid].Name;
}
if (
key !==
row.PolicyList.length - 1
) {
res += " / ";
}
return res;
})}
</TableCell>
<TableCell align={"right"}>
{statics[row.ID] !== undefined &&
statics[row.ID].toLocaleString()}
</TableCell>
<TableCell align={"right"}>
{statics[row.ID] !== undefined &&
sizeToString(row.MaxStorage)}
</TableCell>
<TableCell align={"right"}>
<Tooltip
title={tDashboard("policy.edit")}
>
<IconButton
onClick={() =>
history.push(
"/admin/group/edit/" +
row.ID
)
}
size={"small"}
>
<Edit />
</IconButton>
</Tooltip>
<Tooltip
title={tDashboard("policy.delete")}
>
<IconButton
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,825 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles, useTheme } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Chip from "@material-ui/core/Chip";
import SizeInput from "../Common/SizeInput";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
import Collapse from "@material-ui/core/Collapse";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
import { Link } from "@material-ui/core";
import { getSelectItemStyles } from "../../../utils";
import NodeSelector from "./NodeSelector";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function GroupForm(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "group" });
const { t: tVas } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [group, setGroup] = useState(
props.group
? props.group
: {
ID: 0,
Name: "",
MaxStorage: "1073741824", // 转换类型
ShareEnabled: "true", // 转换类型
WebDAVEnabled: "true", // 转换类型
SpeedLimit: "0", // 转换类型
PolicyList: ["1"], // 转换类型,至少选择一个
OptionsSerialized: {
// 批量转换类型
share_download: "true",
aria2_options: "{}", // json decode
compress_size: "0",
decompress_size: "0",
source_batch: "0",
aria2_batch: "1",
available_nodes: [],
},
}
);
const [policies, setPolicies] = useState({});
const theme = useTheme();
const history = useHistory();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/policy/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
conditions: {},
})
.then((response) => {
const res = {};
response.data.items.forEach((v) => {
res[v.ID] = v.Name;
});
setPolicies(res);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
const handleChange = (name) => (event) => {
setGroup({
...group,
[name]: event.target.value,
});
};
const handleCheckChange = (name) => (event) => {
const value = event.target.checked ? "true" : "false";
setGroup({
...group,
[name]: value,
});
};
const handleOptionCheckChange = (name) => (event) => {
const value = event.target.checked ? "true" : "false";
setGroup({
...group,
OptionsSerialized: {
...group.OptionsSerialized,
[name]: value,
},
});
};
const handleOptionChange = (name) => (event) => {
setGroup({
...group,
OptionsSerialized: {
...group.OptionsSerialized,
[name]: event.target.value,
},
});
};
const submit = (e) => {
e.preventDefault();
const groupCopy = {
...group,
OptionsSerialized: { ...group.OptionsSerialized },
};
// 布尔值转换
["ShareEnabled", "WebDAVEnabled"].forEach((v) => {
groupCopy[v] = groupCopy[v] === "true";
});
[
"archive_download",
"archive_task",
"relocate",
"one_time_download",
"share_download",
"webdav_proxy",
"share_free",
"aria2",
"redirected_source",
"advance_delete",
"select_node",
].forEach((v) => {
if (groupCopy.OptionsSerialized[v] !== undefined) {
groupCopy.OptionsSerialized[v] =
groupCopy.OptionsSerialized[v] === "true";
}
});
// 整型转换
["MaxStorage", "SpeedLimit"].forEach((v) => {
groupCopy[v] = parseInt(groupCopy[v]);
});
[
"compress_size",
"decompress_size",
"source_batch",
"aria2_batch",
].forEach((v) => {
if (groupCopy.OptionsSerialized[v] !== undefined) {
groupCopy.OptionsSerialized[v] = parseInt(
groupCopy.OptionsSerialized[v]
);
}
});
groupCopy.PolicyList = groupCopy.PolicyList.map((v) => {
return parseInt(v);
});
groupCopy.OptionsSerialized.available_nodes =
groupCopy.OptionsSerialized.available_nodes.map((v) => {
return parseInt(v);
});
if (groupCopy.PolicyList.length < 1 && groupCopy.ID !== 3) {
ToggleSnackbar("top", "right", t("atLeastOnePolicy"), "warning");
return;
}
// JSON转换
try {
groupCopy.OptionsSerialized.aria2_options = JSON.parse(
groupCopy.OptionsSerialized.aria2_options
);
} catch (e) {
ToggleSnackbar("top", "right", t("aria2FormatError"), "warning");
return;
}
setLoading(true);
API.post("/admin/group", {
group: groupCopy,
})
.then(() => {
history.push("/admin/group");
ToggleSnackbar(
"top",
"right",
props.group ? t("saved") : t("added"),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{group.ID === 0 && t("new")}
{group.ID !== 0 &&
t("editGroup", { group: group.Name })}
</Typography>
<div className={classes.formContainer}>
{group.ID !== 3 && (
<>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("nameOfGroup")}
</InputLabel>
<Input
value={group.Name}
onChange={handleChange("Name")}
required
/>
<FormHelperText id="component-helper-text">
{t("nameOfGroupDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("availablePolicies")}
</InputLabel>
<Select
labelId="demo-mutiple-chip-label"
id="demo-mutiple-chip"
multiple
value={group.PolicyList}
onChange={handleChange(
"PolicyList"
)}
input={
<Input id="select-multiple-chip" />
}
renderValue={(selected) => (
<div>
{selected.map((value) => (
<Chip
style={{
margin: 2,
}}
key={value}
size={"small"}
label={
policies[value]
}
className={
classes.chip
}
/>
))}
</div>
)}
>
{Object.keys(policies).map(
(pid) => (
<MenuItem
key={pid}
value={pid}
style={getSelectItemStyles(
pid,
group.PolicyList,
theme
)}
>
{policies[pid]}
</MenuItem>
)
)}
</Select>
<FormHelperText id="component-helper-text">
{t("availablePoliciesDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<SizeInput
value={group.MaxStorage}
onChange={handleChange(
"MaxStorage"
)}
min={0}
max={9223372036854775807}
label={t("initialStorageQuota")}
required
/>
</FormControl>
<FormHelperText id="component-helper-text">
{t("initialStorageQuotaDes")}
</FormHelperText>
</div>
</>
)}
<div className={classes.form}>
<FormControl fullWidth>
<SizeInput
value={group.SpeedLimit}
onChange={handleChange("SpeedLimit")}
min={0}
max={9223372036854775807}
label={t("downloadSpeedLimit")}
suffix={"/s"}
required
/>
</FormControl>
<FormHelperText id="component-helper-text">
{t("downloadSpeedLimitDes")}
</FormHelperText>
</div>
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("bathSourceLinkLimit")}
</InputLabel>
<Input
multiline
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={
group.OptionsSerialized.source_batch
}
onChange={handleOptionChange(
"source_batch"
)}
/>
<FormHelperText id="component-helper-text">
{t("bathSourceLinkLimitDes")}
</FormHelperText>
</FormControl>
</div>
)}
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.ShareEnabled ===
"true"
}
onChange={handleCheckChange(
"ShareEnabled"
)}
/>
}
label={t("allowCreateShareLink")}
/>
<FormHelperText id="component-helper-text">
{t("allowCreateShareLinkDes")}
</FormHelperText>
</FormControl>
</div>
)}
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.share_download === "true"
}
onChange={handleOptionCheckChange(
"share_download"
)}
/>
}
label={t("allowDownloadShare")}
/>
<FormHelperText id="component-helper-text">
{t("allowDownloadShareDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.share_free === "true"
}
onChange={handleOptionCheckChange(
"share_free"
)}
/>
}
label={tVas("freeDownload")}
/>
<FormHelperText id="component-helper-text">
{tVas("freeDownloadDes")}
</FormHelperText>
</FormControl>
</div>
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.WebDAVEnabled ===
"true"
}
onChange={handleCheckChange(
"WebDAVEnabled"
)}
/>
}
label={t("allowWabDAV")}
/>
<FormHelperText id="component-helper-text">
{t("allowWabDAVDes")}
</FormHelperText>
</FormControl>
</div>
)}
{group.ID !== 3 && group.WebDAVEnabled === "true" && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized.webdav_proxy ===
"true"
}
onChange={handleOptionCheckChange(
"webdav_proxy"
)}
/>
}
label={t("allowWabDAVProxy")}
/>
<FormHelperText id="component-helper-text">
{t("allowWabDAVProxyDes")}
</FormHelperText>
</FormControl>
</div>
)}
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.one_time_download ===
"true"
}
onChange={handleOptionCheckChange(
"one_time_download"
)}
/>
}
label={t("disableMultipleDownload")}
/>
<FormHelperText id="component-helper-text">
{t("disableMultipleDownloadDes")}
</FormHelperText>
</FormControl>
</div>
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.aria2 === "true"
}
onChange={handleOptionCheckChange(
"aria2"
)}
/>
}
label={t("allowRemoteDownload")}
/>
<FormHelperText id="component-helper-text">
{t("allowRemoteDownloadDes")}
</FormHelperText>
</FormControl>
</div>
)}
<Collapse in={group.OptionsSerialized.aria2 === "true"}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("aria2Options")}
</InputLabel>
<Input
multiline
value={
group.OptionsSerialized
.aria2_options
}
onChange={handleOptionChange(
"aria2_options"
)}
/>
<FormHelperText id="component-helper-text">
{t("aria2OptionsDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("aria2BatchSize")}
</InputLabel>
<Input
multiline
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={
group.OptionsSerialized.aria2_batch
}
onChange={handleOptionChange(
"aria2_batch"
)}
/>
<FormHelperText id="component-helper-text">
{t("aria2BatchSizeDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("availableNodes")}
</InputLabel>
<NodeSelector
selected={
group.OptionsSerialized
.available_nodes
}
handleChange={handleOptionChange(
"available_nodes"
)}
/>
<FormHelperText id="component-helper-text">
{t("availableNodesDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.select_node === "true"
}
onChange={handleOptionCheckChange(
"select_node"
)}
/>
}
label={t("allowSelectNode")}
/>
<FormHelperText id="component-helper-text">
{t("allowSelectNodeDes")}
</FormHelperText>
</FormControl>
</div>
</Collapse>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.archive_download === "true"
}
onChange={handleOptionCheckChange(
"archive_download"
)}
/>
}
label={t("serverSideBatchDownload")}
/>
<FormHelperText id="component-helper-text">
{t("serverSideBatchDownloadDes")}
</FormHelperText>
</FormControl>
</div>
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.archive_task === "true"
}
onChange={handleOptionCheckChange(
"archive_task"
)}
/>
}
label={t("compressTask")}
/>
<FormHelperText id="component-helper-text">
{t("compressTaskDes")}
</FormHelperText>
</FormControl>
</div>
)}
<Collapse
in={group.OptionsSerialized.archive_task === "true"}
>
<div className={classes.form}>
<FormControl fullWidth>
<SizeInput
value={
group.OptionsSerialized
.compress_size
}
onChange={handleOptionChange(
"compress_size"
)}
min={0}
max={9223372036854775807}
label={t("compressSize")}
/>
</FormControl>
<FormHelperText id="component-helper-text">
{t("compressSizeDes")}
</FormHelperText>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<SizeInput
value={
group.OptionsSerialized
.decompress_size
}
onChange={handleOptionChange(
"decompress_size"
)}
min={0}
max={9223372036854775807}
label={t("decompressSize")}
/>
</FormControl>
<FormHelperText id="component-helper-text">
{t("decompressSizeDes")}
</FormHelperText>
</div>
</Collapse>
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.relocate === "true"
}
onChange={handleOptionCheckChange(
"relocate"
)}
/>
}
label={t("migratePolicy")}
/>
<FormHelperText id="component-helper-text">
{t("migratePolicyDes")}
</FormHelperText>
</FormControl>
</div>
)}
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.redirected_source ===
"true"
}
onChange={handleOptionCheckChange(
"redirected_source"
)}
/>
}
label={t("redirectedSource")}
/>
<FormHelperText id="component-helper-text">
<Trans
ns={"dashboard"}
i18nKey={
"group.redirectedSourceDes"
}
components={[
<Link
href={tDashboard(
"policy.comparesStoragePoliciesLink"
)}
key={0}
target={"_blank"}
/>,
]}
/>
</FormHelperText>
</FormControl>
</div>
)}
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.advance_delete ===
"true"
}
onChange={handleOptionCheckChange(
"advance_delete"
)}
/>
}
label={t("advanceDelete")}
/>
<FormHelperText id="component-helper-text">
{t("advanceDeleteDes")}
</FormHelperText>
</FormControl>
</div>
)}
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{tDashboard("settings.save")}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,78 @@
import React, { useCallback, useEffect, useState } from "react";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
import Input from "@material-ui/core/Input";
import Chip from "@material-ui/core/Chip";
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
import { getSelectItemStyles } from "../../../utils";
import { useTheme } from "@material-ui/core/styles";
export default function NodeSelector({ selected, handleChange }) {
const { t } = useTranslation("dashboard", { keyPrefix: "group" });
const [nodes, setNodes] = useState({});
const theme = useTheme();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/node/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
conditions: {},
})
.then((response) => {
const res = {};
response.data.items.forEach((v) => {
res[v.ID] = v.Name;
});
setNodes(res);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
return (
<Select
labelId="demo-mutiple-chip-label"
id="demo-mutiple-chip"
multiple
value={selected}
onChange={handleChange}
input={<Input id="select-multiple-chip" />}
renderValue={(selected) => (
<div>
{selected.map((value) => (
<Chip
style={{
margin: 2,
}}
key={value}
size={"small"}
label={nodes[value]}
/>
))}
</div>
)}
>
{Object.keys(nodes).map((pid) => (
<MenuItem
key={pid}
value={pid}
style={getSelectItemStyles(pid, selected, theme)}
>
{nodes[pid]}
</MenuItem>
))}
</Select>
);
}

View File

@@ -0,0 +1,527 @@
import React, { useCallback, useEffect, useState } from "react";
import Grid from "@material-ui/core/Grid";
import Paper from "@material-ui/core/Paper";
import {
CartesianGrid,
Legend,
Line,
LineChart,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { ResponsiveContainer } from "recharts/lib/component/ResponsiveContainer";
import { makeStyles } from "@material-ui/core/styles";
import pathHelper from "../../utils/page";
import API from "../../middleware/Api";
import { useDispatch } from "react-redux";
import Typography from "@material-ui/core/Typography";
import Divider from "@material-ui/core/Divider";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemAvatar from "@material-ui/core/ListItemAvatar";
import Avatar from "@material-ui/core/Avatar";
import {
Description,
Favorite,
FileCopy,
Forum,
GitHub,
Home,
Launch,
Lock,
People,
Public,
Telegram,
} from "@material-ui/icons";
import ListItemText from "@material-ui/core/ListItemText";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import { blue, green, red, yellow } from "@material-ui/core/colors";
import axios from "axios";
import TimeAgo from "timeago-react";
import Chip from "@material-ui/core/Chip";
import DialogTitle from "@material-ui/core/DialogTitle";
import Dialog from "@material-ui/core/Dialog";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import { toggleSnackbar } from "../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
paper: {
padding: theme.spacing(3),
height: "100%",
},
logo: {
width: 70,
},
logoContainer: {
padding: theme.spacing(3),
display: "flex",
},
title: {
marginLeft: 16,
},
cloudreve: {
fontSize: 25,
color: theme.palette.text.secondary,
},
version: {
color: theme.palette.text.hint,
},
links: {
padding: theme.spacing(3),
},
iconRight: {
minWidth: 0,
},
userIcon: {
backgroundColor: blue[100],
color: blue[600],
},
fileIcon: {
backgroundColor: yellow[100],
color: yellow[800],
},
publicIcon: {
backgroundColor: green[100],
color: green[800],
},
secretIcon: {
backgroundColor: red[100],
color: red[800],
},
}));
export default function Index() {
const { t } = useTranslation("dashboard");
const classes = useStyles();
const [lineData, setLineData] = useState([]);
// const [news, setNews] = useState([]);
// const [newsUsers, setNewsUsers] = useState({});
const [open, setOpen] = React.useState(false);
const [siteURL, setSiteURL] = React.useState("");
const [statistics, setStatistics] = useState({
fileTotal: 0,
userTotal: 0,
publicShareTotal: 0,
secretShareTotal: 0,
});
const [version, setVersion] = useState({
backend: "-",
});
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const ResetSiteURL = () => {
setOpen(false);
API.patch("/admin/setting", {
options: [
{
key: "siteURL",
value: window.location.origin,
},
],
})
.then(() => {
setSiteURL(window.location.origin);
ToggleSnackbar("top", "right", t("settings.saved"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
API.get("/admin/summary")
.then((response) => {
const data = [];
response.data.date.forEach((v, k) => {
data.push({
name: v,
file: response.data.files[k],
user: response.data.users[k],
share: response.data.shares[k],
});
});
setLineData(data);
setStatistics({
fileTotal: response.data.fileTotal,
userTotal: response.data.userTotal,
publicShareTotal: response.data.publicShareTotal,
secretShareTotal: response.data.secretShareTotal,
});
setVersion(response.data.version);
setSiteURL(response.data.siteURL);
if (
response.data.siteURL === "" ||
response.data.siteURL !== window.location.origin
) {
setOpen(true);
}
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// axios
// .get("/api/v3/admin/news?tag=" + t("summary.newsTag"))
// .then((response) => {
// setNews(response.data.data);
// const res = {};
// response.data.included.forEach((v) => {
// if (v.type === "users") {
// res[v.id] = v.attributes;
// }
// });
// setNewsUsers(res);
// })
// .catch((error) => {
// ToggleSnackbar(
// "top",
// "right",
// t("summary.newsletterError"),
// "warning"
// );
// });
}, []);
return (
<Grid container spacing={3}>
<Dialog
open={open}
onClose={() => setOpen(false)}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{t("summary.confirmSiteURLTitle")}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<Typography>
{siteURL === "" &&
t("summary.siteURLNotSet", {
current: window.location.origin,
})}
{siteURL !== "" &&
t("summary.siteURLNotMatch", {
current: window.location.origin,
})}
</Typography>
<Typography>
{t("summary.siteURLDescription")}
</Typography>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)} color="default">
{t("summary.ignore")}
</Button>
<Button onClick={() => ResetSiteURL()} color="primary">
{t("summary.changeIt")}
</Button>
</DialogActions>
</Dialog>
<Grid alignContent={"stretch"} item xs={12} md={8} lg={9}>
<Paper className={classes.paper}>
<Typography variant="button" display="block" gutterBottom>
{t("summary.trend")}
</Typography>
<ResponsiveContainer
width="100%"
aspect={pathHelper.isMobile() ? 4.0 / 3.0 : 3.0 / 1.0}
>
<LineChart width={1200} height={300} data={lineData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Line
name={t("nav.files")}
type="monotone"
dataKey="file"
stroke="#3f51b5"
/>
<Line
name={t("nav.users")}
type="monotone"
dataKey="user"
stroke="#82ca9d"
/>
<Line
name={t("nav.shares")}
type="monotone"
dataKey="share"
stroke="#e91e63"
/>
</LineChart>
</ResponsiveContainer>
</Paper>
</Grid>
<Grid item xs={12} md={4} lg={3}>
<Paper className={classes.paper}>
<Typography variant="button" display="block" gutterBottom>
{t("summary.summary")}
</Typography>
<Divider />
<List className={classes.root}>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.userIcon}>
<People />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={statistics.userTotal}
secondary={t("summary.totalUsers")}
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.fileIcon}>
<FileCopy />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={statistics.fileTotal}
secondary={t("summary.totalFiles")}
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.publicIcon}>
<Public />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={statistics.publicShareTotal}
secondary={t("summary.publicShares")}
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.secretIcon}>
<Lock />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={statistics.secretShareTotal}
secondary={t("summary.privateShares")}
/>
</ListItem>
</List>
</Paper>
</Grid>
<Grid item xs={12} md={4} lg={3}>
<Paper>
<div className={classes.logoContainer}>
<img
alt="cloudreve"
className={classes.logo}
src={"/static/img/cloudreve.svg"}
/>
<div className={classes.title}>
<Typography className={classes.cloudreve}>
Cloudreve
</Typography>
<Typography className={classes.version}>
{version.backend}{" "}
{version.is_plus === "true" && (
<Chip size="small" label="Plus" />
)}
</Typography>
</div>
</div>
<Divider />
<div>
<List component="nav" aria-label="main mailbox folders">
<ListItem
button
onClick={() =>
window.open("https://cloudreve.org")
}
>
<ListItemIcon>
<Home />
</ListItemIcon>
<ListItemText primary={t("summary.homepage")} />
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
<ListItem
button
onClick={() =>
window.open(
"https://github.com/cloudreve/cloudreve"
)
}
>
<ListItemIcon>
<GitHub />
</ListItemIcon>
<ListItemText primary={t("summary.github")} />
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
<ListItem
button
onClick={() =>
window.open("https://docs.cloudreve.org/")
}
>
<ListItemIcon>
<Description />
</ListItemIcon>
<ListItemText
primary={t("summary.documents")}
/>
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
<ListItem
button
onClick={() =>
window.open(t("summary.forumLink"))
}
>
<ListItemIcon>
<Forum />
</ListItemIcon>
<ListItemText primary={t("summary.forum")} />
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
<ListItem
button
onClick={() =>
window.open(t("summary.telegramGroupLink"))
}
>
<ListItemIcon>
<Telegram />
</ListItemIcon>
<ListItemText
primary={t("summary.telegramGroup")}
/>
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
<ListItem
button
onClick={() =>
window.open("https://docs.cloudreve.org/use/pro/jie-shao")
}
>
<ListItemIcon style={{ color: "#ff789d" }}>
<Favorite />
</ListItemIcon>
<ListItemText primary={t("summary.buyPro")} />
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
</List>
</div>
</Paper>
</Grid>
{/* <Grid item xs={12} md={8} lg={9}>
<Paper className={classes.paper}>
<List>
{news &&
news.map((v) => (
<>
<ListItem
button
alignItems="flex-start"
onClick={() =>
window.open(
"https://forum.cloudreve.org/d/" +
v.id
)
}
>
<ListItemAvatar>
<Avatar
alt="Travis Howard"
src={
newsUsers[
v.relationships
.startUser.data.id
] &&
newsUsers[
v.relationships
.startUser.data.id
].avatarUrl
}
/>
</ListItemAvatar>
<ListItemText
primary={v.attributes.title}
secondary={
<React.Fragment>
<Typography
component="span"
variant="body2"
className={
classes.inline
}
color="textPrimary"
>
{newsUsers[
v.relationships
.startUser.data
.id
] &&
newsUsers[
v.relationships
.startUser
.data.id
].username}{" "}
</Typography>
<Trans
ns={"dashboard"}
i18nKey="summary.publishedAt"
components={[
<TimeAgo
key={0}
datetime={
v.attributes
.startTime
}
locale={t(
"timeAgoLocaleCode",
{
ns:
"common",
}
)}
/>,
]}
/>
</React.Fragment>
}
/>
</ListItem>
<Divider />
</>
))}
</List>
</Paper>
</Grid> */}
</Grid>
);
}

View File

@@ -0,0 +1,27 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import NodeGuide from "./Guide/NodeGuide";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
}));
export default function AddNode() {
const classes = useStyles();
return (
<div>
<Paper square className={classes.content}>
<NodeGuide />
</Paper>
</div>
);
}

View File

@@ -0,0 +1,57 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import NodeGuide from "./Guide/NodeGuide";
import { useParams } from "react-router";
import { useDispatch } from "react-redux";
import API from "../../../middleware/Api";
import { toggleSnackbar } from "../../../redux/explorer";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
}));
export default function EditNode() {
const classes = useStyles();
const { id } = useParams();
const [node, setNode] = useState(null);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.get("/admin/node/" + id)
.then((response) => {
response.data.Rank = response.data.Rank.toString();
response.data.Aria2OptionsSerialized.interval = response.data.Aria2OptionsSerialized.interval.toString();
response.data.Aria2OptionsSerialized.timeout = response.data.Aria2OptionsSerialized.timeout.toString();
response.data.Aria2Enabled = response.data.Aria2Enabled
? "true"
: "false";
setNode(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, [id]);
return (
<div>
<Paper square className={classes.content}>
{node && <NodeGuide node={node} />}
</Paper>
</div>
);
}

View File

@@ -0,0 +1,454 @@
import { lighten, makeStyles } from "@material-ui/core/styles";
import React, { useCallback, useState } from "react";
import Typography from "@material-ui/core/Typography";
import { useDispatch } from "react-redux";
import Link from "@material-ui/core/Link";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import RadioGroup from "@material-ui/core/RadioGroup";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Radio from "@material-ui/core/Radio";
import Collapse from "@material-ui/core/Collapse";
import Button from "@material-ui/core/Button";
import Alert from "@material-ui/lab/Alert";
import Box from "@material-ui/core/Box";
import FormHelperText from "@material-ui/core/FormHelperText";
import API from "../../../../middleware/Api";
import { toggleSnackbar } from "../../../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
viewButtonLabel: { textTransform: "none" },
"@global": {
code: {
color: "rgba(0, 0, 0, 0.87)",
display: "inline-block",
padding: "2px 6px",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
borderRadius: "2px",
backgroundColor: "rgba(255,229,100,0.1)",
},
pre: {
margin: "24px 0",
padding: "12px 18px",
overflow: "auto",
direction: "ltr",
borderRadius: "4px",
backgroundColor: "#272c34",
color: "#fff",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
},
},
}));
export default function Aria2RPC(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "node" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const testAria2 = () => {
setLoading(true);
API.post("/admin/node/aria2/test", {
type: props.node.Type,
server: props.node.Server,
secret: props.node.SlaveKey,
rpc: props.node.Aria2OptionsSerialized.server,
token: props.node.Aria2OptionsSerialized.token,
})
.then((response) => {
ToggleSnackbar(
"top",
"right",
t("ariaSuccess", { version: response.data }),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const mode = props.node.Type === 0 ? t("slave") : t("master");
return (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
props.onSubmit(e);
}}
>
<Alert severity="info" style={{ marginBottom: 10 }}>
<Typography variant="body2">
<Trans
ns={"dashboard"}
i18nKey={"node.aria2Des"}
values={{
mode: mode,
}}
components={[
<Link
href={"https://aria2.github.io/"}
target={"_blank"}
key={0}
/>,
<Box
component="span"
fontWeight="fontWeightBold"
key={1}
/>,
<Link
href={t("aria2DocURL")}
target={"_blank"}
key={2}
/>,
]}
/>
</Typography>
</Alert>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{props.node.Type === 0
? t("slaveTakeOverRemoteDownload")
: t("masterTakeOverRemoteDownload")}
<br />
{props.node.Type === 0
? t("routeTaskSlave")
: t("routeTaskMaster")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={props.node.Aria2Enabled}
onChange={props.handleTextChange(
"Aria2Enabled"
)}
row
>
<FormControlLabel
value={"true"}
control={<Radio color={"primary"} />}
label={t("enable")}
/>
<FormControlLabel
value={"false"}
control={<Radio color={"primary"} />}
label={t("disable")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={props.node.Aria2Enabled === "true"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("aria2ConfigDes", {
target:
props.node.Type === 0
? t("slaveNodeTarget")
: t("masterNodeTarget"),
})}
</Typography>
<pre>
# {t("enableRPCComment")}
<br />
enable-rpc=true
<br /># {t("rpcPortComment")}
<br />
rpc-listen-port=6800
<br /># {t("rpcSecretComment")}
<br />
rpc-secret=
{props.node.Aria2OptionsSerialized.token}
<br />
</pre>
<Alert severity="info" style={{ marginBottom: 10 }}>
<Typography variant="body2">
{t("rpcConfigDes")}
</Typography>
</Alert>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>3</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"node.rpcServerDes"}
values={{
mode: mode,
}}
components={[
<code key={0} />,
<code key={1} />,
<code key={2} />,
]}
/>
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("rpcServer")}
</InputLabel>
<Input
required
type={"url"}
value={
props.node.Aria2OptionsSerialized.server
}
onChange={props.handleOptionChange(
"server"
)}
/>
<FormHelperText id="component-helper-text">
{t("rpcServerHelpDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>4</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"node.rpcTokenDes"}
components={[<code key={0} />]}
/>
</Typography>
<div className={classes.form}>
<Input
value={props.node.Aria2OptionsSerialized.token}
onChange={props.handleOptionChange("token")}
/>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>5</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"node.aria2PathDes"}
components={[<strong key={0} />]}
/>
</Typography>
<div className={classes.form}>
<Input
value={
props.node.Aria2OptionsSerialized.temp_path
}
onChange={props.handleOptionChange("temp_path")}
/>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>5</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("aria2SettingDes")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("refreshInterval")}
</InputLabel>
<Input
type={"number"}
inputProps={{
step: 1,
min: 1,
}}
required
value={
props.node.Aria2OptionsSerialized
.interval
}
onChange={props.handleOptionChange(
"interval"
)}
/>
<FormHelperText id="component-helper-text">
{t("refreshIntervalDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("rpcTimeout")}
</InputLabel>
<Input
type={"number"}
inputProps={{
step: 1,
min: 1,
}}
required
value={
props.node.Aria2OptionsSerialized
.timeout
}
onChange={props.handleOptionChange(
"timeout"
)}
/>
<FormHelperText id="component-helper-text">
{t("rpcTimeoutDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("globalOptions")}
</InputLabel>
<Input
multiline
required
value={
props.node.Aria2OptionsSerialized
.options
}
onChange={props.handleOptionChange(
"options"
)}
/>
<FormHelperText id="component-helper-text">
{t("globalOptionsDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>6</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("testAria2Des", { mode })}
{props.node.Type === 0 &&
t("testAria2DesSlaveAddition")}
</Typography>
<div className={classes.form}>
<Button
disabled={loading}
variant={"outlined"}
onClick={() => testAria2()}
color={"primary"}
>
{t("testAria2")}
</Button>
</div>
</div>
</div>
</Collapse>
<div className={classes.stepFooter}>
{props.activeStep !== 0 && (
<Button
color={"default"}
className={classes.button}
onClick={props.onBack}
>
{tDashboard("policy.back")}
</Button>
)}
<Button
disabled={loading || props.loading}
type={"submit"}
variant={"contained"}
color={"primary"}
onClick={props.onSubmit}
>
{tDashboard("policy.next")}
</Button>
</div>
</form>
);
}

View File

@@ -0,0 +1,305 @@
import { lighten, makeStyles } from "@material-ui/core/styles";
import React, { useCallback, useState } from "react";
import Typography from "@material-ui/core/Typography";
import { useDispatch } from "react-redux";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import Button from "@material-ui/core/Button";
import API from "../../../../middleware/Api";
import Alert from "@material-ui/lab/Alert";
import Box from "@material-ui/core/Box";
import { toggleSnackbar } from "../../../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
viewButtonLabel: { textTransform: "none" },
"@global": {
code: {
color: "rgba(0, 0, 0, 0.87)",
display: "inline-block",
padding: "2px 6px",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
borderRadius: "2px",
backgroundColor: "rgba(255,229,100,0.1)",
},
pre: {
margin: "24px 0",
padding: "12px 18px",
overflow: "auto",
direction: "ltr",
borderRadius: "4px",
backgroundColor: "#272c34",
color: "#fff",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
},
},
}));
export default function Communication(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "node" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const testSlave = () => {
setLoading(true);
// 测试路径是否可用
API.post("/admin/policy/test/slave", {
server: props.node.Server,
secret: props.node.SlaveKey,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
tDashboard("policy.communicationOK"),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
props.onSubmit(e);
}}
>
<Alert severity="info" style={{ marginBottom: 10 }}>
<Trans
ns={"dashboard"}
i18nKey={"node.slaveNodeDes"}
components={[<Box key={0} fontWeight="fontWeightBold" />]}
/>
</Alert>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{tDashboard("policy.remoteCopyBinaryDescription")}
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{tDashboard("policy.remoteSecretDescription")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tDashboard("policy.remoteSecret")}
</InputLabel>
<Input
required
inputProps={{
minlength: 64,
}}
value={props.node.SlaveKey}
onChange={props.handleTextChange("SlaveKey")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>3</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{tDashboard("policy.modifyRemoteConfig")}
<br />
<Trans
ns={"dashboard"}
i18nKey={"policy.addRemoteConfigDes"}
components={[<code key={0} />]}
/>
</Typography>
<pre>
[System]
<br />
Mode = slave
<br />
Listen = :5212
<br />
<br />
[Slave]
<br />
Secret = {props.node.SlaveKey}
<br />
<br />
<Trans
ns={"dashboard"}
i18nKey={"node.overwriteDes"}
components={[<br key={0} />, <br key={1} />]}
/>
<br />
[OptionOverwrite]
<br />; {t("workerNumDes")}
<br />
max_worker_num = 50
<br />; {t("parallelTransferDes")}
<br />
max_parallel_transfer = 10
<br />; {t("chunkRetriesDes")}
<br />
chunk_retries = 10
</pre>
<Typography variant={"body2"}>
{tDashboard("policy.remoteConfigDifference")}
<ul>
<li>
<Trans
ns={"dashboard"}
i18nKey={"policy.remoteConfigDifference1"}
components={[
<code key={0} />,
<code key={1} />,
<code key={2} />,
]}
/>
</li>
<li>
<Trans
ns={"dashboard"}
i18nKey={"policy.remoteConfigDifference2"}
components={[
<code key={0} />,
<code key={1} />,
]}
/>
</li>
</ul>
{t("multipleMasterDes")}
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>4</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{tDashboard("policy.inputRemoteAddress")}
<br />
{tDashboard("policy.inputRemoteAddressDes")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tDashboard("policy.remoteAddress")}
</InputLabel>
<Input
fullWidth
required
type={"url"}
value={props.node.Server}
onChange={props.handleTextChange("Server")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>5</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{tDashboard("policy.testCommunicationDes")}
</Typography>
<div className={classes.form}>
<Button
disabled={loading}
variant={"outlined"}
onClick={() => testSlave()}
color={"primary"}
>
{tDashboard("policy.testCommunication")}
</Button>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
disabled={loading || props.loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{tDashboard("policy.next")}
</Button>
</div>
</form>
);
}

View File

@@ -0,0 +1,98 @@
import { lighten, makeStyles } from "@material-ui/core/styles";
import React from "react";
import Typography from "@material-ui/core/Typography";
import Button from "@material-ui/core/Button";
import { useHistory } from "react-router";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
viewButtonLabel: { textTransform: "none" },
"@global": {
code: {
color: "rgba(0, 0, 0, 0.87)",
display: "inline-block",
padding: "2px 6px",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
borderRadius: "2px",
backgroundColor: "rgba(255,229,100,0.1)",
},
pre: {
margin: "24px 0",
padding: "12px 18px",
overflow: "auto",
direction: "ltr",
borderRadius: "4px",
backgroundColor: "#272c34",
color: "#fff",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
},
},
}));
export default function Completed(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "node" });
const classes = useStyles();
const history = useHistory();
return (
<form className={classes.stepContent}>
<Typography>{t("nodeSaved")}</Typography>
<Typography variant={"body2"} color={"textSecondary"}>
{t("nodeSavedFutureAction")}
</Typography>
<div className={classes.stepFooter}>
<Button
color={"primary"}
className={classes.button}
onClick={() => history.push("/admin/node")}
>
{t("backToNodeList")}
</Button>
</div>
</form>
);
}

View File

@@ -0,0 +1,157 @@
import { lighten, makeStyles } from "@material-ui/core/styles";
import React, { useCallback, useState } from "react";
import Typography from "@material-ui/core/Typography";
import { useDispatch } from "react-redux";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import Button from "@material-ui/core/Button";
import { toggleSnackbar } from "../../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
viewButtonLabel: { textTransform: "none" },
"@global": {
code: {
color: "rgba(0, 0, 0, 0.87)",
display: "inline-block",
padding: "2px 6px",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
borderRadius: "2px",
backgroundColor: "rgba(255,229,100,0.1)",
},
pre: {
margin: "24px 0",
padding: "12px 18px",
overflow: "auto",
direction: "ltr",
borderRadius: "4px",
backgroundColor: "#272c34",
color: "#fff",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
},
},
}));
export default function Metainfo(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "node" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
return (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
props.onSubmit(e);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>{t("nameNode")}</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<Input
required
value={props.node.Name}
onChange={props.handleTextChange("Name")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("loadBalancerRankDes")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("loadBalancerRank")}
</InputLabel>
<Input
type={"number"}
required
inputProps={{
step: 1,
min: 0,
}}
value={props.node.Rank}
onChange={props.handleTextChange("Rank")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
disabled={loading || props.loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{tDashboard("policy.next")}
</Button>
</div>
</form>
);
}

View File

@@ -0,0 +1,191 @@
import React, { useCallback, useMemo, useState } from "react";
import Stepper from "@material-ui/core/Stepper";
import StepLabel from "@material-ui/core/StepLabel";
import Step from "@material-ui/core/Step";
import Typography from "@material-ui/core/Typography";
import { useDispatch } from "react-redux";
import { randomStr } from "../../../../utils";
import Communication from "./Communication";
import Aria2RPC from "./Aria2RPC";
import API from "../../../../middleware/Api";
import Metainfo from "./Metainfo";
import Completed from "./Completed";
import { toggleSnackbar } from "../../../../redux/explorer";
import { useTranslation } from "react-i18next";
const steps = [
{
slaveOnly: true,
title: "communication",
optional: false,
component: function show(p) {
return <Communication {...p} />;
},
},
{
slaveOnly: false,
title: "remoteDownload",
optional: false,
component: function show(p) {
return <Aria2RPC {...p} />;
},
},
{
slaveOnly: false,
title: "otherSettings",
optional: false,
component: function show(p) {
return <Metainfo {...p} />;
},
},
{
slaveOnly: false,
title: "finish",
optional: false,
component: function show(p) {
return <Completed {...p} />;
},
},
];
export default function NodeGuide(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "node" });
const { t: tDashboard } = useTranslation("dashboard");
const [activeStep, setActiveStep] = useState(0);
const [skipped, setSkipped] = React.useState(new Set());
const [loading, setLoading] = useState(false);
const [node, setNode] = useState(
props.node
? props.node
: {
Status: 1,
Type: 0,
Aria2Enabled: "false",
Server: "https://example.com:5212",
SlaveKey: randomStr(64),
MasterKey: randomStr(64),
Rank: "0",
Aria2OptionsSerialized: {
token: randomStr(32),
options: "{}",
interval: "10",
timeout: "10",
},
}
);
const usedSteps = useMemo(() => {
return steps.filter((step) => !(step.slaveOnly && node.Type === 1));
}, [node.Type]);
const isStepSkipped = (step) => {
return skipped.has(step);
};
const handleTextChange = (name) => (event) => {
setNode({
...node,
[name]: event.target.value,
});
};
const handleOptionChange = (name) => (event) => {
setNode({
...node,
Aria2OptionsSerialized: {
...node.Aria2OptionsSerialized,
[name]: event.target.value,
},
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const nextStep = () => {
if (props.node || activeStep + 1 === steps.length - 1) {
setLoading(true);
const nodeCopy = { ...node };
nodeCopy.Aria2OptionsSerialized = {
...node.Aria2OptionsSerialized,
};
nodeCopy.Rank = parseInt(nodeCopy.Rank);
nodeCopy.Aria2OptionsSerialized.interval = parseInt(
nodeCopy.Aria2OptionsSerialized.interval
);
nodeCopy.Aria2OptionsSerialized.timeout = parseInt(
nodeCopy.Aria2OptionsSerialized.timeout
);
nodeCopy.Aria2Enabled = nodeCopy.Aria2Enabled === "true";
API.post("/admin/node", {
node: nodeCopy,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
props.node ? t("nodeSavedNow") : t("nodeAdded"),
"success"
);
setActiveStep(activeStep + 1);
setLoading(false);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
} else {
setActiveStep(activeStep + 1);
}
};
return (
<div>
<Typography variant={"h6"}>
{props.node ? t("editNode") : t("addNode")}
</Typography>
<Stepper activeStep={activeStep}>
{usedSteps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (label.optional) {
labelProps.optional = (
<Typography variant="caption">
{tDashboard("policy.optional")}
</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
if (!(label.slaveOnly && node.Type === 1)) {
return (
<Step key={label.title} {...stepProps}>
<StepLabel {...labelProps}>
{t(label.title)}
</StepLabel>
</Step>
);
}
})}
</Stepper>
{usedSteps[activeStep].component({
onSubmit: (e) => nextStep(),
node: node,
loading: loading,
onBack: (e) => setActiveStep(activeStep - 1),
handleTextChange: handleTextChange,
activeStep: activeStep,
handleOptionChange: handleOptionChange,
})}
</div>
);
}

View File

@@ -0,0 +1,333 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import { useHistory } from "react-router";
import IconButton from "@material-ui/core/IconButton";
import {
Cancel,
CheckCircle,
Delete,
Edit,
Pause,
PlayArrow,
} from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Chip from "@material-ui/core/Chip";
import classNames from "classnames";
import Box from "@material-ui/core/Box";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
disabledBadge: {
marginLeft: theme.spacing(1),
height: 18,
},
disabledCell: {
color: theme.palette.text.disabled,
},
verticalAlign: {
verticalAlign: "middle",
display: "inline-block",
},
}));
const columns = [
{ id: "#", minWidth: 50 },
{ id: "name", minWidth: 170 },
{
id: "status",
minWidth: 50,
},
{
id: "features",
minWidth: 170,
},
{
id: "action",
minWidth: 170,
},
];
const features = [
{
field: "Aria2Enabled",
name: "remoteDownload",
},
];
export default function Node() {
const { t } = useTranslation("dashboard", { keyPrefix: "node" });
const classes = useStyles();
const [nodes, setNodes] = useState([]);
const [isActive, setIsActive] = useState({});
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const history = useHistory();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/node/list", {
page: page,
page_size: pageSize,
order_by: "id desc",
})
.then((response) => {
setNodes(response.data.items);
setTotal(response.data.total);
setIsActive(response.data.active);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
const toggleNode = (id, desired) => {
setLoading(true);
API.patch("/admin/node/enable/" + id + "/" + desired)
.then((response) => {
loadList();
ToggleSnackbar(
"top",
"right",
desired === 1 ? t("nodeDisabled") : t("nodeEnabled"),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteNode = (id) => {
setLoading(true);
API.delete("/admin/node/" + id)
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("nodeDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
useEffect(() => {
loadList();
}, [page, pageSize]);
const getStatusBadge = (status) => {
if (status === 1) {
return (
<Chip
className={classes.disabledBadge}
size="small"
label={t("disabled")}
/>
);
}
};
const getFeatureBadge = (node) =>
features.map((feature) => {
if (node[feature.field]) {
return (
<Chip
className={classes.disabledBadge}
size="small"
color="primary"
label={t(feature.name)}
/>
);
}
});
const getRealStatusBadge = (status) =>
status ? (
<Box color="success.main" fontSize="small">
<CheckCircle
className={classes.verticalAlign}
fontSize="small"
/>{" "}
<span className={classes.verticalAlign}>{t("online")}</span>
</Box>
) : (
<Box color="error.main" fontSize="small">
<Cancel className={classes.verticalAlign} fontSize="small" />{" "}
<span className={classes.verticalAlign}>{t("offline")}</span>
</Box>
);
return (
<div>
<div className={classes.header}>
<Button
color={"primary"}
onClick={() => history.push("/admin/node/add")}
variant={"contained"}
>
{t("addNewNode")}
</Button>
<div className={classes.headerRight}>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{t("refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
{columns.map((column) => (
<TableCell
key={column.id}
align={column.align}
style={{
minWidth: column.minWidthclassNames,
}}
>
{t(column.id)}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{nodes.map((row) => (
<TableRow hover key={row.ID}>
<TableCell>{row.ID}</TableCell>
<TableCell
className={classNames({
[classes.disabledCell]:
row.Status === 1,
})}
>
{row.Name}
{getStatusBadge(row.Status)}
</TableCell>
<TableCell>
{getRealStatusBadge(isActive[row.ID])}
</TableCell>
<TableCell>
{getFeatureBadge(row)}
</TableCell>
<TableCell align={"right"}>
<Tooltip
title={
row.Status === 1
? t("enableNode")
: t("disableNode")
}
>
<IconButton
disabled={loading}
onClick={() =>
toggleNode(
row.ID,
1 - row.Status
)
}
size={"small"}
>
{row.Status === 1 && (
<PlayArrow />
)}
{row.Status !== 1 && <Pause />}
</IconButton>
</Tooltip>
<Tooltip title={t("edit")}>
<IconButton
disabled={loading}
onClick={() =>
history.push(
"/admin/node/edit/" +
row.ID
)
}
size={"small"}
>
<Edit />
</IconButton>
</Tooltip>
<Tooltip title={t("delete")}>
<IconButton
onClick={() =>
deleteNode(row.ID)
}
disabled={loading}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,432 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import IconButton from "@material-ui/core/IconButton";
import { Delete } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Checkbox from "@material-ui/core/Checkbox";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { lighten } from "@material-ui/core";
import Link from "@material-ui/core/Link";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import ShareFilter from "../Dialogs/ShareFilter";
import { formatLocalTime } from "../../../utils/datetime";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function Order() {
const { t } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [orders, setOrders] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [users, setUsers] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/order/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setUsers(response.data.users);
setOrders(response.data.items);
setTotal(response.data.total);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deletePolicy = (id) => {
setLoading(true);
API.post("/admin/order/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("orderDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/order/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("orderDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = orders.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<ShareFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<div className={classes.headerRight}>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
{tDashboard("user.selectedObjects", {
num: selected.length,
})}
</Typography>
<Tooltip title={tDashboard("policy.delete")}>
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < orders.length
}
checked={
orders.length > 0 &&
selected.length === orders.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 10 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 130 }}>
{t("orderName")}
</TableCell>
<TableCell style={{ minWidth: 90 }}>
{t("product")}
</TableCell>
<TableCell style={{ minWidth: 80 }}>
<TableSortLabel
active={orderBy[0] === "order_no"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"order_no",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("orderNumber")}
{orderBy[0] === "order_no" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell
style={{ minWidth: 100 }}
align={"right"}
>
{t("price")}
</TableCell>
<TableCell
style={{ minWidth: 80 }}
align={"right"}
>
{t("qyt")}
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{t("status")}
</TableCell>
<TableCell style={{ minWidth: 80 }}>
{t("paidBy")}
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{t("orderOwner")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{tDashboard("file.createdAt")}
</TableCell>
<TableCell style={{ minWidth: 80 }}>
{tDashboard("policy.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{orders.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell>{row.Name}</TableCell>
<TableCell>
{row.Type === 0 && t("storagePack")}
{row.Type === 1 &&
t("purchasableGroups")}
{row.Type === 2 && t("credits")}
</TableCell>
<TableCell>{row.OrderNo}</TableCell>
<TableCell align={"right"}>
{row.Method === "score" && row.Price}
{row.Method !== "score" && (
<>
{(row.Price / 100).toFixed(2)}
</>
)}
</TableCell>
<TableCell align={"right"}>
{row.Num}
</TableCell>
<TableCell>
{row.Status === 0 && t("unpaid")}
{row.Status === 1 && t("paid")}
</TableCell>
<TableCell>
{row.Method === "score" && t("credits")}
{row.Method === "alipay" && t("alipay")}
{row.Method === "payjs" && t("payjs")}
{row.Method === "custom" &&
t("customPayment")}
{row.Method === "weixin" &&
t("wechatPay")}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" + row.UserID
}
>
{users[row.UserID]
? users[row.UserID].Nick
: tDashboard(
"file.unknownUploader"
)}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(row.CreatedAt)}
</TableCell>
<TableCell>
<Tooltip
title={tDashboard("policy.delete")}
>
<IconButton
disabled={loading}
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,45 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import { useParams } from "react-router";
import LocalGuide from "./Guid/LocalGuide";
import RemoteGuide from "./Guid/RemoteGuide";
import QiniuGuide from "./Guid/QiniuGuide";
import OSSGuide from "./Guid/OSSGuide";
import UpyunGuide from "./Guid/UpyunGuide";
import COSGuide from "./Guid/COSGuide";
import OneDriveGuide from "./Guid/OneDriveGuide";
import S3Guide from "./Guid/S3Guide";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
}));
export default function AddPolicyParent() {
const classes = useStyles();
const { type } = useParams();
return (
<div>
<Paper square className={classes.content}>
{type === "local" && <LocalGuide />}
{type === "remote" && <RemoteGuide />}
{type === "qiniu" && <QiniuGuide />}
{type === "oss" && <OSSGuide />}
{type === "upyun" && <UpyunGuide />}
{type === "cos" && <COSGuide />}
{type === "onedrive" && <OneDriveGuide />}
{type === "s3" && <S3Guide />}
</Paper>
</div>
);
}

View File

@@ -0,0 +1,80 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import { useParams } from "react-router";
import LocalGuide from "./Guid/LocalGuide";
import RemoteGuide from "./Guid/RemoteGuide";
import QiniuGuide from "./Guid/QiniuGuide";
import OSSGuide from "./Guid/OSSGuide";
import UpyunGuide from "./Guid/UpyunGuide";
import S3Guide from "./Guid/S3Guide";
import COSGuide from "./Guid/COSGuide";
import OneDriveGuide from "./Guid/OneDriveGuide";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import EditPro from "./Guid/EditPro";
import { toggleSnackbar } from "../../../redux/explorer";
import { transformResponse } from "./utils";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
}));
export default function EditPolicyPreload() {
const classes = useStyles();
const [type, setType] = useState("");
const [policy, setPolicy] = useState({});
const { mode, id } = useParams();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
setType("");
API.get("/admin/policy/" + id)
.then((response) => {
response = transformResponse(response);
setPolicy(response.data);
setType(response.data.Type);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, [id]);
return (
<div>
<Paper square className={classes.content}>
{mode === "guide" && (
<>
{type === "local" && <LocalGuide policy={policy} />}
{type === "remote" && <RemoteGuide policy={policy} />}
{type === "qiniu" && <QiniuGuide policy={policy} />}
{type === "oss" && <OSSGuide policy={policy} />}
{type === "upyun" && <UpyunGuide policy={policy} />}
{type === "cos" && <COSGuide policy={policy} />}
{type === "onedrive" && (
<OneDriveGuide policy={policy} />
)}
{type === "s3" && <S3Guide policy={policy} />}
</>
)}
{mode === "pro" && type !== "" && <EditPro policy={policy} />}
</Paper>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,690 @@
import React, { useCallback, useState } from "react";
import Typography from "@material-ui/core/Typography";
import { useDispatch } from "react-redux";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import RadioGroup from "@material-ui/core/RadioGroup";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Radio from "@material-ui/core/Radio";
import Button from "@material-ui/core/Button";
import API from "../../../../middleware/Api";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import { toggleSnackbar } from "../../../../redux/explorer";
import { useTranslation } from "react-i18next";
import { transformPolicyRequest } from "../utils";
export default function EditPro(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "policy" });
const [, setLoading] = useState(false);
const [policy, setPolicy] = useState(props.policy);
const handleChange = (name) => (event) => {
setPolicy({
...policy,
[name]: event.target.value,
});
};
const handleOptionChange = (name) => (event) => {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
[name]: event.target.value,
},
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const submitPolicy = (e) => {
e.preventDefault();
setLoading(true);
let policyCopy = { ...policy };
policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized };
// 类型转换
policyCopy = transformPolicyRequest(policyCopy);
API.post("/admin/policy", {
policy: policyCopy,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
props.policy ? t("policySaved") : t("policyAdded"),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
setLoading(false);
};
return (
<div>
<Typography variant={"h6"}>{t("editPolicy")}</Typography>
<TableContainer>
<form onSubmit={submitPolicy}>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>{t("setting")}</TableCell>
<TableCell>{t("value")}</TableCell>
<TableCell>{t("description")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell component="th" scope="row">
{t("id")}
</TableCell>
<TableCell>{policy.ID}</TableCell>
<TableCell>{t("policyID")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("type")}
</TableCell>
<TableCell>{policy.Type}</TableCell>
<TableCell>{t("policyType")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("name")}
</TableCell>
<TableCell>
<FormControl>
<Input
required
value={policy.Name}
onChange={handleChange("Name")}
/>
</FormControl>
</TableCell>
<TableCell>{t("policyName")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("server")}
</TableCell>
<TableCell>
<FormControl>
<Input
value={policy.Server}
onChange={handleChange("Server")}
/>
</FormControl>
</TableCell>
<TableCell>{t("policyEndpoint")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("bucketName")}
</TableCell>
<TableCell>
<FormControl>
<Input
value={policy.BucketName}
onChange={handleChange(
"BucketName"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("bucketID")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("privateBucket")}
</TableCell>
<TableCell>
<FormControl>
<RadioGroup
required
value={policy.IsPrivate}
onChange={handleChange("IsPrivate")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("yes")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("no")}
/>
</RadioGroup>
</FormControl>
</TableCell>
<TableCell>{t("privateBucketDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("resourceRootURL")}
</TableCell>
<TableCell>
<FormControl>
<Input
value={policy.BaseURL}
onChange={handleChange("BaseURL")}
/>
</FormControl>
</TableCell>
<TableCell>{t("resourceRootURLDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("accessKey")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
rowsMax={10}
value={policy.AccessKey}
onChange={handleChange("AccessKey")}
/>
</FormControl>
</TableCell>
<TableCell>{t("akDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("secretKey")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
rowsMax={10}
value={policy.SecretKey}
onChange={handleChange("SecretKey")}
/>
</FormControl>
</TableCell>
<TableCell>{t("secretKey")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("maxSizeBytes")}
</TableCell>
<TableCell>
<FormControl>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={policy.MaxSize}
onChange={handleChange("MaxSize")}
/>
</FormControl>
</TableCell>
<TableCell>{t("maxSizeBytesDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("autoRename")}
</TableCell>
<TableCell>
<FormControl>
<RadioGroup
required
value={policy.AutoRename}
onChange={handleChange(
"AutoRename"
)}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("yes")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("no")}
/>
</RadioGroup>
</FormControl>
</TableCell>
<TableCell>{t("autoRenameDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("storagePath")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={policy.DirNameRule}
onChange={handleChange(
"DirNameRule"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("storagePathDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("fileName")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={policy.FileNameRule}
onChange={handleChange(
"FileNameRule"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("fileNameDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("allowGetSourceLink")}
</TableCell>
<TableCell>
<FormControl>
<RadioGroup
required
value={policy.IsOriginLinkEnable}
onChange={handleChange(
"IsOriginLinkEnable"
)}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("yes")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("no")}
/>
</RadioGroup>
</FormControl>
</TableCell>
<TableCell>
{t("allowGetSourceLinkDes")}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("upyunToken")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized.token
}
onChange={handleOptionChange(
"token"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("upyunOnly")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("allowedFileExtension")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.file_type
}
onChange={handleOptionChange(
"file_type"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("emptyIsNoLimit")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("allowedMimetype")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.mimetype
}
onChange={handleOptionChange(
"mimetype"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("qiniuOnly")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("odRedirectURL")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.od_redirect
}
onChange={handleOptionChange(
"od_redirect"
)}
/>
</FormControl>
</TableCell>
<TableCell>
{t("noModificationNeeded")}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("odReverseProxy")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.od_proxy
}
onChange={handleOptionChange(
"od_proxy"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("odOnly")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("odDriverID")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.od_driver
}
onChange={handleOptionChange(
"od_driver"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("odDriverIDDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("s3Region")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized.region
}
onChange={handleOptionChange(
"region"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("s3Only")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("lanEndpoint")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.server_side_endpoint
}
onChange={handleOptionChange(
"server_side_endpoint"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("ossOnly")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("chunkSizeBytes")}
</TableCell>
<TableCell>
<FormControl>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={
policy.OptionsSerialized
.chunk_size
}
onChange={handleOptionChange(
"chunk_size"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("chunkSizeBytesDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("placeHolderWithSize")}
</TableCell>
<TableCell>
<FormControl>
<RadioGroup
required
value={
policy.OptionsSerialized
.placeholder_with_size
}
onChange={handleOptionChange(
"placeholder_with_size"
)}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("yes")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("no")}
/>
</RadioGroup>
</FormControl>
</TableCell>
<TableCell>
{t("placeHolderWithSizeDes")}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("tps")}
</TableCell>
<TableCell>
<FormControl>
<Input
type={"number"}
inputProps={{
step: 0.1,
}}
value={
policy.OptionsSerialized
.tps_limit
}
onChange={handleOptionChange(
"tps_limit"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("odOnly")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("tpsBurst")}
</TableCell>
<TableCell>
<FormControl>
<Input
type={"number"}
inputProps={{
step: 1,
}}
value={
policy.OptionsSerialized
.tps_limit_burst
}
onChange={handleOptionChange(
"tps_limit_burst"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("odOnly")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("usePathEndpoint")}
</TableCell>
<TableCell>
<FormControl>
<RadioGroup
required
value={
policy.OptionsSerialized
.s3_path_style
}
onChange={handleOptionChange(
"s3_path_style"
)}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("yes")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("no")}
/>
</RadioGroup>
</FormControl>
</TableCell>
<TableCell>{t("s3Only")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("thumbExt")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.thumb_exts
}
onChange={handleOptionChange(
"thumb_exts"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("thumbExtDes")}</TableCell>
</TableRow>
</TableBody>
</Table>
<Button
type={"submit"}
color={"primary"}
variant={"contained"}
style={{ margin: 8 }}
>
{t("saveChanges")}
</Button>
</form>
</TableContainer>
</div>
);
}

View File

@@ -0,0 +1,797 @@
import { lighten, makeStyles } from "@material-ui/core/styles";
import React, { useCallback, useState } from "react";
import Stepper from "@material-ui/core/Stepper";
import StepLabel from "@material-ui/core/StepLabel";
import Step from "@material-ui/core/Step";
import Typography from "@material-ui/core/Typography";
import { useDispatch } from "react-redux";
import Link from "@material-ui/core/Link";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import RadioGroup from "@material-ui/core/RadioGroup";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Radio from "@material-ui/core/Radio";
import Collapse from "@material-ui/core/Collapse";
import Button from "@material-ui/core/Button";
import API from "../../../../middleware/Api";
import MagicVar from "../../Dialogs/MagicVar";
import DomainInput from "../../Common/DomainInput";
import SizeInput from "../../Common/SizeInput";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../../redux/explorer";
import { getNumber } from "../../../../utils";
import { Trans, useTranslation } from "react-i18next";
import { transformPolicyRequest } from "../utils";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
}));
const steps = [
{
title: "storagePathStep",
optional: false,
},
{
title: "sourceLinkStep",
optional: false,
},
{
title: "uploadSettingStep",
optional: false,
},
{
title: "finishStep",
optional: false,
},
];
export default function LocalGuide(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "policy" });
const classes = useStyles();
const history = useHistory();
const [activeStep, setActiveStep] = useState(0);
const [loading, setLoading] = useState(false);
const [skipped] = React.useState(new Set());
const [magicVar, setMagicVar] = useState("");
const [useCDN, setUseCDN] = useState("false");
const [policy, setPolicy] = useState(
props.policy
? props.policy
: {
Type: "local",
Name: "",
DirNameRule: "uploads/{uid}/{path}",
AutoRename: "true",
FileNameRule: "{randomkey8}_{originname}",
IsOriginLinkEnable: "false",
BaseURL: "",
IsPrivate: "true",
MaxSize: "0",
OptionsSerialized: {
file_type: "",
chunk_size: 25 << 20,
},
}
);
const handleChange = (name) => (event) => {
setPolicy({
...policy,
[name]: event.target.value,
});
};
const handleOptionChange = (name) => (event) => {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
[name]: event.target.value,
},
});
};
const isStepSkipped = (step) => {
return skipped.has(step);
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const checkPathSetting = (e) => {
e.preventDefault();
setLoading(true);
// 测试路径是否可用
API.post("/admin/policy/test/path", {
path: policy.DirNameRule,
})
.then(() => {
setActiveStep(1);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const submitPolicy = (e) => {
e.preventDefault();
setLoading(true);
let policyCopy = { ...policy };
policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized };
// 处理存储策略
if (useCDN === "false" || policy.IsOriginLinkEnable === "false") {
policyCopy.BaseURL = "";
}
// 类型转换
policyCopy = transformPolicyRequest(policyCopy);
API.post("/admin/policy", {
policy: policyCopy,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
props.policy ? t("policySaved") : t("policyAdded"),
"success"
);
setActiveStep(4);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
setLoading(false);
};
return (
<div>
<Typography variant={"h6"}>
{props.policy
? t("editLocalStoragePolicy")
: t("addLocalStoragePolicy")}
</Typography>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (label.optional) {
labelProps.optional = (
<Typography variant="caption">
{t("optional")}
</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={t(label.title)} {...stepProps}>
<StepLabel {...labelProps}>
{t(label.title)}
</StepLabel>
</Step>
);
})}
</Stepper>
{activeStep === 0 && (
<form
className={classes.stepContent}
onSubmit={checkPathSetting}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"policy.pathMagicVarDes"}
components={[
<Link
key={0}
color={"secondary"}
href={"javascript:void()"}
onClick={() => setMagicVar("path")}
/>,
]}
/>
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("pathOfFolderToStoreFiles")}
</InputLabel>
<Input
required
value={policy.DirNameRule}
onChange={handleChange("DirNameRule")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"policy.filePathMagicVarDes"}
components={[
<Link
key={0}
color={"secondary"}
href={"javascript:void()"}
onClick={() => setMagicVar("file")}
/>,
]}
/>
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
aria-label="gender"
name="gender1"
value={policy.AutoRename}
onChange={handleChange("AutoRename")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("autoRenameStoredFile")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("keepOriginalFileName")}
/>
</RadioGroup>
</FormControl>
</div>
<Collapse in={policy.AutoRename === "true"}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("renameRule")}
</InputLabel>
<Input
required={
policy.AutoRename === "true"
}
value={policy.FileNameRule}
onChange={handleChange(
"FileNameRule"
)}
/>
</FormControl>
</div>
</Collapse>
</div>
</div>
<div className={classes.stepFooter}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("next")}
</Button>
</div>
</form>
)}
{activeStep === 1 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(2);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("enableGettingPermanentSourceLink")}
<br />
{t("enableGettingPermanentSourceLinkDes")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={policy.IsOriginLinkEnable}
onChange={handleChange(
"IsOriginLinkEnable"
)}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("allowed")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("forbidden")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.IsOriginLinkEnable === "true"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("useCDN")}
<br />
{t("useCDNDes")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={useCDN}
onChange={(e) => {
if (
e.target.value === "false"
) {
setPolicy({
...policy,
BaseURL: "",
});
}
setUseCDN(e.target.value);
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("use")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("notUse")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={useCDN === "true"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>3</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("cdnDomain")}
</Typography>
<div className={classes.form}>
<DomainInput
value={policy.BaseURL}
onChange={handleChange("BaseURL")}
required={
policy.IsOriginLinkEnable ===
"true" && useCDN === "true"
}
label={t("cdnPrefix")}
/>
</div>
</div>
</div>
</Collapse>
</Collapse>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(0)}
>
{t("back")}
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("next")}
</Button>
</div>
</form>
)}
{activeStep === 2 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(3);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("limitFileSize")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.MaxSize === "0"
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
MaxSize: "10485760",
});
} else {
setPolicy({
...policy,
MaxSize: "0",
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("limit")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("notLimit")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.MaxSize !== "0"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("enterSizeLimit")}
</Typography>
<div className={classes.form}>
<SizeInput
value={policy.MaxSize}
onChange={handleChange("MaxSize")}
min={0}
max={9223372036854775807}
label={t("maxSizeOfSingleFile")}
/>
</div>
</div>
</div>
</Collapse>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "3" : "2"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("limitFileExt")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.OptionsSerialized
.file_type === ""
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type:
"jpg,png,mp4,zip,rar",
},
});
} else {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type: "",
},
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("limit")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("notLimit")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.OptionsSerialized.file_type !== ""}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "4" : "3"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("enterFileExt")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("extList")}
</InputLabel>
<Input
value={
policy.OptionsSerialized
.file_type
}
onChange={handleOptionChange(
"file_type"
)}
/>
</FormControl>
</div>
</div>
</div>
</Collapse>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{getNumber(3, [
policy.MaxSize !== "0",
policy.OptionsSerialized.file_type !== "",
])}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("chunkSizeLabel")}
<br />
{t("chunkSizeDes")}
</Typography>
<div className={classes.form}>
<SizeInput
value={policy.OptionsSerialized.chunk_size}
onChange={handleOptionChange("chunk_size")}
min={0}
max={9223372036854775807}
label={t("chunkSize")}
/>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(1)}
>
{t("back")}
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("next")}
</Button>
</div>
</form>
)}
{activeStep === 3 && (
<form className={classes.stepContent} onSubmit={submitPolicy}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer} />
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("nameThePolicy")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("policyName")}
</InputLabel>
<Input
required
value={policy.Name}
onChange={handleChange("Name")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(2)}
>
{t("back")}
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("finish")}
</Button>
</div>
</form>
)}
{activeStep === 4 && (
<>
<form className={classes.stepContent}>
<Typography>
{props.policy ? t("policySaved") : t("policyAdded")}
</Typography>
<Typography variant={"body2"} color={"textSecondary"}>
{t("furtherActions")}
</Typography>
</form>
<div className={classes.stepFooter}>
<Button
color={"primary"}
className={classes.button}
onClick={() => history.push("/admin/policy")}
>
{t("backToList")}
</Button>
</div>
</>
)}
<MagicVar
open={magicVar === "file"}
isFile
onClose={() => setMagicVar("")}
/>
<MagicVar
open={magicVar === "path"}
onClose={() => setMagicVar("")}
/>
</div>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,903 @@
import { lighten, makeStyles } from "@material-ui/core/styles";
import React, { useCallback, useState } from "react";
import Stepper from "@material-ui/core/Stepper";
import StepLabel from "@material-ui/core/StepLabel";
import Step from "@material-ui/core/Step";
import Typography from "@material-ui/core/Typography";
import { useDispatch } from "react-redux";
import Link from "@material-ui/core/Link";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import RadioGroup from "@material-ui/core/RadioGroup";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Radio from "@material-ui/core/Radio";
import Collapse from "@material-ui/core/Collapse";
import Button from "@material-ui/core/Button";
import API from "../../../../middleware/Api";
import MagicVar from "../../Dialogs/MagicVar";
import DomainInput from "../../Common/DomainInput";
import SizeInput from "../../Common/SizeInput";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
import { transformPolicyRequest } from "../utils";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
}));
const steps = [
{
title: "storageBucket",
optional: false,
},
{
title: "storagePathStep",
optional: false,
},
{
title: "sourceLinkStep",
optional: false,
},
{
title: "uploadSettingStep",
optional: false,
},
{
title: "finishStep",
optional: false,
},
];
export default function UpyunGuide(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "policy" });
const classes = useStyles();
const history = useHistory();
const [activeStep, setActiveStep] = useState(0);
const [loading, setLoading] = useState(false);
const [skipped] = React.useState(new Set());
const [magicVar, setMagicVar] = useState("");
const [policy, setPolicy] = useState(
props.policy
? props.policy
: {
Type: "upyun",
Name: "",
SecretKey: "",
AccessKey: "",
BaseURL: "",
IsPrivate: "false",
DirNameRule: "uploads/{year}/{month}/{day}",
AutoRename: "true",
FileNameRule: "{randomkey8}_{originname}",
IsOriginLinkEnable: "false",
MaxSize: "0",
OptionsSerialized: {
file_type: "",
token: "",
},
}
);
const handleChange = (name) => (event) => {
setPolicy({
...policy,
[name]: event.target.value,
});
};
const handleOptionChange = (name) => (event) => {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
[name]: event.target.value,
},
});
};
const isStepSkipped = (step) => {
return skipped.has(step);
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const submitPolicy = (e) => {
e.preventDefault();
setLoading(true);
let policyCopy = { ...policy };
policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized };
// 类型转换
policyCopy = transformPolicyRequest(policyCopy);
API.post("/admin/policy", {
policy: policyCopy,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
props.policy ? t("policySaved") : t("policyAdded"),
"success"
);
setActiveStep(5);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
setLoading(false);
};
return (
<div>
<Typography variant={"h6"}>
{props.policy
? t("editUpyunStoragePolicy")
: t("addUpyunStoragePolicy")}
</Typography>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (label.optional) {
labelProps.optional = (
<Typography variant="caption">可选</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label.title} {...stepProps}>
<StepLabel {...labelProps}>
{t(label.title)}
</StepLabel>
</Step>
);
})}
</Stepper>
{activeStep === 0 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(1);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>0</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"policy.wanSiteURLDes"}
components={[<strong key={0} />]}
/>
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"policy.createUpyunBucketDes"}
components={[
<Link
key={0}
href={
"https://console.upyun.com/services/create/file/"
}
target={"_blank"}
/>,
]}
/>
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("storageServiceNameDes")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("storageServiceName")}
</InputLabel>
<Input
required
value={policy.BucketName}
onChange={handleChange("BucketName")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>3</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("operatorNameDes")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("operatorName")}
</InputLabel>
<Input
required
value={policy.AccessKey}
onChange={handleChange("AccessKey")}
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("operatorPassword")}
</InputLabel>
<Input
required
value={policy.SecretKey}
onChange={handleChange("SecretKey")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>4</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("upyunCDNDes")}
</Typography>
<div className={classes.form}>
<DomainInput
value={policy.BaseURL}
onChange={handleChange("BaseURL")}
required
label={t("bucketCDNDomain")}
/>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>5</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("upyunOptionalDes")}
<br />
{t("upyunTokenDes")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={policy.IsPrivate}
onChange={handleChange("IsPrivate")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("tokenEnabled")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("tokenDisabled")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.IsPrivate === "true"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>6</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("upyunTokenSecretDes")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("upyunTokenSecret")}
</InputLabel>
<Input
value={
policy.OptionsSerialized.token
}
onChange={handleOptionChange(
"token"
)}
required={
policy.IsPrivate === "true"
}
/>
</FormControl>
</div>
</div>
</div>
</Collapse>
<div className={classes.stepFooter}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("next")}
</Button>
</div>
</form>
)}
{activeStep === 1 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(2);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"policy.pathMagicVarDes"}
components={[
<Link
key={0}
color={"secondary"}
href={"javascript:void()"}
onClick={() => setMagicVar("path")}
/>,
]}
/>
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("pathOfFolderToStoreFiles")}
</InputLabel>
<Input
required
value={policy.DirNameRule}
onChange={handleChange("DirNameRule")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"policy.filePathMagicVarDes"}
components={[
<Link
key={0}
color={"secondary"}
href={"javascript:void()"}
onClick={() => setMagicVar("file")}
/>,
]}
/>
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
aria-label="gender"
name="gender1"
value={policy.AutoRename}
onChange={handleChange("AutoRename")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("autoRenameStoredFile")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("keepOriginalFileName")}
/>
</RadioGroup>
</FormControl>
</div>
<Collapse in={policy.AutoRename === "true"}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("renameRule")}
</InputLabel>
<Input
required={
policy.AutoRename === "true"
}
value={policy.FileNameRule}
onChange={handleChange(
"FileNameRule"
)}
/>
</FormControl>
</div>
</Collapse>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(0)}
>
{t("back")}
</Button>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("next")}
</Button>
</div>
</form>
)}
{activeStep === 2 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(3);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("enableGettingPermanentSourceLink")}
<br />
{t("enableGettingPermanentSourceLinkDes")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={policy.IsOriginLinkEnable}
onChange={(e) => {
if (
policy.IsPrivate === "true" &&
e.target.value === "true"
) {
ToggleSnackbar(
"top",
"right",
t(
"cannotEnableForTokenProtectedBucket"
),
"warning"
);
}
handleChange("IsOriginLinkEnable")(
e
);
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("allowed")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("forbidden")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(1)}
>
{t("back")}
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("next")}
</Button>
</div>
</form>
)}
{activeStep === 3 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(4);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("limitFileSize")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.MaxSize === "0"
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
MaxSize: "10485760",
});
} else {
setPolicy({
...policy,
MaxSize: "0",
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("limit")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("notLimit")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.MaxSize !== "0"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("enterSizeLimit")}
</Typography>
<div className={classes.form}>
<SizeInput
value={policy.MaxSize}
onChange={handleChange("MaxSize")}
min={0}
max={9223372036854775807}
label={t("maxSizeOfSingleFile")}
/>
</div>
</div>
</div>
</Collapse>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "3" : "2"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("limitFileExt")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.OptionsSerialized
.file_type === ""
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type:
"jpg,png,mp4,zip,rar",
},
});
} else {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type: "",
},
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("limit")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("notLimit")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.OptionsSerialized.file_type !== ""}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "4" : "3"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("enterFileExt")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("extList")}
</InputLabel>
<Input
value={
policy.OptionsSerialized
.file_type
}
onChange={handleOptionChange(
"file_type"
)}
/>
</FormControl>
</div>
</div>
</div>
</Collapse>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(2)}
>
{t("back")}
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("next")}
</Button>
</div>
</form>
)}
{activeStep === 4 && (
<form className={classes.stepContent} onSubmit={submitPolicy}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}></div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("nameThePolicy")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("policyName")}
</InputLabel>
<Input
required
value={policy.Name}
onChange={handleChange("Name")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(3)}
>
{t("back")}
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("finish")}
</Button>
</div>
</form>
)}
{activeStep === 5 && (
<>
<form className={classes.stepContent}>
<Typography>
{props.policy ? t("policySaved") : t("policyAdded")}
</Typography>
<Typography variant={"body2"} color={"textSecondary"}>
{t("furtherActions")}
</Typography>
</form>
<div className={classes.stepFooter}>
<Button
color={"primary"}
className={classes.button}
onClick={() => history.push("/admin/policy")}
>
{t("backToList")}
</Button>
</div>
</>
)}
<MagicVar
open={magicVar === "file"}
isFile
onClose={() => setMagicVar("")}
/>
<MagicVar
open={magicVar === "path"}
onClose={() => setMagicVar("")}
/>
</div>
);
}

View File

@@ -0,0 +1,297 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import { sizeToString } from "../../../utils";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import AddPolicy from "../Dialogs/AddPolicy";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import { useHistory, useLocation } from "react-router";
import IconButton from "@material-ui/core/IconButton";
import { Delete, Edit } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Menu from "@material-ui/core/Menu";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
}));
const columns = [
{ id: "#", label: "sharp", minWidth: 50 },
{ id: "name", label: "name", minWidth: 170 },
{ id: "type", label: "type", minWidth: 170 },
{
id: "count",
label: "childFiles",
minWidth: 50,
align: "right",
},
{
id: "size",
label: "totalSize",
minWidth: 100,
align: "right",
},
{
id: "action",
label: "actions",
minWidth: 170,
align: "right",
},
];
function useQuery() {
return new URLSearchParams(useLocation().search);
}
export default function Policy() {
const { t } = useTranslation("dashboard", { keyPrefix: "policy" });
const classes = useStyles();
const [policies, setPolicies] = useState([]);
const [statics, setStatics] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [addDialog, setAddDialog] = useState(false);
const [filter, setFilter] = useState("all");
const [anchorEl, setAnchorEl] = React.useState(null);
const [editID, setEditID] = React.useState(0);
const location = useLocation();
const history = useHistory();
const query = useQuery();
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
if (query.get("code") === "0") {
ToggleSnackbar("top", "right", t("authSuccess"), "success");
} else if (query.get("msg") && query.get("msg") !== "") {
ToggleSnackbar(
"top",
"right",
query.get("msg") + ", " + query.get("err"),
"warning"
);
}
}, [location]);
const loadList = () => {
API.post("/admin/policy/list", {
page: page,
page_size: pageSize,
order_by: "id desc",
conditions: filter === "all" ? {} : { type: filter },
})
.then((response) => {
setPolicies(response.data.items);
setStatics(response.data.statics);
setTotal(response.data.total);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, filter]);
const deletePolicy = (id) => {
API.delete("/admin/policy/" + id)
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("policyDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
const open = Boolean(anchorEl);
return (
<div>
<AddPolicy open={addDialog} onClose={() => setAddDialog(false)} />
<div className={classes.header}>
<Button
color={"primary"}
onClick={() => setAddDialog(true)}
variant={"contained"}
>
{t("newStoragePolicy")}
</Button>
<div className={classes.headerRight}>
<Select
style={{
marginRight: 8,
}}
value={filter}
onChange={(e) => setFilter(e.target.value)}
>
{[
"all",
"local",
"remote",
"qiniu",
"upyun",
"oss",
"cos",
"onedrive",
"s3",
].map((v) => (
<MenuItem key={v} value={v}>
{t(v)}
</MenuItem>
))}
</Select>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{t("refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
{columns.map((column) => (
<TableCell
key={column.id}
align={column.align}
style={{ minWidth: column.minWidth }}
>
{t(column.label)}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{policies.map((row) => (
<TableRow hover key={row.ID}>
<TableCell>{row.ID}</TableCell>
<TableCell>{row.Name}</TableCell>
<TableCell>{t(row.Type)}</TableCell>
<TableCell align={"right"}>
{statics[row.ID] !== undefined &&
statics[row.ID][0].toLocaleString()}
</TableCell>
<TableCell align={"right"}>
{statics[row.ID] !== undefined &&
sizeToString(statics[row.ID][1])}
</TableCell>
<TableCell align={"right"}>
<Tooltip title={t("delete")}>
<IconButton
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
<Tooltip title={t("edit")}>
<IconButton
onClick={(e) => {
setEditID(row.ID);
handleClick(e);
}}
size={"small"}
>
<Edit />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
<Menu
open={open}
anchorEl={anchorEl}
onClose={handleClose}
keepMounted
>
<MenuItem
onClick={(e) => {
handleClose(e);
history.push("/admin/policy/edit/pro/" + editID);
}}
>
{t("editInProMode")}
</MenuItem>
<MenuItem
onClick={(e) => {
handleClose(e);
history.push("/admin/policy/edit/guide/" + editID);
}}
>
{t("editInWizardMode")}
</MenuItem>
</Menu>
</div>
);
}

View File

@@ -0,0 +1,76 @@
const boolFields = ["IsOriginLinkEnable", "AutoRename", "IsPrivate"];
const numberFields = ["MaxSize"];
const boolFieldsInOptions = ["placeholder_with_size", "s3_path_style"];
const numberFieldsInOptions = ["chunk_size", "tps_limit", "tps_limit_burst"];
const listJsonFieldsInOptions = ["file_type", "thumb_exts"];
export const transformResponse = (response) => {
boolFields.forEach(
(field) =>
(response.data[field] = response.data[field] ? "true" : "false")
);
numberFields.forEach(
(field) => (response.data[field] = response.data[field].toString())
);
boolFieldsInOptions.forEach(
(field) =>
(response.data.OptionsSerialized[field] = response.data
.OptionsSerialized[field]
? "true"
: "false")
);
numberFieldsInOptions.forEach(
(field) =>
(response.data.OptionsSerialized[field] = response.data
.OptionsSerialized[field]
? response.data.OptionsSerialized[field].toString()
: 0)
);
listJsonFieldsInOptions.forEach((field) => {
response.data.OptionsSerialized[field] = response.data
.OptionsSerialized[field]
? response.data.OptionsSerialized[field].join(",")
: "";
});
return response;
};
export const transformPolicyRequest = (policyCopy) => {
boolFields.forEach(
(field) => (policyCopy[field] = policyCopy[field] === "true")
);
numberFields.forEach(
(field) => (policyCopy[field] = parseInt(policyCopy[field]))
);
boolFieldsInOptions.forEach(
(field) =>
(policyCopy.OptionsSerialized[field] =
policyCopy.OptionsSerialized[field] === "true")
);
numberFieldsInOptions.forEach(
(field) =>
(policyCopy.OptionsSerialized[field] = parseInt(
policyCopy.OptionsSerialized[field]
))
);
listJsonFieldsInOptions.forEach((field) => {
policyCopy.OptionsSerialized[field] = policyCopy.OptionsSerialized[
field
]
? policyCopy.OptionsSerialized[field].split(",")
: [];
if (
policyCopy.OptionsSerialized[field].length === 1 &&
policyCopy.OptionsSerialized[field][0] === ""
) {
policyCopy.OptionsSerialized[field] = [];
}
});
return policyCopy;
};

View File

@@ -0,0 +1,422 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import IconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";
import Checkbox from "@material-ui/core/Checkbox";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { lighten } from "@material-ui/core";
import Link from "@material-ui/core/Link";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import { reportReasons } from "../../../config";
import CheckCircleOutlineIcon from "@material-ui/icons/CheckCircleOutline";
import { Delete } from "@material-ui/icons";
import { formatLocalTime } from "../../../utils/datetime";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function ReportList() {
const { t } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tApp } = useTranslation("application");
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [reports, setReports] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [users, setUsers] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const [ids, setIds] = useState({});
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/report/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
})
.then((response) => {
setUsers(response.data.users);
setReports(response.data.items);
setTotal(response.data.total);
setIds(response.data.ids);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy]);
const deleteReport = (id) => {
setLoading(true);
API.post("/admin/report/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("markSuccessful"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteShare = (id) => {
setLoading(true);
API.post("/admin/share/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar(
"top",
"right",
tDashboard("share.deleted"),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/report/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("markSuccessful"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = reports.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<div className={classes.header}>
<div className={classes.headerRight}>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
{tDashboard("user.selectedObjects", {
num: selected.length,
})}
</Typography>
<Tooltip title={t("markAsResolved")}>
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<CheckCircleOutlineIcon />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < reports.length
}
checked={
reports.length > 0 &&
selected.length === reports.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 10 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 130 }}>
{t("reportedContent")}
</TableCell>
<TableCell style={{ minWidth: 90 }}>
{tDashboard("policy.type")}
</TableCell>
<TableCell style={{ minWidth: 90 }}>
{t("reason")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{t("description")}
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{tDashboard("vas.shareLink")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{t("reportTime")}
</TableCell>
<TableCell style={{ minWidth: 80 }}>
{tDashboard("policy.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{reports.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell
style={{ wordBreak: "break-all" }}
>
{row.Share.ID === 0 && t("invalid")}
{row.Share.ID > 0 && (
<Link
target={"_blank"}
color="inherit"
href={
"/s/" +
ids[row.Share.ID] +
(row.Share.Password === ""
? ""
: "?password=" +
row.Share.Password)
}
>
{row.Share.SourceName}
</Link>
)}
</TableCell>
<TableCell>
{row.Share.ID > 0 &&
(row.Share.IsDir
? tDashboard("share.folder")
: tDashboard("share.file"))}
</TableCell>
<TableCell>
{tApp(reportReasons[row.Reason])}
</TableCell>
<TableCell
style={{ wordBreak: "break-all" }}
>
{row.Description}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" +
row.Share.UserID
}
>
{users[row.Share.UserID]
? users[row.Share.UserID].Nick
: tDashboard(
"file.unknownUploader"
)}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(row.CreatedAt)}
</TableCell>
<TableCell>
<Tooltip title={t("markAsResolved")}>
<IconButton
disabled={loading}
onClick={() =>
deleteReport(row.ID)
}
size={"small"}
>
<CheckCircleOutlineIcon />
</IconButton>
</Tooltip>
{row.Share.ID > 0 && (
<Tooltip title={t("deleteShare")}>
<IconButton
disabled={loading}
onClick={() =>
deleteShare(
row.Share.ID
)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,502 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Switch from "@material-ui/core/Switch";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import AlertDialog from "../Dialogs/Alert";
import Alert from "@material-ui/lab/Alert";
import FileSelector from "../Common/FileSelector";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function Access() {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const { t: tVas } = useTranslation("dashboard", { keyPrefix: "vas" });
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [initCompleted, setInitComplete] = useState(false);
const [options, setOptions] = useState({
register_enabled: "1",
default_group: "1",
email_active: "0",
login_captcha: "0",
reg_captcha: "0",
forget_captcha: "0",
qq_login: "0",
qq_direct_login: "0",
qq_login_id: "",
qq_login_key: "",
authn_enabled: "0",
mail_domain_filter: "0",
mail_domain_filter_list: "",
initial_files: "[]",
});
const [siteURL, setSiteURL] = useState("");
const [groups, setGroups] = useState([]);
const [httpAlert, setHttpAlert] = useState(false);
const handleChange = (name) => (event) => {
let value = event.target.value;
if (event.target.checked !== undefined) {
value = event.target.checked ? "1" : "0";
}
setOptions({
...options,
[name]: value,
});
};
const handleInputChange = (name) => (event) => {
const value = event.target.value;
setOptions({
...options,
[name]: value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: [...Object.keys(options), "siteURL"],
})
.then((response) => {
setSiteURL(response.data.siteURL);
delete response.data.siteURL;
setOptions(response.data);
setInitComplete(true);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
API.get("/admin/groups")
.then((response) => {
setGroups(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", t("saved"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<AlertDialog
title={t("hint")}
msg={t("webauthnNoHttps")}
onClose={() => setHttpAlert(false)}
open={httpAlert}
/>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("accountManagement")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.register_enabled === "1"
}
onChange={handleChange(
"register_enabled"
)}
/>
}
label={t("allowNewRegistrations")}
/>
<FormHelperText id="component-helper-text">
{t("allowNewRegistrationsDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.email_active === "1"
}
onChange={handleChange(
"email_active"
)}
/>
}
label={t("emailActivation")}
/>
<FormHelperText id="component-helper-text">
{t("emailActivationDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.reg_captcha === "1"
}
onChange={handleChange(
"reg_captcha"
)}
/>
}
label={t("captchaForSignup")}
/>
<FormHelperText id="component-helper-text">
{t("captchaForSignupDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.login_captcha === "1"
}
onChange={handleChange(
"login_captcha"
)}
/>
}
label={t("captchaForLogin")}
/>
<FormHelperText id="component-helper-text">
{t("captchaForLoginDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.forget_captcha === "1"
}
onChange={handleChange(
"forget_captcha"
)}
/>
}
label={t("captchaForReset")}
/>
<FormHelperText id="component-helper-text">
{t("captchaForResetDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.authn_enabled === "1"
}
onChange={(e) => {
if (
!siteURL.startsWith(
"https://"
)
) {
setHttpAlert(true);
return;
}
handleChange("authn_enabled")(
e
);
}}
/>
}
label={t("webauthn")}
/>
<FormHelperText id="component-helper-text">
{t("webauthnDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("defaultGroup")}
</InputLabel>
<Select
value={options.default_group}
onChange={handleInputChange(
"default_group"
)}
required
>
{groups.map((v) => {
if (v.ID === 3) {
return null;
}
return (
<MenuItem
key={v.ID}
value={v.ID.toString()}
>
{v.Name}
</MenuItem>
);
})}
</Select>
<FormHelperText id="component-helper-text">
{t("defaultGroupDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
{initCompleted && (
<FileSelector
label={tVas("initialFiles")}
value={JSON.parse(
options.initial_files
)}
onChange={(v) =>
handleInputChange("initial_files")({
target: { value: v },
})
}
/>
)}
<FormHelperText id="component-helper-text">
{tVas("initialFilesDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{tVas("filterEmailProvider")}
</InputLabel>
<Select
value={options.mail_domain_filter}
onChange={handleInputChange(
"mail_domain_filter"
)}
required
>
{[
tVas("filterEmailProviderDisabled"),
tVas("filterEmailProviderWhitelist"),
tVas("filterEmailProviderBlacklist"),
].map((v, i) => (
<MenuItem key={i} value={i.toString()}>
{v}
</MenuItem>
))}
</Select>
<FormHelperText id="component-helper-text">
{tVas("filterEmailProviderDes")}
</FormHelperText>
</FormControl>
</div>
{options.mail_domain_filter !== "0" && (
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tVas("filterEmailProviderRule")}
</InputLabel>
<Input
value={options.mail_domain_filter_list}
onChange={handleChange(
"mail_domain_filter_list"
)}
multiline
rowsMax="10"
/>
<FormHelperText id="component-helper-text">
{tVas("filterEmailProviderRuleDes")}
</FormHelperText>
</FormControl>
</div>
)}
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{tVas("qqConnect")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Alert severity="info">
{tVas("qqConnectHint", {
url: siteURL.endsWith("/")
? siteURL + "login/qq"
: siteURL + "/login/qq",
})}
</Alert>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={options.qq_login === "1"}
onChange={handleChange("qq_login")}
/>
}
label={tVas("enableQQConnect")}
/>
<FormHelperText id="component-helper-text">
{tVas("enableQQConnectDes")}
</FormHelperText>
</FormControl>
</div>
{options.qq_login === "1" && (
<>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.qq_direct_login ===
"1"
}
onChange={handleChange(
"qq_direct_login"
)}
/>
}
label={tVas("loginWithoutBinding")}
/>
<FormHelperText id="component-helper-text">
{tVas("loginWithoutBindingDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tVas("appid")}
</InputLabel>
<Input
required
value={options.qq_login_id}
onChange={handleInputChange(
"qq_login_id"
)}
/>
<FormHelperText id="component-helper-text">
{tVas("appidDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tVas("appKey")}
</InputLabel>
<Input
required
value={options.qq_login_key}
onChange={handleInputChange(
"qq_login_key"
)}
/>
<FormHelperText id="component-helper-text">
{tVas("appKeyDes")}
</FormHelperText>
</FormControl>
</div>
</>
)}
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("save")}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,537 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Input from "@material-ui/core/Input";
import Link from "@material-ui/core/Link";
import { toggleSnackbar } from "../../../redux/explorer";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
import { Trans, useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function Captcha() {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
captcha_type: "normal",
captcha_height: "1",
captcha_width: "1",
captcha_mode: "3",
captcha_CaptchaLen: "6",
captcha_ComplexOfNoiseText: "0",
captcha_ComplexOfNoiseDot: "0",
captcha_IsShowHollowLine: "0",
captcha_IsShowNoiseDot: "0",
captcha_IsShowNoiseText: "0",
captcha_IsShowSlimeLine: "0",
captcha_IsShowSineLine: "0",
captcha_ReCaptchaKey: "",
captcha_ReCaptchaSecret: "",
captcha_TCaptcha_CaptchaAppId: "",
captcha_TCaptcha_AppSecretKey: "",
captcha_TCaptcha_SecretId: "",
captcha_TCaptcha_SecretKey: "",
});
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", t("saved"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleCheckChange = (name) => (event) => {
const value = event.target.checked ? "1" : "0";
setOptions({
...options,
[name]: value,
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("captcha")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("captchaType")}
</InputLabel>
<Select
value={options.captcha_type}
onChange={handleChange("captcha_type")}
required
>
<MenuItem value={"normal"}>
{t("plainCaptcha")}
</MenuItem>
<MenuItem value={"recaptcha"}>
{t("reCaptchaV2")}
</MenuItem>
<MenuItem value={"tcaptcha"}>
{t("tencentCloudCaptcha")}
</MenuItem>
</Select>
<FormHelperText id="component-helper-text">
{t("captchaProvider")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
{options.captcha_type === "normal" && (
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("plainCaptchaTitle")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("captchaWidth")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.captcha_width}
onChange={handleChange("captcha_width")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("captchaHeight")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.captcha_height}
onChange={handleChange(
"captcha_height"
)}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("captchaLength")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.captcha_CaptchaLen}
onChange={handleChange(
"captcha_CaptchaLen"
)}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("captchaMode")}
</InputLabel>
<Select
value={options.captcha_mode}
onChange={handleChange("captcha_mode")}
required
>
<MenuItem value={"0"}>
{t("captchaModeNumber")}
</MenuItem>
<MenuItem value={"1"}>
{t("captchaModeLetter")}
</MenuItem>
<MenuItem value={"2"}>
{t("captchaModeMath")}
</MenuItem>
<MenuItem value={"3"}>
{t("captchaModeNumberLetter")}
</MenuItem>
</Select>
<FormHelperText id="component-helper-text">
{t("captchaElement")}
</FormHelperText>
</FormControl>
</div>
{[
{
name: "complexOfNoiseText",
field: "captcha_ComplexOfNoiseText",
},
{
name: "complexOfNoiseDot",
field: "captcha_ComplexOfNoiseDot",
},
{
name: "showHollowLine",
field: "captcha_IsShowHollowLine",
},
{
name: "showNoiseDot",
field: "captcha_IsShowNoiseDot",
},
{
name: "showNoiseText",
field: "captcha_IsShowNoiseText",
},
{
name: "showSlimeLine",
field: "captcha_IsShowSlimeLine",
},
{
name: "showSineLine",
field: "captcha_IsShowSineLine",
},
].map((input) => (
<div key={input.name} className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options[input.field] ===
"1"
}
onChange={handleCheckChange(
input.field
)}
/>
}
label={t(input.name)}
/>
</FormControl>
</div>
))}
</div>
</div>
)}
{options.captcha_type === "recaptcha" && (
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("reCaptchaV2")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("siteKey")}
</InputLabel>
<Input
required
value={options.captcha_ReCaptchaKey}
onChange={handleChange(
"captcha_ReCaptchaKey"
)}
/>
<FormHelperText id="component-helper-text">
<Trans
ns={"dashboard"}
i18nKey={"settings.siteKeyDes"}
components={[
<Link
key={0}
href={
"https://www.google.com/recaptcha/admin/create"
}
target={"_blank"}
/>,
]}
/>
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("siteSecret")}
</InputLabel>
<Input
required
value={
options.captcha_ReCaptchaSecret
}
onChange={handleChange(
"captcha_ReCaptchaSecret"
)}
/>
<FormHelperText id="component-helper-text">
<Trans
ns={"dashboard"}
i18nKey={
"settings.siteSecretDes"
}
components={[
<Link
key={0}
href={
"https://www.google.com/recaptcha/admin/create"
}
target={"_blank"}
/>,
]}
/>
</FormHelperText>
</FormControl>
</div>
</div>
</div>
</div>
)}
{options.captcha_type === "tcaptcha" && (
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("tencentCloudCaptcha")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("secretID")}
</InputLabel>
<Input
required
value={
options.captcha_TCaptcha_SecretId
}
onChange={handleChange(
"captcha_TCaptcha_SecretId"
)}
/>
<FormHelperText id="component-helper-text">
<Trans
ns={"dashboard"}
i18nKey={
"settings.siteSecretDes"
}
components={[
<Link
key={0}
href={
"https://console.cloud.tencent.com/capi"
}
target={"_blank"}
/>,
]}
/>
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("secretKey")}
</InputLabel>
<Input
required
value={
options.captcha_TCaptcha_SecretKey
}
onChange={handleChange(
"captcha_TCaptcha_SecretKey"
)}
/>
<FormHelperText id="component-helper-text">
<Trans
ns={"dashboard"}
i18nKey={
"settings.secretKeyDes"
}
components={[
<Link
key={0}
href={
"https://console.cloud.tencent.com/capi"
}
target={"_blank"}
/>,
]}
/>
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("tCaptchaAppID")}
</InputLabel>
<Input
required
value={
options.captcha_TCaptcha_CaptchaAppId
}
onChange={handleChange(
"captcha_TCaptcha_CaptchaAppId"
)}
/>
<FormHelperText id="component-helper-text">
<Trans
ns={"dashboard"}
i18nKey={
"settings.tCaptchaAppIDDes"
}
components={[
<Link
key={0}
href={
"https://console.cloud.tencent.com/captcha/graphical"
}
target={"_blank"}
/>,
]}
/>
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("tCaptchaSecretKey")}
</InputLabel>
<Input
required
value={
options.captcha_TCaptcha_AppSecretKey
}
onChange={handleChange(
"captcha_TCaptcha_AppSecretKey"
)}
/>
<FormHelperText id="component-helper-text">
<Trans
ns={"dashboard"}
i18nKey={
"settings.tCaptchaSecretKeyDes"
}
components={[
<Link
key={0}
href={
"https://console.cloud.tencent.com/captcha/graphical"
}
target={"_blank"}
/>,
]}
/>
</FormHelperText>
</FormControl>
</div>
</div>
</div>
</div>
)}
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("save")}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,664 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import SizeInput from "../Common/SizeInput";
import { toggleSnackbar } from "../../../redux/explorer";
import Alert from "@material-ui/lab/Alert";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
import { Trans, useTranslation } from "react-i18next";
import Link from "@material-ui/core/Link";
import ThumbGenerators from "./ThumbGenerators";
import PolicySelector from "../Common/PolicySelector";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function ImageSetting() {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
gravatar_server: "",
avatar_path: "",
avatar_size: "",
avatar_size_l: "",
avatar_size_m: "",
avatar_size_s: "",
thumb_width: "",
thumb_height: "",
office_preview_service: "",
thumb_file_suffix: "",
thumb_max_task_count: "",
thumb_encode_method: "",
thumb_gc_after_gen: "0",
thumb_encode_quality: "",
maxEditSize: "",
wopi_enabled: "0",
wopi_endpoint: "",
wopi_session_timeout: "0",
thumb_builtin_enabled: "0",
thumb_vips_enabled: "0",
thumb_vips_exts: "",
thumb_ffmpeg_enabled: "0",
thumb_vips_path: "",
thumb_ffmpeg_path: "",
thumb_ffmpeg_exts: "",
thumb_ffmpeg_seek: "",
thumb_libreoffice_path: "",
thumb_libreoffice_enabled: "0",
thumb_libreoffice_exts: "",
thumb_proxy_enabled: "0",
thumb_proxy_policy: [],
thumb_max_src_size: "",
});
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
response.data.thumb_proxy_policy = JSON.parse(
response.data.thumb_proxy_policy
).map((v) => {
return v.toString();
});
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const reload = () => {
API.get("/admin/reload/wopi")
// eslint-disable-next-line @typescript-eslint/no-empty-function
.then(() => {})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
.then(() => {});
};
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
let value = options[k];
if (k === "thumb_proxy_policy") {
value = JSON.stringify(value.map((v) => parseInt(v)));
}
option.push({
key: k,
value,
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", t("saved"), "success");
reload();
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleCheckChange = (name) => (event) => {
const value = event.target.checked ? "1" : "0";
setOptions({
...options,
[name]: value,
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("avatar")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("gravatarServer")}
</InputLabel>
<Input
type={"url"}
value={options.gravatar_server}
onChange={handleChange("gravatar_server")}
required
/>
<FormHelperText id="component-helper-text">
{t("gravatarServerDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("avatarFilePath")}
</InputLabel>
<Input
value={options.avatar_path}
onChange={handleChange("avatar_path")}
required
/>
<FormHelperText id="component-helper-text">
{t("avatarFilePathDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
{options.avatar_size !== "" && (
<SizeInput
value={options.avatar_size}
onChange={handleChange("avatar_size")}
required
min={0}
max={2147483647}
label={t("avatarSize")}
/>
)}
<FormHelperText id="component-helper-text">
{t("avatarSizeDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("smallAvatarSize")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.avatar_size_s}
onChange={handleChange("avatar_size_s")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("mediumAvatarSize")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.avatar_size_m}
onChange={handleChange("avatar_size_m")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("largeAvatarSize")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.avatar_size_l}
onChange={handleChange("avatar_size_l")}
required
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("filePreview")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("officePreviewService")}
</InputLabel>
<Input
value={options.office_preview_service}
onChange={handleChange(
"office_preview_service"
)}
required
/>
<FormHelperText id="component-helper-text">
{t("officePreviewServiceDes")}
<br />
<code>{"{$src}"}</code> -{" "}
{t("officePreviewServiceSrcDes")}
<br />
<code>{"{$srcB64}"}</code> -{" "}
{t("officePreviewServiceSrcB64Des")}
<br />
<code>{"{$name}"}</code> -{" "}
{t("officePreviewServiceName")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
{options.maxEditSize !== "" && (
<SizeInput
value={options.maxEditSize}
onChange={handleChange("maxEditSize")}
required
min={0}
max={2147483647}
label={t("textEditMaxSize")}
/>
)}
<FormHelperText id="component-helper-text">
{t("textEditMaxSizeDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("wopiClient")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Alert severity="info">
<Trans
ns={"dashboard"}
i18nKey={"settings.wopiClientDes"}
components={[
<Link
key={0}
target={"_blank"}
href={t("wopiDocLink")}
/>,
]}
/>
</Alert>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.wopi_enabled === "1"
}
onChange={handleCheckChange(
"wopi_enabled"
)}
/>
}
label={t("enableWopi")}
/>
</FormControl>
</div>
{options.wopi_enabled === "1" && (
<>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("wopiEndpoint")}
</InputLabel>
<Input
value={options.wopi_endpoint}
onChange={handleChange(
"wopi_endpoint"
)}
required
/>
<FormHelperText id="component-helper-text">
{t("wopiEndpointDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("wopiSessionTtl")}
</InputLabel>
<Input
inputProps={{ min: 1, step: 1 }}
type={"number"}
value={options.wopi_session_timeout}
onChange={handleChange(
"wopi_session_timeout"
)}
required
/>
<FormHelperText id="component-helper-text">
{t("wopiSessionTtlDes")}
</FormHelperText>
</FormControl>
</div>
</>
)}
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("thumbnails")}
</Typography>
<div className={classes.form}>
<Alert severity="info">
<Trans
ns={"dashboard"}
i18nKey={"settings.thumbnailDoc"}
components={[
<Link
key={0}
target={"_blank"}
href={t("thumbnailDocLink")}
/>,
]}
/>
</Alert>
</div>
<Typography variant="subtitle1" gutterBottom>
{t("thumbnailBasic")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("thumbWidth")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.thumb_width}
onChange={handleChange("thumb_width")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("thumbHeight")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.thumb_height}
onChange={handleChange("thumb_height")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("thumbSuffix")}
</InputLabel>
<Input
type={"text"}
value={options.thumb_file_suffix}
onChange={handleChange("thumb_file_suffix")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("thumbConcurrent")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: -1,
step: 1,
}}
value={options.thumb_max_task_count}
onChange={handleChange(
"thumb_max_task_count"
)}
required
/>
<FormHelperText id="component-helper-text">
{t("thumbConcurrentDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("thumbFormat")}
</InputLabel>
<Input
type={"test"}
value={options.thumb_encode_method}
onChange={handleChange(
"thumb_encode_method"
)}
required
/>
<FormHelperText id="component-helper-text">
{t("thumbFormatDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("thumbQuality")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
max: 100,
}}
value={options.thumb_encode_quality}
onChange={handleChange(
"thumb_encode_quality"
)}
required
/>
<FormHelperText id="component-helper-text">
{t("thumbQualityDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
{options.thumb_max_src_size !== "" && (
<SizeInput
value={options.thumb_max_src_size}
onChange={handleChange(
"thumb_max_src_size"
)}
required
min={0}
max={2147483647}
label={t("thumbMaxSize")}
/>
)}
<FormHelperText id="component-helper-text">
{t("thumbMaxSizeDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.thumb_gc_after_gen ===
"1"
}
onChange={handleCheckChange(
"thumb_gc_after_gen"
)}
/>
}
label={t("thumbGC")}
/>
</FormControl>
</div>
</div>
<Typography variant="subtitle1" gutterBottom>
{t("generators")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<ThumbGenerators
options={options}
setOptions={setOptions}
/>
</div>
</div>
<Typography variant="subtitle1" gutterBottom>
{t("generatorProxy")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Alert severity="info">
{t("generatorProxyWarning")}
</Alert>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.thumb_proxy_enabled ===
"1"
}
onChange={handleCheckChange(
"thumb_proxy_enabled"
)}
/>
}
label={t("enableThumbProxy")}
/>
</FormControl>
</div>
{options.thumb_proxy_enabled === "1" && (
<>
<div className={classes.form}>
<PolicySelector
value={options.thumb_proxy_policy}
onChange={handleChange(
"thumb_proxy_policy"
)}
filter={(t) => t.Type !== "local"}
label={t("proxyPolicyList")}
helperText={t("proxyPolicyListDes")}
/>
</div>
</>
)}
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("save")}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,450 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
import { useDispatch } from "react-redux";
import Dialog from "@material-ui/core/Dialog";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import TextField from "@material-ui/core/TextField";
import DialogActions from "@material-ui/core/DialogActions";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
buttonMargin: {
marginLeft: 8,
},
}));
export default function Mail() {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const { t: tVas } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tGlobal } = useTranslation("common");
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [test, setTest] = useState(false);
const [tesInput, setTestInput] = useState("");
const [options, setOptions] = useState({
fromName: "",
fromAdress: "",
smtpHost: "",
smtpPort: "",
replyTo: "",
smtpUser: "",
smtpPass: "",
smtpEncryption: "",
mail_keepalive: "30",
over_used_template: "",
mail_activation_template: "",
mail_reset_pwd_template: "",
});
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const handleCheckChange = (name) => (event) => {
let value = event.target.value;
if (event.target.checked !== undefined) {
value = event.target.checked ? "1" : "0";
}
setOptions({
...options,
[name]: value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const sendTestMail = () => {
setLoading(true);
API.post("/admin/test/mail", {
to: tesInput,
})
.then(() => {
ToggleSnackbar("top", "right", t("testMailSent"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const reload = () => {
API.get("/admin/reload/email")
// eslint-disable-next-line @typescript-eslint/no-empty-function
.then(() => {})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
.then(() => {});
};
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", t("saved"), "success");
reload();
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<Dialog
open={test}
onClose={() => setTest(false)}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">
{t("testSMTPSettings")}
</DialogTitle>
<DialogContent>
<DialogContentText>
<Typography>{t("testSMTPTooltip")}</Typography>
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="name"
label={t("recipient")}
value={tesInput}
onChange={(e) => setTestInput(e.target.value)}
type="email"
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setTest(false)} color="default">
{tGlobal("cancel")}
</Button>
<Button
onClick={() => sendTestMail()}
disabled={loading}
color="primary"
>
{t("send")}
</Button>
</DialogActions>
</Dialog>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("smtp")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("senderName")}
</InputLabel>
<Input
value={options.fromName}
onChange={handleChange("fromName")}
required
/>
<FormHelperText id="component-helper-text">
{t("senderNameDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("senderAddress")}
</InputLabel>
<Input
type={"email"}
required
value={options.fromAdress}
onChange={handleChange("fromAdress")}
/>
<FormHelperText id="component-helper-text">
{t("senderAddressDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("smtpServer")}
</InputLabel>
<Input
value={options.smtpHost}
onChange={handleChange("smtpHost")}
required
/>
<FormHelperText id="component-helper-text">
{t("smtpServerDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("smtpPort")}
</InputLabel>
<Input
inputProps={{ min: 1, step: 1 }}
type={"number"}
value={options.smtpPort}
onChange={handleChange("smtpPort")}
required
/>
<FormHelperText id="component-helper-text">
{t("smtpPortDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("smtpUsername")}
</InputLabel>
<Input
value={options.smtpUser}
onChange={handleChange("smtpUser")}
required
/>
<FormHelperText id="component-helper-text">
{t("smtpUsernameDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("smtpPassword")}
</InputLabel>
<Input
type={"password"}
value={options.smtpPass}
onChange={handleChange("smtpPass")}
required
/>
<FormHelperText id="component-helper-text">
{t("smtpPasswordDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("replyToAddress")}
</InputLabel>
<Input
value={options.replyTo}
onChange={handleChange("replyTo")}
required
/>
<FormHelperText id="component-helper-text">
{t("replyToAddressDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.smtpEncryption === "1"
}
onChange={handleCheckChange(
"smtpEncryption"
)}
/>
}
label={t("enforceSSL")}
/>
<FormHelperText id="component-helper-text">
{t("enforceSSLDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("smtpTTL")}
</InputLabel>
<Input
inputProps={{ min: 1, step: 1 }}
type={"number"}
value={options.mail_keepalive}
onChange={handleChange("mail_keepalive")}
required
/>
<FormHelperText id="component-helper-text">
{t("smtpTTLDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("emailTemplates")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("activateNewUser")}
</InputLabel>
<Input
value={options.mail_activation_template}
onChange={handleChange(
"mail_activation_template"
)}
multiline
rowsMax="10"
required
/>
<FormHelperText id="component-helper-text">
{t("activateNewUserDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tVas("overuseReminder")}
</InputLabel>
<Input
value={options.over_used_template}
onChange={handleChange(
"over_used_template"
)}
multiline
rowsMax="10"
required
/>
<FormHelperText id="component-helper-text">
{tVas("overuseReminderDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("resetPassword")}
</InputLabel>
<Input
value={options.mail_reset_pwd_template}
onChange={handleChange(
"mail_reset_pwd_template"
)}
multiline
rowsMax="10"
required
/>
<FormHelperText id="component-helper-text">
{t("resetPasswordDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("save")}
</Button>
{" "}
<Button
className={classes.buttonMargin}
variant={"outlined"}
color={"primary"}
onClick={() => setTest(true)}
>
{t("sendTestEmail")}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,517 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import { toggleSnackbar } from "../../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
import InputAdornment from "@material-ui/core/InputAdornment";
import { green } from "@material-ui/core/colors";
import { Cancel, CheckCircle, Sync } from "@material-ui/icons";
import IconButton from "@material-ui/core/IconButton";
import { Tooltip } from "@material-ui/core";
import Alert from "@material-ui/lab/Alert";
import Link from "@material-ui/core/Link";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function SiteInformation() {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const { t: tVas } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tGlobal } = useTranslation("dashboard");
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
siteURL: "",
siteName: "",
siteTitle: "",
siteKeywords: "",
siteDes: "",
siteScript: "",
siteNotice: "",
pwa_small_icon: "",
pwa_medium_icon: "",
pwa_large_icon: "",
pwa_display: "",
pwa_theme_color: "",
pwa_background_color: "",
vol_content: "",
show_app_promotion: "0",
app_feedback_link: "",
app_forum_link: "",
});
const vol = useMemo(() => {
if (options.vol_content) {
const volJson = atob(options.vol_content);
return JSON.parse(volJson);
}
}, [options]);
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const handleOptionChange = (name) => (event) => {
let value = event.target.value;
if (event.target.checked !== undefined) {
value = event.target.checked ? "1" : "0";
}
setOptions({
...options,
[name]: value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const refresh = () =>
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
useEffect(() => {
refresh();
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", t("saved"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const syncVol = () => {
setLoading(true);
API.get("/admin/vol/sync")
.then(() => {
refresh();
ToggleSnackbar("top", "right", tVas("volSynced"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("basicInformation")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("mainTitle")}
</InputLabel>
<Input
value={options.siteName}
onChange={handleChange("siteName")}
required
/>
<FormHelperText id="component-helper-text">
{t("mainTitleDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("subTitle")}
</InputLabel>
<Input
value={options.siteTitle}
onChange={handleChange("siteTitle")}
/>
<FormHelperText id="component-helper-text">
{t("subTitleDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("siteKeywords")}
</InputLabel>
<Input
value={options.siteKeywords}
onChange={handleChange("siteKeywords")}
/>
<FormHelperText id="component-helper-text">
{t("siteKeywordsDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("siteDescription")}
</InputLabel>
<Input
value={options.siteDes}
onChange={handleChange("siteDes")}
/>
<FormHelperText id="component-helper-text">
{t("siteDescriptionDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("siteURL")}
</InputLabel>
<Input
type={"url"}
value={options.siteURL}
onChange={handleChange("siteURL")}
required
/>
<FormHelperText id="component-helper-text">
{t("siteURLDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("customFooterHTML")}
</InputLabel>
<Input
multiline
value={options.siteScript}
onChange={handleChange("siteScript")}
/>
<FormHelperText id="component-helper-text">
{t("customFooterHTMLDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("announcement")}
</InputLabel>
<Input
placeholder={t("supportHTML")}
multiline
value={options.siteNotice}
onChange={handleChange("siteNotice")}
/>
<FormHelperText id="component-helper-text">
{t("announcementDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{tVas("mobileApp")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Alert severity="info">
<Typography variant="body2">
<Trans
ns={"dashboard"}
i18nKey={"vas.volPurchase"}
components={[
<Link
key={0}
href={
"https://cloudreve.org/login"
}
target={"_blank"}
/>,
<Link
key={1}
href={
"https://cloudreve.org/ios"
}
target={"_blank"}
/>,
]}
/>
</Typography>
</Alert>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tVas("iosVol")}
</InputLabel>
<Input
startAdornment={
<InputAdornment position="start">
{vol ? (
<CheckCircle
style={{
color: green[500],
}}
/>
) : (
<Cancel color={"error"} />
)}
</InputAdornment>
}
endAdornment={
<InputAdornment position="end">
<Tooltip
title={tVas("syncLicense")}
>
<IconButton
disabled={loading}
onClick={() => syncVol()}
aria-label="toggle password visibility"
>
<Sync />
</IconButton>
</Tooltip>
</InputAdornment>
}
readOnly
value={
vol ? vol.domain : tGlobal("share.none")
}
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.show_app_promotion ===
"1"
}
onChange={handleOptionChange(
"show_app_promotion"
)}
/>
}
label={tVas("showAppPromotion")}
/>
<FormHelperText id="component-helper-text">
{tVas("showAppPromotionDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tVas("appFeedback")}
</InputLabel>
<Input
value={options.app_feedback_link}
onChange={handleChange("app_feedback_link")}
/>
<FormHelperText id="component-helper-text">
{tVas("appLinkDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tVas("appForum")}
</InputLabel>
<Input
value={options.app_forum_link}
onChange={handleChange("app_forum_link")}
/>
<FormHelperText id="component-helper-text">
{tVas("appLinkDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("pwa")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("smallIcon")}
</InputLabel>
<Input
value={options.pwa_small_icon}
onChange={handleChange("pwa_small_icon")}
/>
<FormHelperText id="component-helper-text">
{t("smallIconDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("mediumIcon")}
</InputLabel>
<Input
value={options.pwa_medium_icon}
onChange={handleChange("pwa_medium_icon")}
/>
<FormHelperText id="component-helper-text">
{t("mediumIconDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("largeIcon")}
</InputLabel>
<Input
value={options.pwa_large_icon}
onChange={handleChange("pwa_large_icon")}
/>
<FormHelperText id="component-helper-text">
{t("largeIconDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("displayMode")}
</InputLabel>
<Select
value={options.pwa_display}
onChange={handleChange("pwa_display")}
>
<MenuItem value={"fullscreen"}>
fullscreen
</MenuItem>
<MenuItem value={"standalone"}>
standalone
</MenuItem>
<MenuItem value={"minimal-ui"}>
minimal-ui
</MenuItem>
<MenuItem value={"browser"}>
browser
</MenuItem>
</Select>
<FormHelperText id="component-helper-text">
{t("displayModeDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("themeColor")}
</InputLabel>
<Input
value={options.pwa_theme_color}
onChange={handleChange("pwa_theme_color")}
/>
<FormHelperText id="component-helper-text">
{t("themeColorDes")}
</FormHelperText>
</FormControl>
</div>
</div>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("backgroundColor")}
</InputLabel>
<Input
value={options.pwa_background_color}
onChange={handleChange(
"pwa_background_color"
)}
/>
<FormHelperText id="component-helper-text">
{t("backgroundColorDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("save")}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,465 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import TableHead from "@material-ui/core/TableHead";
import Table from "@material-ui/core/Table";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import TableBody from "@material-ui/core/TableBody";
import { Delete } from "@material-ui/icons";
import IconButton from "@material-ui/core/IconButton";
import TextField from "@material-ui/core/TextField";
import CreateTheme from "../Dialogs/CreateTheme";
import Alert from "@material-ui/lab/Alert";
import Link from "@material-ui/core/Link";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import FormHelperText from "@material-ui/core/FormHelperText";
import { toggleSnackbar } from "../../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 500,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
colorContainer: {
display: "flex",
},
colorDot: {
width: 20,
height: 20,
borderRadius: "50%",
marginLeft: 6,
},
}));
export default function Theme() {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const { t: tApp } = useTranslation();
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [theme, setTheme] = useState({});
const [options, setOptions] = useState({
themes: "{}",
defaultTheme: "",
home_view_method: "icon",
share_view_method: "list",
});
const [themeConfig, setThemeConfig] = useState({});
const [themeConfigError, setThemeConfigError] = useState({});
const [create, setCreate] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const deleteTheme = (color) => {
if (color === options.defaultTheme) {
ToggleSnackbar(
"top",
"right",
t("cannotDeleteDefaultTheme"),
"warning"
);
return;
}
if (Object.keys(theme).length <= 1) {
ToggleSnackbar("top", "right", t("keepAtLeastOneTheme"), "warning");
return;
}
const themeCopy = { ...theme };
delete themeCopy[color];
const resStr = JSON.stringify(themeCopy);
setOptions({
...options,
themes: resStr,
});
};
const addTheme = (newTheme) => {
setCreate(false);
if (theme[newTheme.palette.primary.main] !== undefined) {
ToggleSnackbar(
"top",
"right",
t("duplicatedThemePrimaryColor"),
"warning"
);
return;
}
const res = {
...theme,
[newTheme.palette.primary.main]: newTheme,
};
const resStr = JSON.stringify(res);
setOptions({
...options,
themes: resStr,
});
};
useEffect(() => {
const res = JSON.parse(options.themes);
const themeString = {};
Object.keys(res).map((k) => {
themeString[k] = JSON.stringify(res[k]);
});
setTheme(res);
setThemeConfig(themeString);
}, [options.themes]);
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", t("saved"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("themes")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>{t("colors")}</TableCell>
<TableCell>
{t("themeConfig")}
</TableCell>
<TableCell>{t("actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.keys(theme).map((k) => (
<TableRow key={k}>
<TableCell
component="th"
scope="row"
>
<div
className={
classes.colorContainer
}
>
<div
style={{
backgroundColor:
theme[k].palette
.primary
.main,
}}
className={
classes.colorDot
}
/>
<div
style={{
backgroundColor:
theme[k].palette
.secondary
.main,
}}
className={
classes.colorDot
}
/>
</div>
</TableCell>
<TableCell>
<TextField
error={themeConfigError[k]}
helperText={
themeConfigError[k] &&
t("wrongFormat")
}
fullWidth
multiline
onChange={(e) => {
setThemeConfig({
...themeConfig,
[k]: e.target.value,
});
}}
onBlur={(e) => {
try {
const res = JSON.parse(
e.target.value
);
if (
!(
"palette" in
res
) ||
!(
"primary" in
res.palette
) ||
!(
"main" in
res.palette
.primary
) ||
!(
"secondary" in
res.palette
) ||
!(
"main" in
res.palette
.secondary
)
) {
throw "error";
}
setTheme({
...theme,
[k]: res,
});
} catch (e) {
setThemeConfigError(
{
...themeConfigError,
[k]: true,
}
);
return;
}
setThemeConfigError({
...themeConfigError,
[k]: false,
});
}}
value={themeConfig[k]}
/>
</TableCell>
<TableCell>
<IconButton
onClick={() =>
deleteTheme(k)
}
>
<Delete />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div>
<Button
variant="outlined"
color="primary"
style={{ marginTop: 8 }}
onClick={() => setCreate(true)}
>
{t("createNewTheme")}
</Button>
</div>
<Alert severity="info" style={{ marginTop: 8 }}>
<Typography variant="body2">
<Trans
i18nKey={"settings.themeConfigDes"}
ns={"dashboard"}
components={[
<Link
key={0}
href={t("themeConfigDoc")}
target={"_blank"}
/>,
]}
/>
</Typography>
</Alert>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("defaultTheme")}
</InputLabel>
<Select
value={options.defaultTheme}
onChange={handleChange("defaultTheme")}
>
{Object.keys(theme).map((k) => (
<MenuItem key={k} value={k}>
<div
className={
classes.colorContainer
}
>
<div
style={{
backgroundColor:
theme[k].palette
.primary.main,
}}
className={classes.colorDot}
/>
<div
style={{
backgroundColor:
theme[k].palette
.secondary.main,
}}
className={classes.colorDot}
/>
</div>
</MenuItem>
))}
</Select>
<FormHelperText id="component-helper-text">
{t("defaultThemeDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("appearance")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("personalFileListView")}
</InputLabel>
<Select
value={options.home_view_method}
onChange={handleChange("home_view_method")}
required
>
<MenuItem value={"icon"}>
{tApp("fileManager.gridViewLarge")}
</MenuItem>
<MenuItem value={"smallIcon"}>
{tApp("fileManager.gridViewSmall")}
</MenuItem>
<MenuItem value={"list"}>
{tApp("fileManager.listView")}
</MenuItem>
</Select>
<FormHelperText id="component-helper-text">
{t("personalFileListViewDes")}
</FormHelperText>
</FormControl>
</div>
</div>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("sharedFileListView")}
</InputLabel>
<Select
value={options.share_view_method}
onChange={handleChange("share_view_method")}
required
>
<MenuItem value={"icon"}>
{tApp("fileManager.gridViewLarge")}
</MenuItem>
<MenuItem value={"smallIcon"}>
{tApp("fileManager.gridViewSmall")}
</MenuItem>
<MenuItem value={"list"}>
{tApp("fileManager.listView")}
</MenuItem>
</Select>
<FormHelperText id="component-helper-text">
{t("sharedFileListViewDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("save")}
</Button>
</div>
</form>
<CreateTheme
onSubmit={addTheme}
open={create}
onClose={() => setCreate(false)}
/>
</div>
);
}

View File

@@ -0,0 +1,248 @@
import React, { useCallback, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Accordion from "@material-ui/core/Accordion";
import AccordionSummary from "@material-ui/core/AccordionSummary";
import AccordionDetails from "@material-ui/core/AccordionDetails";
import Checkbox from "@material-ui/core/Checkbox";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Typography from "@material-ui/core/Typography";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../redux/explorer";
import FormHelperText from "@material-ui/core/FormHelperText";
import FormControl from "@material-ui/core/FormControl";
import { Button, TextField } from "@material-ui/core";
import InputAdornment from "@material-ui/core/InputAdornment";
import API from "../../../middleware/Api";
const useStyles = makeStyles((theme) => ({
root: {
width: "100%",
},
secondaryHeading: {
fontSize: theme.typography.pxToRem(15),
color: theme.palette.text.secondary,
},
column: {
flexBasis: "33.33%",
},
details: {
display: "block",
},
}));
const generators = [
{
name: "policyBuiltin",
des: "policyBuiltinDes",
readOnly: true,
},
{
name: "libreOffice",
des: "libreOfficeDes",
enableFlag: "thumb_libreoffice_enabled",
executableSetting: "thumb_libreoffice_path",
inputs: [
{
name: "thumb_libreoffice_exts",
label: "generatorExts",
des: "generatorExtsDes",
},
],
},
{
name: "vips",
des: "vipsDes",
enableFlag: "thumb_vips_enabled",
executableSetting: "thumb_vips_path",
inputs: [
{
name: "thumb_vips_exts",
label: "generatorExts",
des: "generatorExtsDes",
},
],
},
{
name: "ffmpeg",
des: "ffmpegDes",
enableFlag: "thumb_ffmpeg_enabled",
executableSetting: "thumb_ffmpeg_path",
inputs: [
{
name: "thumb_ffmpeg_exts",
label: "generatorExts",
des: "generatorExtsDes",
},
{
name: "thumb_ffmpeg_seek",
label: "ffmpegSeek",
des: "ffmpegSeekDes",
required: true,
},
],
},
{
name: "cloudreveBuiltin",
des: "cloudreveBuiltinDes",
enableFlag: "thumb_builtin_enabled",
},
];
export default function ThumbGenerators({ options, setOptions }) {
const classes = useStyles();
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const testExecutable = (name, executable) => {
setLoading(true);
API.post("/admin/test/thumb", {
name,
executable,
})
.then((response) => {
ToggleSnackbar(
"top",
"right",
t("executableTestSuccess", { version: response.data }),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleEnableChange = (name) => (event) => {
const newOpts = {
...options,
[name]: event.target.checked ? "1" : "0",
};
setOptions(newOpts);
if (
newOpts["thumb_libreoffice_enabled"] === "1" &&
newOpts["thumb_builtin_enabled"] === "0" &&
newOpts["thumb_vips_enabled"] === "0"
) {
ToggleSnackbar(
"top",
"center",
t("thumbDependencyWarning"),
"warning"
);
}
};
return (
<div className={classes.root}>
{generators.map((generator) => (
<Accordion key={generator.name}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-label="Expand"
aria-controls="additional-actions1-content"
id="additional-actions1-header"
>
<FormControlLabel
aria-label="Acknowledge"
onClick={(event) => event.stopPropagation()}
onFocus={(event) => event.stopPropagation()}
control={
<Checkbox
checked={
generator.readOnly ||
options[generator.enableFlag] === "1"
}
onChange={handleEnableChange(
generator.enableFlag
)}
/>
}
label={t(generator.name)}
disabled={generator.readOnly}
/>
</AccordionSummary>
<AccordionDetails className={classes.details}>
<Typography color="textSecondary">
{t(generator.des)}
</Typography>
{generator.executableSetting && (
<FormControl margin="normal" fullWidth>
<TextField
label={t("executable")}
variant="outlined"
value={options[generator.executableSetting]}
onChange={handleChange(
generator.executableSetting
)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Button
disabled={loading}
onClick={() =>
testExecutable(
generator.name,
options[
generator
.executableSetting
]
)
}
color="primary"
>
{t("executableTest")}
</Button>
</InputAdornment>
),
}}
required
/>
<FormHelperText id="component-helper-text">
{t("executableDes")}
</FormHelperText>
</FormControl>
)}
{generator.inputs &&
generator.inputs.map((input) => (
<FormControl
key={input.name}
margin="normal"
fullWidth
>
<TextField
label={t(input.label)}
variant="outlined"
value={options[input.name]}
onChange={handleChange(input.name)}
required={!!input.required}
/>
<FormHelperText id="component-helper-text">
{t(input.des)}
</FormHelperText>
</FormControl>
))}
</AccordionDetails>
</Accordion>
))}
</div>
);
}

View File

@@ -0,0 +1,407 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import SizeInput from "../Common/SizeInput";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function UploadDownload() {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
max_worker_num: "1",
max_parallel_transfer: "1",
temp_path: "",
chunk_retries: "0",
archive_timeout: "0",
download_timeout: "0",
preview_timeout: "0",
doc_preview_timeout: "0",
upload_credential_timeout: "0",
upload_session_timeout: "0",
slave_api_timeout: "0",
onedrive_monitor_timeout: "0",
share_download_session_timeout: "0",
onedrive_callback_check: "0",
reset_after_upload_failed: "0",
onedrive_source_timeout: "0",
slave_node_retry: "0",
slave_ping_interval: "0",
slave_recover_interval: "0",
slave_transfer_timeout: "0",
use_temp_chunk_buffer: "1",
public_resource_maxage: "0",
});
const handleCheckChange = (name) => (event) => {
const value = event.target.checked ? "1" : "0";
setOptions({
...options,
[name]: value,
});
};
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", t("saved"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("transportation")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("workerNum")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.max_worker_num}
onChange={handleChange("max_worker_num")}
required
/>
<FormHelperText id="component-helper-text">
{t("workerNumDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("transitParallelNum")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.max_parallel_transfer}
onChange={handleChange(
"max_parallel_transfer"
)}
required
/>
<FormHelperText id="component-helper-text">
{t("transitParallelNumDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("tempFolder")}
</InputLabel>
<Input
value={options.temp_path}
onChange={handleChange("temp_path")}
required
/>
<FormHelperText id="component-helper-text">
{t("tempFolderDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("failedChunkRetry")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={options.chunk_retries}
onChange={handleChange("chunk_retries")}
required
/>
<FormHelperText id="component-helper-text">
{t("failedChunkRetryDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.use_temp_chunk_buffer ===
"1"
}
onChange={handleCheckChange(
"use_temp_chunk_buffer"
)}
/>
}
label={t("cacheChunks")}
/>
<FormHelperText id="component-helper-text">
{t("cacheChunksDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.reset_after_upload_failed ===
"1"
}
onChange={handleCheckChange(
"reset_after_upload_failed"
)}
/>
}
label={t("resetConnection")}
/>
<FormHelperText id="component-helper-text">
{t("resetConnectionDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("expirationDuration")}
</Typography>
<div className={classes.formContainer}>
{[
{
name: "batchDownload",
field: "archive_timeout",
},
{
name: "downloadSession",
field: "download_timeout",
},
{
name: "previewURL",
field: "preview_timeout",
},
{
name: "docPreviewURL",
field: "doc_preview_timeout",
},
{
name: "staticResourceCache",
field: "public_resource_maxage",
des: "staticResourceCacheDes",
},
{
name: "uploadSession",
field: "upload_session_timeout",
des: "uploadSessionDes",
},
{
name: "downloadSessionForShared",
field: "share_download_session_timeout",
des: "downloadSessionForSharedDes",
},
{
name: "onedriveMonitorInterval",
field: "onedrive_monitor_timeout",
des: "onedriveMonitorIntervalDes",
},
{
name: "onedriveCallbackTolerance",
field: "onedrive_callback_check",
des: "onedriveCallbackToleranceDes",
},
{
name: "onedriveDownloadURLCache",
field: "onedrive_source_timeout",
des: "onedriveDownloadURLCacheDes",
},
].map((input) => (
<div key={input.name} className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t(input.name)}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options[input.field]}
onChange={handleChange(input.field)}
required
/>
{input.des && (
<FormHelperText id="component-helper-text">
{t(input.des)}
</FormHelperText>
)}
</FormControl>
</div>
))}
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("nodesCommunication")}
</Typography>
<div className={classes.formContainer}>
{[
{
name: "slaveAPIExpiration",
field: "slave_api_timeout",
des: "slaveAPIExpirationDes",
},
{
name: "heartbeatInterval",
field: "slave_ping_interval",
des: "heartbeatIntervalDes",
},
{
name: "heartbeatFailThreshold",
field: "slave_node_retry",
des: "heartbeatFailThresholdDes",
},
{
name: "heartbeatRecoverModeInterval",
field: "slave_recover_interval",
des: "heartbeatRecoverModeIntervalDes",
},
{
name: "slaveTransitExpiration",
field: "slave_transfer_timeout",
des: "slaveTransitExpirationDes",
},
].map((input) => (
<div key={input.name} className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t(input.name)}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options[input.field]}
onChange={handleChange(input.field)}
required
/>
<FormHelperText id="component-helper-text">
{t(input.des)}
</FormHelperText>
</FormControl>
</div>
))}
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("save")}
</Button>
</div>
</form>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,532 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import IconButton from "@material-ui/core/IconButton";
import { Delete, FilterList } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Checkbox from "@material-ui/core/Checkbox";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { lighten } from "@material-ui/core";
import Link from "@material-ui/core/Link";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import Badge from "@material-ui/core/Badge";
import ShareFilter from "../Dialogs/ShareFilter";
import { formatLocalTime } from "../../../utils/datetime";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function Share() {
const { t } = useTranslation("dashboard", { keyPrefix: "share" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [shares, setShares] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [users, setUsers] = useState({});
const [ids, setIds] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/share/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setUsers(response.data.users);
setIds(response.data.ids);
setShares(response.data.items);
setTotal(response.data.total);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deletePolicy = (id) => {
setLoading(true);
API.post("/admin/share/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("deleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/share/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("deleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = shares.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<ShareFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<div className={classes.headerRight}>
<Tooltip title={tDashboard("user.filter")}>
<IconButton
style={{ marginRight: 8 }}
onClick={() => setFilterDialog(true)}
>
<Badge
color="secondary"
variant="dot"
invisible={
Object.keys(search).length === 0 &&
Object.keys(filter).length === 0
}
>
<FilterList />
</Badge>
</IconButton>
</Tooltip>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
{tDashboard("user.selectedObjects", {
num: selected.length,
})}
</Typography>
<Tooltip title={tDashboard("node.delete")}>
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < shares.length
}
checked={
shares.length > 0 &&
selected.length === shares.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 10 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 200 }}>
<TableSortLabel
active={orderBy[0] === "source_name"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"source_name",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("objectName")}
{orderBy[0] === "source_name" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 70 }}>
{tDashboard("policy.type")}
</TableCell>
<TableCell
style={{ minWidth: 100 }}
align={"right"}
>
<TableSortLabel
active={orderBy[0] === "views"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"views",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("views")}
{orderBy[0] === "views" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell
style={{ minWidth: 100 }}
align={"right"}
>
<TableSortLabel
active={orderBy[0] === "downloads"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"downloads",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("downloads")}
{orderBy[0] === "downloads" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell
style={{ minWidth: 100 }}
align={"right"}
>
<TableSortLabel
active={orderBy[0] === "score"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"score",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("price")}
{orderBy[0] === "score" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 120 }}>
{t("autoExpire")}
</TableCell>
<TableCell style={{ minWidth: 120 }}>
{t("owner")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{t("createdAt")}
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{tDashboard("policy.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{shares.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell
style={{ wordBreak: "break-all" }}
>
<Link
target={"_blank"}
color="inherit"
href={
"/s/" +
ids[row.ID] +
(row.Password === ""
? ""
: "?password=" +
row.Password)
}
>
{row.SourceName}
</Link>
</TableCell>
<TableCell>
{row.Password === ""
? t("public")
: t("private")}
</TableCell>
<TableCell align={"right"}>
{row.Views}
</TableCell>
<TableCell align={"right"}>
{row.Downloads}
</TableCell>
<TableCell align={"right"}>
{row.Score}
</TableCell>
<TableCell>
{row.RemainDownloads > -1 &&
t("afterNDownloads", {
num: row.RemainDownloads,
})}
{row.RemainDownloads === -1 &&
t("none")}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" + row.UserID
}
>
{users[row.UserID]
? users[row.UserID].Nick
: tDashboard(
"file.unknownUploader"
)}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(row.CreatedAt)}
</TableCell>
<TableCell>
<Tooltip
title={tDashboard("node.delete")}
>
<IconButton
disabled={loading}
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,83 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Link,
} from "@material-ui/core";
import { Link as RouterLink } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({}));
export default function Aria2Helper(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "task" });
const { t: tCommon } = useTranslation("common");
const classes = useStyles();
return (
<Dialog
open={props.open}
onClose={props.onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{t("howToConfigAria2")}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{t("aria2Des")}
<ul>
<li>
<Trans
ns={"dashboard"}
i18nKey={"task.masterAria2Des"}
components={[
<Link
component={RouterLink}
to={"/admin/node/edit/1"}
key={0}
/>,
]}
/>
</li>
<li>
<Trans
ns={"dashboard"}
i18nKey={"task.slaveAria2Des"}
components={[
<Link
component={RouterLink}
to={"/admin/node/add"}
key={0}
/>,
]}
/>
</li>
</ul>
<Trans
ns={"dashboard"}
i18nKey={"task.editGroupDes"}
components={[
<Link
component={RouterLink}
to={"/admin/group"}
key={0}
/>,
]}
/>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} color="primary" autoFocus>
{tCommon("close")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,444 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import IconButton from "@material-ui/core/IconButton";
import { Delete } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Checkbox from "@material-ui/core/Checkbox";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { lighten } from "@material-ui/core";
import Link from "@material-ui/core/Link";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import ShareFilter from "../Dialogs/ShareFilter";
import { sizeToString } from "../../../utils";
import { formatLocalTime } from "../../../utils/datetime";
import Aria2Helper from "./Aria2Helper";
import HelpIcon from "@material-ui/icons/Help";
import { Link as RouterLink } from "react-router-dom";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function Download() {
const { t } = useTranslation("dashboard", { keyPrefix: "task" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [downloads, setDownloads] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [users, setUsers] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const [helperOpen, setHelperOpen] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/download/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setUsers(response.data.users);
setDownloads(response.data.items);
setTotal(response.data.total);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deletePolicy = (id) => {
setLoading(true);
API.post("/admin/download/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("taskDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/download/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("taskDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = downloads.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<Aria2Helper
open={helperOpen}
onClose={() => setHelperOpen(false)}
/>
<ShareFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
<div className={classes.headerRight}>
<Button
color={"primary"}
onClick={() => setHelperOpen(true)}
>
<HelpIcon /> {" "}
{t("howToConfigAria2")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
{tDashboard("user.selectedObjects", {
num: selected.length,
})}
</Typography>
<Tooltip title={tDashboard("policy.delete")}>
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < downloads.length
}
checked={
downloads.length > 0 &&
selected.length === downloads.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 10 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 130 }}>
{t("srcURL")}
</TableCell>
<TableCell style={{ minWidth: 90 }}>
{tDashboard("user.status")}
</TableCell>
<TableCell style={{ minWidth: 90 }}>
{t("node")}
</TableCell>
<TableCell
style={{ minWidth: 150 }}
align={"right"}
>
<TableSortLabel
active={orderBy[0] === "total_size"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"total_size",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{tDashboard("file.size")}
{orderBy[0] === "total_size" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{t("createdBy")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{tDashboard("file.createdAt")}
</TableCell>
<TableCell style={{ minWidth: 80 }}>
{tDashboard("policy.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{downloads.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell
style={{ wordBreak: "break-all" }}
>
{row.Source}
</TableCell>
<TableCell>
{row.Status === 0 && t("ready")}
{row.Status === 1 && t("downloading")}
{row.Status === 2 && t("paused")}
{row.Status === 3 && t("error")}
{row.Status === 4 && t("finished")}
{row.Status === 5 && t("canceled")}
{row.Status === 6 && t("unknown")}
{row.Status === 7 && t("seeding")}
</TableCell>
<TableCell>
{row.NodeID <= 1 && (
<Link
component={RouterLink}
to={"/admin/node/edit/1"}
>
{tDashboard("node.master")}
</Link>
)}
{row.NodeID > 1 && (
<Link
component={RouterLink}
to={
"/admin/node/edit/" +
row.NodeID
}
>
{tDashboard("node.slave")}#
{row.NodeID}
</Link>
)}
</TableCell>
<TableCell align={"right"}>
{sizeToString(row.TotalSize)}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" + row.UserID
}
>
{users[row.UserID]
? users[row.UserID].Nick
: tDashboard(
"file.unknownUploader"
)}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(row.CreatedAt)}
</TableCell>
<TableCell>
<Tooltip
title={tDashboard("policy.delete")}
>
<IconButton
disabled={loading}
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,389 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import IconButton from "@material-ui/core/IconButton";
import { Delete } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Checkbox from "@material-ui/core/Checkbox";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { lighten } from "@material-ui/core";
import Link from "@material-ui/core/Link";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import ShareFilter from "../Dialogs/ShareFilter";
import { getTaskProgress, getTaskStatus, getTaskType } from "../../../config";
import { formatLocalTime } from "../../../utils/datetime";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function Task() {
const { t } = useTranslation("dashboard", { keyPrefix: "task" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [tasks, setTasks] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [users, setUsers] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/task/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setUsers(response.data.users);
setTasks(response.data.items);
setTotal(response.data.total);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deletePolicy = (id) => {
setLoading(true);
API.post("/admin/task/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("taskDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/task/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("taskDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = tasks.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const getError = (error) => {
if (error === "") {
return "-";
}
try {
const res = JSON.parse(error);
return res.msg;
} catch (e) {
return t("unknown");
}
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<ShareFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<div className={classes.headerRight}>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
{tDashboard("user.selectedObjects", {
num: selected.length,
})}
</Typography>
<Tooltip title={tDashboard("policy.delete")}>
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < tasks.length
}
checked={
tasks.length > 0 &&
selected.length === tasks.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 10 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 130 }}>
{tDashboard("policy.type")}
</TableCell>
<TableCell style={{ minWidth: 90 }}>
{tDashboard("user.status")}
</TableCell>
<TableCell style={{ minWidth: 90 }}>
{t("lastProgress")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{t("errorMsg")}
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{t("createdBy")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{tDashboard("file.createdAt")}
</TableCell>
<TableCell style={{ minWidth: 80 }}>
{tDashboard("policy.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{tasks.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell
style={{ wordBreak: "break-all" }}
>
{getTaskType(row.Type)}
</TableCell>
<TableCell>
{getTaskStatus(row.Status)}
</TableCell>
<TableCell>
{getTaskProgress(
row.Type,
row.Progress
)}
</TableCell>
<TableCell className={classes.noWrap}>
{getError(row.Error)}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" + row.UserID
}
>
{users[row.UserID]
? users[row.UserID].Nick
: t("unknown")}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(row.CreatedAt)}
</TableCell>
<TableCell>
<Tooltip
title={tDashboard("policy.delete")}
>
<IconButton
disabled={loading}
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import React, { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import UserForm from "./UserForm";
import { toggleSnackbar } from "../../../redux/explorer";
export default function EditUserPreload() {
const [user, setUser] = useState({});
const { id } = useParams();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
setUser({});
API.get("/admin/user/" + id)
.then((response) => {
// 整型转换
["Status", "GroupID", "Score"].forEach((v) => {
response.data[v] = response.data[v].toString();
});
setUser(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, [id]);
return <div>{user.ID !== undefined && <UserForm user={user} />}</div>;
}

View File

@@ -0,0 +1,541 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles, useTheme } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import { sizeToString } from "../../../utils";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import { useHistory } from "react-router";
import IconButton from "@material-ui/core/IconButton";
import { Block, Delete, Edit, FilterList } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Checkbox from "@material-ui/core/Checkbox";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { lighten } from "@material-ui/core";
import Link from "@material-ui/core/Link";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import UserFilter from "../Dialogs/UserFilter";
import Badge from "@material-ui/core/Badge";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function Group() {
const { t } = useTranslation("dashboard", { keyPrefix: "user" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [users, setUsers] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const history = useHistory();
const theme = useTheme();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/user/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setUsers(response.data.items);
setTotal(response.data.total);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deletePolicy = (id) => {
setLoading(true);
API.post("/admin/user/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", "用户已删除", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/user/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("deleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const block = (id) => {
setLoading(true);
API.patch("/admin/user/ban/" + id)
.then((response) => {
setUsers(
users.map((v) => {
if (v.ID === id) {
const newUser = { ...v, Status: response.data };
return newUser;
}
return v;
})
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = users.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<UserFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<Button
style={{ alignSelf: "center" }}
color={"primary"}
onClick={() => history.push("/admin/user/add")}
variant={"contained"}
>
{t("new")}
</Button>
<div className={classes.headerRight}>
<Tooltip title={t("filter")}>
<IconButton
style={{ marginRight: 8 }}
onClick={() => setFilterDialog(true)}
>
<Badge
color="secondary"
variant="dot"
invisible={
Object.keys(search).length === 0 &&
Object.keys(filter).length === 0
}
>
<FilterList />
</Badge>
</IconButton>
</Tooltip>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
{t("selectedObjects", { num: selected.length })}
</Typography>
<Tooltip title={tDashboard("policy.delete")}>
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < users.length
}
checked={
users.length > 0 &&
selected.length === users.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 59 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{tDashboard("node.#")}
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 120 }}>
<TableSortLabel
active={orderBy[0] === "nick"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"nick",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("nick")}
{orderBy[0] === "nick" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 170 }}>
<TableSortLabel
active={orderBy[0] === "email"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"email",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("email")}
{orderBy[0] === "email" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 70 }}>
{t("group")}
</TableCell>
<TableCell style={{ minWidth: 50 }}>
{t("status")}
</TableCell>
<TableCell
align={"right"}
style={{ minWidth: 80 }}
>
<TableSortLabel
active={orderBy[0] === "storage"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"storage",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("usedStorage")}
{orderBy[0] === "storage" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{tDashboard("policy.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell>{row.Nick}</TableCell>
<TableCell>{row.Email}</TableCell>
<TableCell>
<Link
href={
"/admin/group/edit/" +
row.Group.ID
}
>
{row.Group.Name}
</Link>
</TableCell>
<TableCell>
{row.Status === 0 && (
<Typography
style={{
color: theme.palette.success
.main,
}}
variant={"body2"}
>
{t("active")}
</Typography>
)}
{row.Status === 1 && (
<Typography
color={"textSecondary"}
variant={"body2"}
>
{t("notActivated")}
</Typography>
)}
{row.Status === 2 && (
<Typography
color={"error"}
variant={"body2"}
>
{t("banned")}
</Typography>
)}
{row.Status === 3 && (
<Typography
color={"error"}
variant={"body2"}
>
{t("bannedBySys")}
</Typography>
)}
</TableCell>
<TableCell align={"right"}>
{sizeToString(row.Storage)}
</TableCell>
<TableCell>
<Tooltip
title={tDashboard("node.edit")}
>
<IconButton
onClick={() =>
history.push(
"/admin/user/edit/" +
row.ID
)
}
size={"small"}
>
<Edit />
</IconButton>
</Tooltip>
<Tooltip title={t("toggleBan")}>
<IconButton
disabled={loading}
onClick={() => block(row.ID)}
size={"small"}
>
<Block />
</IconButton>
</Tooltip>
<Tooltip
title={tDashboard("node.delete")}
>
<IconButton
disabled={loading}
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,273 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function UserForm(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "user" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [user, setUser] = useState(
props.user
? props.user
: {
ID: 0,
Email: "",
Nick: "",
Password: "", // 为空时只读
Status: "0", // 转换类型
GroupID: "2", // 转换类型
Score: "0", // 转换类型
TwoFactor: "",
}
);
const [groups, setGroups] = useState([]);
const history = useHistory();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.get("/admin/groups")
.then((response) => {
setGroups(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
const handleChange = (name) => (event) => {
setUser({
...user,
[name]: event.target.value,
});
};
const submit = (e) => {
e.preventDefault();
const userCopy = { ...user };
// 整型转换
["Status", "GroupID", "Score"].forEach((v) => {
userCopy[v] = parseInt(userCopy[v]);
});
setLoading(true);
API.post("/admin/user", {
user: userCopy,
password: userCopy.Password,
})
.then(() => {
history.push("/admin/user");
ToggleSnackbar(
"top",
"right",
props.user ? t("saved") : t("added"),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const groupSelections = useMemo(
() =>
groups.map((v) => {
if (v.ID === 3) {
return null;
}
return (
<MenuItem key={v.ID} value={v.ID.toString()}>
{v.Name}
</MenuItem>
);
}),
[groups]
);
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{user.ID === 0 && t("new")}
{user.ID !== 0 && t("editUser", { nick: user.Nick })}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("email")}
</InputLabel>
<Input
value={user.Email}
type={"email"}
onChange={handleChange("Email")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("nick")}
</InputLabel>
<Input
value={user.Nick}
onChange={handleChange("Nick")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("password")}
</InputLabel>
<Input
type={"password"}
value={user.Password}
onChange={handleChange("Password")}
required={user.ID === 0}
/>
<FormHelperText id="component-helper-text">
{user.ID !== 0 && t("passwordDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("group")}
</InputLabel>
<Select
value={user.GroupID}
onChange={handleChange("GroupID")}
required
>
{groupSelections}
</Select>
<FormHelperText id="component-helper-text">
{t("groupDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("status")}
</InputLabel>
<Select
value={user.Status}
onChange={handleChange("Status")}
required
>
<MenuItem value={"0"}>
{t("active")}
</MenuItem>
<MenuItem value={"1"}>
{t("notActivated")}
</MenuItem>
<MenuItem value={"2"}>
{t("banned")}
</MenuItem>
<MenuItem value={"3"}>
{t("bannedBySys")}
</MenuItem>
</Select>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tDashboard("vas.credits")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={user.Score}
onChange={handleChange("Score")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("2FASecret")}
</InputLabel>
<Input
value={user.TwoFactor}
onChange={handleChange("TwoFactor")}
/>
</FormControl>
<FormHelperText id="component-helper-text">
{t("2FASecretDes")}
</FormHelperText>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{tDashboard("settings.save")}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,42 @@
import { Link, makeStyles } from "@material-ui/core";
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { useLocation } from "react-router";
import pageHelper from "../../utils/page";
const useStyles = makeStyles(() => ({
icp: {
padding: "8px 24px",
position: "absolute",
bottom: 0,
},
}));
export const ICPFooter = () => {
const siteICPId = useSelector((state) => state.siteConfig.siteICPId);
const classes = useStyles();
const location = useLocation();
const [show, setShow] = useState(true);
useEffect(() => {
// 只在分享和登录界面显示
const isSharePage = pageHelper.isSharePage(location.pathname);
const isLoginPage = pageHelper.isLoginPage(location.pathname);
setShow(siteICPId && (isSharePage || isLoginPage));
}, [siteICPId, location]);
if (!show) {
return <></>;
}
return (
<div className={classes.icp}>
<Link
href="https://beian.miit.gov.cn/"
rel="noopener noreferrer"
target="_blank"
>
{siteICPId}
</Link>
</div>
);
};

View File

@@ -0,0 +1,151 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import classNames from "classnames";
import ErrorIcon from "@material-ui/icons/Error";
import InfoIcon from "@material-ui/icons/Info";
import CloseIcon from "@material-ui/icons/Close";
import CheckCircleIcon from "@material-ui/icons/CheckCircle";
import WarningIcon from "@material-ui/icons/Warning";
import {
IconButton,
Snackbar,
SnackbarContent,
withStyles,
} from "@material-ui/core";
const mapStateToProps = (state) => {
return {
snackbar: state.viewUpdate.snackbar,
};
};
const mapDispatchToProps = () => {
return {};
};
const variantIcon = {
success: CheckCircleIcon,
warning: WarningIcon,
error: ErrorIcon,
info: InfoIcon,
};
const styles1 = (theme) => ({
success: {
backgroundColor: theme.palette.success.main,
},
error: {
backgroundColor: theme.palette.error.dark,
},
info: {
backgroundColor: theme.palette.info.main,
},
warning: {
backgroundColor: theme.palette.warning.main,
},
icon: {
fontSize: 20,
},
iconVariant: {
opacity: 0.9,
marginRight: theme.spacing(1),
},
message: {
display: "flex",
alignItems: "center",
},
});
function MySnackbarContent(props) {
const { classes, className, message, onClose, variant, ...other } = props;
const Icon = variantIcon[variant];
return (
<SnackbarContent
className={classNames(classes[variant], className)}
aria-describedby="client-snackbar"
message={
<span id="client-snackbar" className={classes.message}>
<Icon
className={classNames(
classes.icon,
classes.iconVariant
)}
/>
{message}
</span>
}
action={[
<IconButton
key="close"
aria-label="Close"
color="inherit"
className={classes.close}
onClick={onClose}
>
<CloseIcon className={classes.icon} />
</IconButton>,
]}
{...other}
/>
);
}
MySnackbarContent.propTypes = {
classes: PropTypes.object.isRequired,
className: PropTypes.string,
message: PropTypes.node,
onClose: PropTypes.func,
variant: PropTypes.oneOf(["alert", "success", "warning", "error", "info"])
.isRequired,
};
const MySnackbarContentWrapper = withStyles(styles1)(MySnackbarContent);
const styles = (theme) => ({
margin: {
margin: theme.spacing(1),
},
});
class SnackbarCompoment extends Component {
state = {
open: false,
};
UNSAFE_componentWillReceiveProps = (nextProps) => {
if (nextProps.snackbar.toggle !== this.props.snackbar.toggle) {
this.setState({ open: true });
}
};
handleClose = () => {
this.setState({ open: false });
};
render() {
return (
<Snackbar
anchorOrigin={{
vertical: this.props.snackbar.vertical,
horizontal: this.props.snackbar.horizontal,
}}
open={this.state.open}
autoHideDuration={6000}
onClose={this.handleClose}
>
<MySnackbarContentWrapper
onClose={this.handleClose}
variant={this.props.snackbar.color}
message={this.props.snackbar.msg}
/>
</Snackbar>
);
}
}
const AlertBar = connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(SnackbarCompoment));
export default AlertBar;

View File

@@ -0,0 +1,45 @@
import React, { useCallback } from "react";
import { useDispatch } from "react-redux";
import AutoHidden from "./AutoHidden";
import { makeStyles } from "@material-ui/core";
import Fab from "@material-ui/core/Fab";
import { Add } from "@material-ui/icons";
import Modals from "../FileManager/Modals";
import { openRemoteDownloadDialog } from "../../redux/explorer";
const useStyles = makeStyles(() => ({
fab: {
margin: 0,
top: "auto",
right: 20,
bottom: 20,
left: "auto",
zIndex: 5,
position: "fixed",
},
}));
export default function RemoteDownloadButton() {
const classes = useStyles();
const dispatch = useDispatch();
const OpenRemoteDownloadDialog = useCallback(
() => dispatch(openRemoteDownloadDialog()),
[dispatch]
);
return (
<>
<Modals />
<AutoHidden enable>
<Fab
className={classes.fab}
color="secondary"
onClick={() => OpenRemoteDownloadDialog()}
>
<Add />
</Fab>
</AutoHidden>
</>
);
}

View File

@@ -0,0 +1,38 @@
import React, { useEffect, useState } from "react";
import Zoom from "@material-ui/core/Zoom";
function AutoHidden({ children, enable, hide = false, element = null }) {
const [hidden, setHidden] = useState(false);
let prev = window.scrollY;
let lastUpdate = window.scrollY;
const show = 50;
useEffect(() => {
const handleNavigation = (e) => {
const window = e.currentTarget;
const current = element ? element.scrollTop : window.scrollY;
if (prev > current) {
if (lastUpdate - current > show) {
lastUpdate = current;
setHidden(false);
}
} else if (prev < current) {
if (current - lastUpdate > show) {
lastUpdate = current;
setHidden(true);
}
}
prev = current;
};
if (enable) {
const target = element ? element : window;
target.addEventListener("scroll", (e) => handleNavigation(e));
}
// eslint-disable-next-line
}, [enable]);
return <Zoom in={!hidden && !hide}>{children}</Zoom>;
}
export default AutoHidden;

View File

@@ -0,0 +1,195 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Badge, CircularProgress, makeStyles } from "@material-ui/core";
import SpeedDial from "@material-ui/lab/SpeedDial";
import SpeedDialIcon from "@material-ui/lab/SpeedDialIcon";
import SpeedDialAction from "@material-ui/lab/SpeedDialAction";
import CreateNewFolderIcon from "@material-ui/icons/CreateNewFolder";
import PublishIcon from "@material-ui/icons/Publish";
import { useDispatch, useSelector } from "react-redux";
import AutoHidden from "./AutoHidden";
import statusHelper from "../../utils/page";
import Backdrop from "@material-ui/core/Backdrop";
import { FilePlus, FolderUpload } from "mdi-material-ui";
import { green } from "@material-ui/core/colors";
import { SelectType } from "../Uploader/core";
import {
openCreateFileDialog,
openCreateFolderDialog,
toggleSnackbar,
} from "../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles(() => ({
fab: {
margin: 0,
top: "auto",
right: 20,
bottom: 20,
left: "auto",
zIndex: 5,
position: "fixed",
},
badge: {
position: "absolute",
bottom: 26,
top: "auto",
zIndex: 9999,
right: 7,
},
"@global": {
".MuiSpeedDialAction-staticTooltipLabel": {
whiteSpace: "nowrap",
},
},
fabProgress: {
color: green[500],
position: "absolute",
bottom: -6,
left: -6,
zIndex: 1,
},
buttonSuccess: {
backgroundColor: green[500],
"&:hover": {
backgroundColor: green[700],
},
},
}));
export default function UploadButton(props) {
const { t } = useTranslation("application", { keyPrefix: "fileManager" });
const [open, setOpen] = useState(false);
const [queued, setQueued] = useState(5);
const path = useSelector((state) => state.navigator.path);
const classes = useStyles();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const OpenNewFolderDialog = useCallback(
() => dispatch(openCreateFolderDialog()),
[dispatch]
);
const OpenNewFileDialog = useCallback(
() => dispatch(openCreateFileDialog()),
[dispatch]
);
useEffect(() => {
setQueued(props.Queued);
}, [props.Queued]);
const uploadClicked = () => {
if (open) {
if (queued !== 0) {
props.openFileList();
} else {
props.selectFile(path);
}
}
};
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const circularProgress = useMemo(() => {
if (props.progress.totalSize > 0) {
return (
<CircularProgress
key={1}
size={68}
variant="determinate"
value={
(props.progress.processedSize /
props.progress.totalSize) *
100
}
className={classes.fabProgress}
/>
);
}
}, [classes, props.progress]);
return (
<AutoHidden enable hide={props.taskListOpen}>
<Badge
badgeContent={queued}
classes={{
badge: classes.badge, // class name, e.g. `root-x`
}}
className={classes.fab}
invisible={queued === 0}
color="primary"
>
<Backdrop open={open && statusHelper.isMobile()} />
<SpeedDial
hidden={false}
tooltipTitle={t("uploadFiles")}
icon={
<SpeedDialIcon
openIcon={
!statusHelper.isMobile() && <PublishIcon />
}
/>
}
onClose={handleClose}
FabProps={{
onClick: () =>
!statusHelper.isMobile() && uploadClicked(),
color: "secondary",
}}
onOpen={handleOpen}
open={open}
ariaLabel={""}
>
{statusHelper.isMobile() && (
<SpeedDialAction
key="UploadFile"
icon={<PublishIcon />}
tooltipOpen
tooltipTitle={t("uploadFiles")}
onClick={() => uploadClicked()}
title={t("uploadFiles")}
/>
)}
{!statusHelper.isMobile() && (
<SpeedDialAction
key="UploadFolder"
icon={<FolderUpload />}
tooltipOpen
tooltipTitle={t("uploadFolder")}
onClick={() =>
props.selectFile(path, SelectType.Directory)
}
title={t("uploadFolder")}
/>
)}
<SpeedDialAction
key="NewFolder"
icon={<CreateNewFolderIcon />}
tooltipOpen
tooltipTitle={t("newFolder")}
onClick={() => OpenNewFolderDialog()}
title={t("newFolder")}
/>
<SpeedDialAction
key="NewFile"
icon={<FilePlus />}
tooltipOpen
tooltipTitle={t("newFile")}
onClick={() => OpenNewFileDialog()}
title={t("newFile")}
/>
</SpeedDial>
{circularProgress}
</Badge>
</AutoHidden>
);
}

View File

@@ -0,0 +1,84 @@
import React from "react";
import { makeStyles } from "@material-ui/core";
import SaveIcon from "@material-ui/icons/Save";
import CheckIcon from "@material-ui/icons/Check";
import AutoHidden from "./AutoHidden";
import statusHelper from "../../utils/page";
import Fab from "@material-ui/core/Fab";
import Tooltip from "@material-ui/core/Tooltip";
import CircularProgress from "@material-ui/core/CircularProgress";
import { green } from "@material-ui/core/colors";
import clsx from "clsx";
const useStyles = makeStyles((theme) => ({
fab: {
margin: 0,
top: "auto",
right: 20,
bottom: 20,
left: "auto",
zIndex: 5,
position: "fixed",
},
badge: {
position: "absolute",
bottom: 26,
top: "auto",
zIndex: 9999,
right: 7,
},
fabProgress: {
color: green[500],
position: "absolute",
top: -6,
left: -6,
zIndex: 1,
},
wrapper: {
margin: theme.spacing(1),
position: "relative",
},
buttonSuccess: {
backgroundColor: green[500],
"&:hover": {
backgroundColor: green[700],
},
},
}));
export default function SaveButton(props) {
const classes = useStyles();
const buttonClassname = clsx({
[classes.buttonSuccess]: props.status === "success",
});
return (
<AutoHidden enable={statusHelper.isMobile()}>
<div className={classes.fab}>
<div className={classes.wrapper}>
<Tooltip title={"保存"} placement={"left"}>
<Fab
onClick={props.onClick}
color="primary"
className={buttonClassname}
disabled={props.status === "loading"}
aria-label="add"
>
{props.status === "success" ? (
<CheckIcon />
) : (
<SaveIcon />
)}
</Fab>
</Tooltip>
{props.status === "loading" && (
<CircularProgress
size={68}
className={classes.fabProgress}
/>
)}
</div>
</div>
</AutoHidden>
);
}

View File

@@ -0,0 +1,229 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import RefreshIcon from "@material-ui/icons/Refresh";
import API from "../../middleware/Api";
import { Button, IconButton, Typography, withStyles } from "@material-ui/core";
import DownloadingCard from "./DownloadingCard";
import FinishedCard from "./FinishedCard";
import RemoteDownloadButton from "../Dial/Aria2";
import Auth from "../../middleware/Auth";
import { toggleSnackbar } from "../../redux/explorer";
import Nothing from "../Placeholder/Nothing";
import { withTranslation } from "react-i18next";
const styles = (theme) => ({
actions: {
display: "flex",
},
title: {
marginTop: "20px",
},
layout: {
width: "auto",
marginTop: "30px",
marginLeft: theme.spacing(3),
marginRight: theme.spacing(3),
[theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: {
width: 700,
marginLeft: "auto",
marginRight: "auto",
},
},
shareTitle: {
maxWidth: "200px",
},
avatarFile: {
backgroundColor: theme.palette.primary.light,
},
avatarFolder: {
backgroundColor: theme.palette.secondary.light,
},
gird: {
marginTop: "30px",
},
hide: {
display: "none",
},
loadingAnimation: {
borderRadius: "6px 6px 0 0",
},
shareFix: {
marginLeft: "20px",
},
loadMore: {
textAlign: "center",
marginTop: "20px",
marginBottom: "20px",
},
margin: {
marginTop: theme.spacing(2),
},
});
const mapStateToProps = () => {
return {};
};
const mapDispatchToProps = (dispatch) => {
return {
toggleSnackbar: (vertical, horizontal, msg, color) => {
dispatch(toggleSnackbar(vertical, horizontal, msg, color));
},
};
};
class DownloadComponent extends Component {
page = 0;
interval = 0;
previousDownloading = -1;
state = {
downloading: [],
loading: false,
finishedList: [],
continue: true,
};
componentDidMount = () => {
this.loadDownloading();
};
componentWillUnmount() {
clearTimeout(this.interval);
}
loadDownloading = () => {
this.setState({
loading: true,
});
API.get("/aria2/downloading")
.then((response) => {
this.setState({
downloading: response.data,
loading: false,
});
// 设定自动更新
clearTimeout(this.interval);
if (response.data.length > 0) {
this.interval = setTimeout(
this.loadDownloading,
1000 *
response.data.reduce(function (prev, current) {
return prev.interval < current.interval
? prev
: current;
}).interval
);
}
// 下载中条目变更时刷新已完成列表
if (response.data.length !== this.previousDownloading) {
this.page = 0;
this.setState({
finishedList: [],
continue: true,
});
this.loadMore();
}
this.previousDownloading = response.data.length;
})
.catch((error) => {
this.props.toggleSnackbar(
"top",
"right",
error.message,
"error"
);
});
};
loadMore = () => {
this.setState({
loading: true,
});
API.get("/aria2/finished?page=" + ++this.page)
.then((response) => {
this.setState({
finishedList: [
...this.state.finishedList,
...response.data,
],
loading: false,
continue: response.data.length >= 10,
});
})
.catch(() => {
this.props.toggleSnackbar(
"top",
"right",
this.props.t("download.failedToLoad"),
"error"
);
this.setState({
loading: false,
});
});
};
render() {
const { classes, t } = this.props;
const user = Auth.GetUser();
return (
<div className={classes.layout}>
{user.group.allowRemoteDownload && <RemoteDownloadButton />}
<Typography
color="textSecondary"
variant="h4"
className={classes.title}
>
{t("download.active")}
<IconButton
disabled={this.state.loading}
onClick={this.loadDownloading}
>
<RefreshIcon />
</IconButton>
</Typography>
{this.state.downloading.length === 0 && (
<Nothing primary={t("download.activeEmpty")} />
)}
{this.state.downloading.map((value, k) => (
<DownloadingCard key={k} task={value} />
))}
<Typography
color="textSecondary"
variant="h4"
className={classes.title}
>
{t("download.finished")}
</Typography>
<div className={classes.loadMore}>
{this.state.finishedList.length === 0 && (
<Nothing primary={t("download.finishedEmpty")} />
)}
{this.state.finishedList.map((value, k) => {
if (value.files) {
return <FinishedCard key={k} task={value} />;
}
return null;
})}
<Button
size="large"
className={classes.margin}
disabled={!this.state.continue}
onClick={this.loadMore}
>
{t("download.loadMore")}
</Button>
</div>
</div>
);
}
}
const Download = connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(withTranslation()(DownloadComponent)));
export default Download;

View File

@@ -0,0 +1,761 @@
import React, { useCallback, useEffect,useMemo } from "react";
import {
Card,
CardContent,
darken,
IconButton,
lighten,
LinearProgress,
makeStyles,
Typography,
useTheme,
} from "@material-ui/core";
import { useDispatch } from "react-redux";
import { hex2bin, sizeToString } from "../../utils";
import PermMediaIcon from "@material-ui/icons/PermMedia";
import TypeIcon from "../FileManager/TypeIcon";
import MuiExpansionPanel from "@material-ui/core/ExpansionPanel";
import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary";
import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails";
import withStyles from "@material-ui/core/styles/withStyles";
import Divider from "@material-ui/core/Divider";
import { ExpandMore, HighlightOff } from "@material-ui/icons";
import classNames from "classnames";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import Table from "@material-ui/core/Table";
import Badge from "@material-ui/core/Badge";
import Tooltip from "@material-ui/core/Tooltip";
import API from "../../middleware/Api";
import Button from "@material-ui/core/Button";
import Grid from "@material-ui/core/Grid";
import TimeAgo from "timeago-react";
import SelectFileDialog from "../Modals/SelectFile";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../redux/explorer";
import { useTranslation } from "react-i18next";
import { TableVirtuoso } from "react-virtuoso";
const ExpansionPanel = withStyles({
root: {
maxWidth: "100%",
boxShadow: "none",
"&:not(:last-child)": {
borderBottom: 0,
},
"&:before": {
display: "none",
},
"&$expanded": {},
},
expanded: {},
})(MuiExpansionPanel);
const ExpansionPanelSummary = withStyles({
root: {
minHeight: 0,
padding: 0,
"&$expanded": {
minHeight: 56,
},
},
content: {
maxWidth: "100%",
margin: 0,
display: "flex",
"&$expanded": {
margin: "0",
},
},
expanded: {},
})(MuiExpansionPanelSummary);
const ExpansionPanelDetails = withStyles((theme) => ({
root: {
display: "block",
padding: theme.spacing(0),
},
}))(MuiExpansionPanelDetails);
const useStyles = makeStyles((theme) => ({
card: {
marginTop: "20px",
justifyContent: "space-between",
},
iconContainer: {
width: "90px",
height: "96px",
padding: " 35px 29px 29px 29px",
paddingLeft: "35px",
[theme.breakpoints.down("sm")]: {
display: "none",
},
},
content: {
width: "100%",
minWidth: 0,
[theme.breakpoints.up("sm")]: {
borderInlineStart: "1px " + theme.palette.divider + " solid",
},
},
contentSide: {
minWidth: 0,
paddingTop: "24px",
paddingRight: "28px",
[theme.breakpoints.down("sm")]: {
display: "none",
},
},
iconBig: {
fontSize: "30px",
},
iconMultiple: {
fontSize: "30px",
color: "#607D8B",
},
progress: {
marginTop: 8,
marginBottom: 4,
},
expand: {
transition: ".15s transform ease-in-out",
},
expanded: {
transform: "rotate(180deg)",
},
subFile: {
width: "100%",
minWidth: 300,
wordBreak: "break-all",
},
subFileName: {
display: "flex",
},
subFileIcon: {
marginRight: "20px",
},
subFileSize: {
minWidth: 120,
},
subFilePercent: {
minWidth: 105,
},
scroll: {
overflow: "auto",
maxHeight: "300px",
},
action: {
padding: theme.spacing(2),
textAlign: "right",
},
actionButton: {
marginLeft: theme.spacing(1),
},
info: {
padding: theme.spacing(2),
},
infoTitle: {
fontWeight: 700,
textAlign: "left",
},
infoValue: {
color: theme.palette.text.secondary,
textAlign: "left",
paddingLeft:theme.spacing(1),
},
bitmap: {
width: "100%",
height: "50px",
backgroundColor: theme.palette.background.default,
},
}));
export default function DownloadingCard(props) {
const { t } = useTranslation("application", { keyPrefix: "download" });
const { t: tGlobal } = useTranslation();
const canvasRef = React.createRef();
const classes = useStyles();
const theme = useTheme();
const history = useHistory();
const [expanded, setExpanded] = React.useState("");
const [task, setTask] = React.useState(props.task);
const [loading, setLoading] = React.useState(false);
const [selectDialogOpen, setSelectDialogOpen] = React.useState(false);
const [selectFileOption, setSelectFileOption] = React.useState([]);
const handleChange = (panel) => (event, newExpanded) => {
setExpanded(newExpanded ? panel : false);
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
setTask(props.task);
}, [props.task]);
useEffect(() => {
if (task.info.bitfield === "") {
return;
}
let result = "";
task.info.bitfield.match(/.{1,2}/g).forEach((str) => {
result += hex2bin(str);
});
const canvas = canvasRef.current;
const context = canvas.getContext("2d");
context.clearRect(0, 0, canvas.width, canvas.height);
context.strokeStyle = theme.palette.primary.main;
for (let i = 0; i < canvas.width; i++) {
let bit =
result[
Math.round(((i + 1) / canvas.width) * task.info.numPieces)
];
bit = bit ? bit : result.slice(-1);
if (bit === "1") {
context.beginPath();
context.moveTo(i, 0);
context.lineTo(i, canvas.height);
context.stroke();
}
}
// eslint-disable-next-line
}, [task.info.bitfield, task.info.numPieces, theme]);
const getPercent = (completed, total) => {
if (total === 0) {
return 0;
}
return (completed / total) * 100;
};
const activeFiles = useCallback(() => {
return task.info.files.filter((v) => v.selected === "true");
}, [task.info.files]);
const deleteFile = (index) => {
setLoading(true);
const current = activeFiles();
const newIndex = [];
const newFiles = [];
// eslint-disable-next-line
current.map((v) => {
if (v.index !== index && v.selected) {
newIndex.push(parseInt(v.index));
newFiles.push({
...v,
selected: "true",
});
} else {
newFiles.push({
...v,
selected: "false",
});
}
});
API.put("/aria2/select/" + task.info.gid, {
indexes: newIndex,
})
.then(() => {
setTask({
...task,
info: {
...task.info,
files: newFiles,
},
});
ToggleSnackbar("top", "right", t("taskFileDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const getDownloadName = useCallback(() => {
if (task.info.bittorrent.info.name !== "") {
return task.info.bittorrent.info.name;
}
return task.name === "." ? t("unknownTaskName") : task.name;
}, [task]);
const getIcon = useCallback(() => {
if (task.info.bittorrent.mode === "multi") {
return (
<Badge badgeContent={activeFiles().length} color="secondary">
<PermMediaIcon className={classes.iconMultiple} />
</Badge>
);
} else {
return (
<TypeIcon
className={classes.iconBig}
fileName={getDownloadName(task)}
/>
);
}
// eslint-disable-next-line
}, [task, classes]);
const cancel = () => {
setLoading(true);
API.delete("/aria2/task/" + task.info.gid)
.then(() => {
ToggleSnackbar("top", "right", t("taskCanceled"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const changeSelectedFile = (fileIndex) => {
setLoading(true);
API.put("/aria2/select/" + task.info.gid, {
indexes: fileIndex,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
t("operationSubmitted"),
"success"
);
setSelectDialogOpen(false);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const subFileList = useMemo(() => {
const processStyle = (value) => ({
background:
"linear-gradient(to right, " +
(theme.palette.type ===
"dark"
? darken(
theme.palette
.primary
.main,
0.4
)
: lighten(
theme.palette
.primary
.main,
0.85
)) +
" 0%," +
(theme.palette.type ===
"dark"
? darken(
theme.palette
.primary
.main,
0.4
)
: lighten(
theme.palette
.primary
.main,
0.85
)) +
" " +
getPercent(
value.completedLength,
value.length
).toFixed(0) +
"%," +
theme.palette.background
.paper +
" " +
getPercent(
value.completedLength,
value.length
).toFixed(0) +
"%," +
theme.palette.background
.paper +
" 100%)",
});
const subFileCell = (value) => (
<>
<TableCell
component="th"
scope="row"
className={classes.subFile}
>
<Typography
className={
classes.subFileName
}
>
<TypeIcon
className={
classes.subFileIcon
}
fileName={
value.path
}
/>
{value.path}
</Typography>
</TableCell>
<TableCell
component="th"
scope="row"
className={classes.subFileSize}
>
<Typography noWrap>
{" "}
{sizeToString(
value.length
)}
</Typography>
</TableCell>
<TableCell
component="th"
scope="row"
className={classes.subFilePercent}
>
<Typography noWrap>
{getPercent(
value.completedLength,
value.length
).toFixed(2)}
%
</Typography>
</TableCell>
<TableCell>
<Tooltip
title={t(
"deleteThisFile"
)}
>
<IconButton
onClick={() =>
deleteFile(
value.index
)
}
disabled={loading}
size={"small"}
>
<HighlightOff />
</IconButton>
</Tooltip>
</TableCell>
</>
);
return activeFiles().length > 5 ? (
<TableVirtuoso
style={{ height: 43 * activeFiles().length }}
className={classes.scroll}
components={{
// eslint-disable-next-line react/display-name
Table: (props) => <Table {...props} size={"small"} />,
// eslint-disable-next-line react/display-name
TableRow: (props) => {
const index = props["data-index"];
const value = activeFiles()[index];
return (
<TableRow
{...props}
key={index}
style={processStyle(value)}
/>
);
},
}}
data={activeFiles()}
itemContent={(index, value) => (
subFileCell(value)
)}
/>
) : (
<div className={classes.scroll}>
<Table size="small">
<TableBody>
{activeFiles().map((value) => {
return (
<TableRow
key={value.index}
style={processStyle(value)}
>
{subFileCell(value)}
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
}, [
classes,
theme,
activeFiles,
]);
return (
<Card className={classes.card}>
<SelectFileDialog
open={selectDialogOpen}
onClose={() => setSelectDialogOpen(false)}
modalsLoading={loading}
files={selectFileOption}
onSubmit={changeSelectedFile}
/>
<ExpansionPanel
square
expanded={expanded === task.info.gid}
onChange={handleChange(task.info.gid)}
>
<ExpansionPanelSummary
aria-controls="panel1d-content"
id="panel1d-header"
>
<div className={classes.iconContainer}>{getIcon()}</div>
<CardContent className={classes.content}>
<Typography color="primary" noWrap>
<Tooltip title={getDownloadName()}>
<span>{getDownloadName()}</span>
</Tooltip>
</Typography>
<LinearProgress
color="secondary"
variant="determinate"
className={classes.progress}
value={getPercent(task.downloaded, task.total)}
/>
<Typography
variant="body2"
color="textSecondary"
noWrap
>
{task.total > 0 && (
<span>
{getPercent(
task.downloaded,
task.total
).toFixed(2)}
% -{" "}
{task.downloaded === 0
? "0Bytes"
: sizeToString(task.downloaded)}
/
{task.total === 0
? "0Bytes"
: sizeToString(task.total)}{" "}
-{" "}
{task.speed === "0"
? "0B/s"
: sizeToString(task.speed) + "/s"}
</span>
)}
{task.total === 0 && <span> - </span>}
</Typography>
</CardContent>
<CardContent className={classes.contentSide}>
<IconButton>
<ExpandMore
className={classNames(
{
[classes.expanded]:
expanded === task.info.gid,
},
classes.expand
)}
/>
</IconButton>
</CardContent>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Divider />
{task.info.bittorrent.mode === "multi" && subFileList}
<div className={classes.action}>
<Button
className={classes.actionButton}
variant="outlined"
color="secondary"
onClick={() =>
history.push(
"/home?path=" + encodeURIComponent(task.dst)
)
}
>
{t("openDstFolder")}
</Button>
{task.info.bittorrent.mode === "multi" && (
<Button
className={classes.actionButton}
variant="outlined"
color="secondary"
disabled={loading}
onClick={() => {
setSelectDialogOpen(true);
setSelectFileOption([
...props.task.info.files,
]);
}}
>
{t("selectDownloadingFile")}
</Button>
)}
<Button
className={classes.actionButton}
onClick={cancel}
variant="contained"
color="secondary"
disabled={loading}
>
{t("cancelTask")}
</Button>
</div>
<Divider />
<div className={classes.info}>
{task.info.bitfield !== "" && (
<canvas
width={"700"}
height={"100"}
ref={canvasRef}
className={classes.bitmap}
/>
)}
<Grid container>
<Grid container xs={12} sm={4}>
<Grid item xs={5} className={classes.infoTitle}>
{t("updatedAt")}
</Grid>
<Grid item xs={7} className={classes.infoValue}>
<TimeAgo
datetime={task.update}
locale={tGlobal("timeAgoLocaleCode", {
ns: "common",
})}
/>
</Grid>
</Grid>
<Grid container xs={12} sm={4}>
<Grid item xs={5} className={classes.infoTitle}>
{t("uploaded")}
</Grid>
<Grid item xs={7} className={classes.infoValue}>
{sizeToString(task.info.uploadLength)}
</Grid>
</Grid>
<Grid container xs={12} sm={4}>
<Grid item xs={5} className={classes.infoTitle}>
{t("uploadSpeed")}
</Grid>
<Grid item xs={7} className={classes.infoValue}>
{sizeToString(task.info.uploadSpeed)} / s
</Grid>
</Grid>
{task.info.bittorrent.mode !== "" && (
<>
<Grid container xs={12} sm={8}>
<Grid
item
sm={2}
xs={4}
className={classes.infoTitle}
>
{t("InfoHash")}
</Grid>
<Grid
item
sm={10}
xs={8}
style={{
wordBreak: "break-all",
}}
className={classes.infoValue}
>
{task.info.infoHash}
</Grid>
</Grid>
<Grid container xs={12} sm={4}>
<Grid
item
xs={5}
className={classes.infoTitle}
>
{t("seederCount")}
</Grid>
<Grid
item
xs={7}
className={classes.infoValue}
>
{task.info.numSeeders}
</Grid>
</Grid>
<Grid container xs={12} sm={4}>
<Grid
item
xs={5}
className={classes.infoTitle}
>
{t("seeding")}
</Grid>
<Grid
item
xs={7}
className={classes.infoValue}
>
{task.info.seeder === "true"
? t("isSeeding")
: t("notSeeding")}
</Grid>
</Grid>
</>
)}
<Grid container xs={12} sm={4}>
<Grid item xs={5} className={classes.infoTitle}>
{t("chunkSize")}
</Grid>
<Grid item xs={7} className={classes.infoValue}>
{sizeToString(task.info.pieceLength)}
</Grid>
</Grid>
<Grid container xs={12} sm={4}>
<Grid item xs={5} className={classes.infoTitle}>
{t("chunkNumbers")}
</Grid>
<Grid item xs={7} className={classes.infoValue}>
{task.info.numPieces}
</Grid>
</Grid>
{props.task.node && <Grid container xs={12} sm={4}>
<Grid item xs={5} className={classes.infoTitle}>
{t("downloadNode")}
</Grid>
<Grid item xs={7} className={classes.infoValue}>
{props.task.node}
</Grid>
</Grid>}
</Grid>
</div>
</ExpansionPanelDetails>
</ExpansionPanel>
</Card>
);
}

View File

@@ -0,0 +1,489 @@
import React, { useCallback, useMemo } from "react";
import {
Card,
CardContent,
IconButton,
makeStyles,
Typography,
useTheme,
} from "@material-ui/core";
import { sizeToString } from "../../utils";
import PermMediaIcon from "@material-ui/icons/PermMedia";
import TypeIcon from "../FileManager/TypeIcon";
import MuiExpansionPanel from "@material-ui/core/ExpansionPanel";
import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary";
import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails";
import withStyles from "@material-ui/core/styles/withStyles";
import Divider from "@material-ui/core/Divider";
import { ExpandMore } from "@material-ui/icons";
import classNames from "classnames";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import Badge from "@material-ui/core/Badge";
import Tooltip from "@material-ui/core/Tooltip";
import Button from "@material-ui/core/Button";
import Grid from "@material-ui/core/Grid";
import API from "../../middleware/Api";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router";
import { formatLocalTime } from "../../utils/datetime";
import { toggleSnackbar } from "../../redux/explorer";
import { TableVirtuoso } from "react-virtuoso";
import { useTranslation } from "react-i18next";
const ExpansionPanel = withStyles({
root: {
maxWidth: "100%",
boxShadow: "none",
"&:not(:last-child)": {
borderBottom: 0,
},
"&:before": {
display: "none",
},
"&$expanded": {},
},
expanded: {},
})(MuiExpansionPanel);
const ExpansionPanelSummary = withStyles({
root: {
minHeight: 0,
padding: 0,
"&$expanded": {
minHeight: 56,
},
},
content: {
maxWidth: "100%",
margin: 0,
display: "flex",
"&$expanded": {
margin: "0",
},
},
expanded: {},
})(MuiExpansionPanelSummary);
const ExpansionPanelDetails = withStyles((theme) => ({
root: {
display: "block",
padding: theme.spacing(0),
},
}))(MuiExpansionPanelDetails);
const useStyles = makeStyles((theme) => ({
card: {
marginTop: "20px",
justifyContent: "space-between",
},
iconContainer: {
width: "90px",
height: "96px",
padding: " 35px 29px 29px 29px",
paddingLeft: "35px",
[theme.breakpoints.down("sm")]: {
display: "none",
},
},
content: {
width: "100%",
minWidth: 0,
[theme.breakpoints.up("sm")]: {
borderInlineStart: "1px " + theme.palette.divider + " solid",
},
textAlign: "left",
},
contentSide: {
minWidth: 0,
paddingTop: "24px",
paddingRight: "28px",
[theme.breakpoints.down("sm")]: {
display: "none",
},
},
iconBig: {
fontSize: "30px",
},
iconMultiple: {
fontSize: "30px",
color: "#607D8B",
},
progress: {
marginTop: 8,
marginBottom: 4,
},
expand: {
transition: ".15s transform ease-in-out",
},
expanded: {
transform: "rotate(180deg)",
},
subFile: {
width: "100%",
minWidth: 300,
wordBreak: "break-all",
},
subFileName: {
display: "flex",
},
subFileIcon: {
marginRight: "20px",
},
subFileSize: {
minWidth: 115,
},
subFilePercent: {
minWidth: 100,
},
scroll: {
overflow: "auto",
maxHeight: "300px",
},
action: {
padding: theme.spacing(2),
textAlign: "right",
},
actionButton: {
marginLeft: theme.spacing(1),
},
info: {
padding: theme.spacing(2),
},
infoTitle: {
fontWeight: 700,
textAlign: "left",
},
infoValue: {
color: theme.palette.text.secondary,
textAlign: "left",
paddingLeft: theme.spacing(1),
},
}));
export default function FinishedCard(props) {
const { t } = useTranslation("application", { keyPrefix: "download" });
const classes = useStyles();
const theme = useTheme();
const history = useHistory();
const [expanded, setExpanded] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const handleChange = () => (event, newExpanded) => {
setExpanded(!!newExpanded);
};
const cancel = () => {
setLoading(true);
API.delete("/aria2/task/" + props.task.gid)
.then(() => {
ToggleSnackbar("top", "right", t("taskDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
window.location.reload();
});
};
const getPercent = (completed, total) => {
if (total == 0) {
return 0;
}
return (completed / total) * 100;
};
const getDownloadName = useCallback(() => {
return props.task.name === "." ? t("unknownTaskName") : props.task.name;
}, [props.task.name]);
const activeFiles = useCallback(() => {
return props.task.files.filter((v) => v.selected === "true");
}, [props.task.files]);
const getIcon = useCallback(() => {
if (props.task.files.length > 1) {
return (
<Badge badgeContent={activeFiles().length} color="secondary">
<PermMediaIcon className={classes.iconMultiple} />
</Badge>
);
} else {
return (
<TypeIcon
className={classes.iconBig}
fileName={getDownloadName(props.task)}
/>
);
}
}, [props.task, classes]);
const getTaskError = (error) => {
try {
const res = JSON.parse(error);
return res.msg + "" + res.error;
} catch (e) {
return t("transferFailed");
}
};
const subFileList = useMemo(() => {
const subFileCell = (value) => (
<>
<TableCell
component="th"
scope="row"
className={classes.subFile}
>
<Typography className={classes.subFileName}>
<TypeIcon
className={classes.subFileIcon}
fileName={value.path}
/>
{value.path}
</Typography>
</TableCell>
<TableCell
component="th"
scope="row"
className={classes.subFileSize}
>
<Typography noWrap>
{" "}
{sizeToString(value.length)}
</Typography>
</TableCell>
<TableCell
component="th"
scope="row"
className={classes.subFilePercent}
>
<Typography noWrap>
{getPercent(
value.completedLength,
value.length
).toFixed(2)}
%
</Typography>
</TableCell>
</>
);
return activeFiles().length > 5 ? (
<TableVirtuoso
style={{ height: 57 * activeFiles().length }}
className={classes.scroll}
components={{
// eslint-disable-next-line react/display-name
Table: (props) => <Table {...props} />,
}}
data={activeFiles()}
itemContent={(index, value) => subFileCell(value)}
/>
) : (
<div className={classes.scroll}>
<Table>
<TableBody>
{activeFiles().map((value) => {
return (
<TableRow key={value.index}>
{subFileCell(value)}
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
}, [classes, activeFiles]);
return (
<Card className={classes.card}>
<ExpansionPanel
square
expanded={expanded}
onChange={handleChange("")}
>
<ExpansionPanelSummary
aria-controls="panel1d-content"
id="panel1d-header"
>
<div className={classes.iconContainer}>{getIcon()}</div>
<CardContent className={classes.content}>
<Typography color="primary" noWrap>
<Tooltip title={getDownloadName()}>
<span>{getDownloadName()}</span>
</Tooltip>
</Typography>
{props.task.status === 3 && (
<Tooltip title={props.task.error}>
<Typography
variant="body2"
color="error"
noWrap
>
{t("downloadFailed", {
msg: props.task.error,
})}
</Typography>
</Tooltip>
)}
{props.task.status === 5 && (
<Typography
variant="body2"
color="textSecondary"
noWrap
>
{t("canceledStatus")}
{props.task.error !== "" && (
<span>({props.task.error})</span>
)}
</Typography>
)}
{props.task.status === 4 &&
props.task.task_status === 4 && (
<Typography
variant="body2"
style={{
color: theme.palette.success.main,
}}
noWrap
>
{t("finishedStatus")}
</Typography>
)}
{props.task.status === 4 &&
props.task.task_status === 0 && (
<Typography
variant="body2"
style={{
color: theme.palette.success.light,
}}
noWrap
>
{t("pending")}
</Typography>
)}
{props.task.status === 4 &&
props.task.task_status === 1 && (
<Typography
variant="body2"
style={{
color: theme.palette.success.light,
}}
noWrap
>
{t("transferring")}
</Typography>
)}
{props.task.status === 4 &&
props.task.task_status === 2 && (
<Tooltip
title={getTaskError(props.task.task_error)}
>
<Typography
variant="body2"
color={"error"}
noWrap
>
{getTaskError(props.task.task_error)}
</Typography>
</Tooltip>
)}
</CardContent>
<CardContent className={classes.contentSide}>
<IconButton>
<ExpandMore
className={classNames(
{
[classes.expanded]: expanded,
},
classes.expand
)}
/>
</IconButton>
</CardContent>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Divider />
{props.task.files.length > 1 && subFileList}
<div className={classes.action}>
<Button
className={classes.actionButton}
variant="outlined"
color="secondary"
onClick={() =>
history.push(
"/home?path=" +
encodeURIComponent(props.task.dst)
)
}
>
{t("openDstFolder")}
</Button>
<Button
className={classes.actionButton}
onClick={cancel}
variant="contained"
color="secondary"
disabled={loading}
>
{t("deleteRecord")}
</Button>
</div>
<Divider />
<div className={classes.info}>
<Grid container>
<Grid container xs={12} sm={6}>
<Grid item xs={5} className={classes.infoTitle}>
{t("createdAt")}
</Grid>
<Grid item xs={7} className={classes.infoValue}>
{formatLocalTime(props.task.create)}
</Grid>
</Grid>
<Grid container xs={12} sm={6}>
<Grid item xs={5} className={classes.infoTitle}>
{t("updatedAt")}
</Grid>
<Grid item xs={7} className={classes.infoValue}>
{formatLocalTime(props.task.update)}
</Grid>
</Grid>
{props.task.node && (
<Grid container xs={12} sm={6}>
<Grid
item
xs={5}
className={classes.infoTitle}
>
{t("downloadNode")}
</Grid>
<Grid
item
xs={7}
className={classes.infoValue}
>
{props.task.node}
</Grid>
</Grid>
)}
</Grid>
</div>
</ExpansionPanelDetails>
</ExpansionPanel>
</Card>
);
}

View File

@@ -0,0 +1,754 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { isCompressFile, isPreviewable, isTorrent } from "../../config";
import UploadIcon from "@material-ui/icons/CloudUpload";
import DownloadIcon from "@material-ui/icons/CloudDownload";
import NewFolderIcon from "@material-ui/icons/CreateNewFolder";
import OpenFolderIcon from "@material-ui/icons/FolderOpen";
import FileCopyIcon from "@material-ui/icons/FileCopy";
import ShareIcon from "@material-ui/icons/Share";
import RenameIcon from "@material-ui/icons/BorderColor";
import MoveIcon from "@material-ui/icons/Input";
import LinkIcon from "@material-ui/icons/InsertLink";
import DeleteIcon from "@material-ui/icons/Delete";
import OpenIcon from "@material-ui/icons/OpenInNew";
import {
FolderDownload,
FilePlus,
FolderUpload,
MagnetOn,
Transfer,
} from "mdi-material-ui";
import {
Divider,
ListItemIcon,
MenuItem,
Typography,
withStyles,
} from "@material-ui/core";
import pathHelper from "../../utils/page";
import { withRouter } from "react-router-dom";
import Auth from "../../middleware/Auth";
import { Archive, InfoOutlined, Unarchive } from "@material-ui/icons";
import Menu from "@material-ui/core/Menu";
import RefreshIcon from "@material-ui/icons/Refresh";
import {
batchGetSource,
openParentFolder,
openPreview,
openTorrentDownload,
setSelectedTarget,
startBatchDownload,
startDirectoryDownload,
startDownload,
toggleObjectInfoSidebar,
} from "../../redux/explorer/action";
import {
changeContextMenu,
navigateTo,
openCompressDialog,
openCopyDialog,
openCreateFileDialog,
openCreateFolderDialog,
openDecompressDialog,
openLoadingDialog,
openMoveDialog,
openMusicDialog,
openRelocateDialog,
openRemoteDownloadDialog,
openRemoveDialog,
openRenameDialog,
openShareDialog,
refreshFileList,
setNavigatorLoadingStatus,
showImgPreivew,
toggleSnackbar,
} from "../../redux/explorer";
import { pathJoin } from "../Uploader/core/utils";
import {
openFileSelector,
openFolderSelector,
} from "../../redux/viewUpdate/action";
import { withTranslation } from "react-i18next";
const styles = () => ({
propover: {},
divider: {
marginTop: 4,
marginBottom: 4,
},
});
const StyledListItemIcon = withStyles({
root: {
minWidth: 38,
},
})(ListItemIcon);
const mapStateToProps = (state) => {
return {
menuType: state.viewUpdate.contextType,
menuOpen: state.viewUpdate.contextOpen,
isMultiple: state.explorer.selectProps.isMultiple,
withFolder: state.explorer.selectProps.withFolder,
withFile: state.explorer.selectProps.withFile,
withSourceEnabled: state.explorer.selectProps.withSourceEnabled,
path: state.navigator.path,
selected: state.explorer.selected,
search: state.explorer.search,
};
};
const mapDispatchToProps = (dispatch) => {
return {
changeContextMenu: (type, open) => {
dispatch(changeContextMenu(type, open));
},
setNavigatorLoadingStatus: (status) => {
dispatch(setNavigatorLoadingStatus(status));
},
setSelectedTarget: (targets) => {
dispatch(setSelectedTarget(targets));
},
navigateTo: (path) => {
dispatch(navigateTo(path));
},
openCreateFolderDialog: () => {
dispatch(openCreateFolderDialog());
},
openCreateFileDialog: () => {
dispatch(openCreateFileDialog());
},
openRenameDialog: () => {
dispatch(openRenameDialog());
},
openMoveDialog: () => {
dispatch(openMoveDialog());
},
openRemoveDialog: () => {
dispatch(openRemoveDialog());
},
openShareDialog: () => {
dispatch(openShareDialog());
},
showImgPreivew: (first) => {
dispatch(showImgPreivew(first));
},
openMusicDialog: () => {
dispatch(openMusicDialog());
},
toggleSnackbar: (vertical, horizontal, msg, color) => {
dispatch(toggleSnackbar(vertical, horizontal, msg, color));
},
openRemoteDownloadDialog: () => {
dispatch(openRemoteDownloadDialog());
},
openTorrentDownloadDialog: () => {
dispatch(openTorrentDownload());
},
openCopyDialog: () => {
dispatch(openCopyDialog());
},
openLoadingDialog: (text) => {
dispatch(openLoadingDialog(text));
},
openDecompressDialog: () => {
dispatch(openDecompressDialog());
},
openCompressDialog: () => {
dispatch(openCompressDialog());
},
refreshFileList: () => {
dispatch(refreshFileList());
},
openRelocateDialog: () => {
dispatch(openRelocateDialog());
},
openPreview: (share) => {
dispatch(openPreview(share));
},
toggleObjectInfoSidebar: (open) => {
dispatch(toggleObjectInfoSidebar(open));
},
startBatchDownload: (share) => {
dispatch(startBatchDownload(share));
},
openFileSelector: () => {
dispatch(openFileSelector());
},
openFolderSelector: () => {
dispatch(openFolderSelector());
},
startDownload: (share, file) => {
dispatch(startDownload(share, file));
},
batchGetSource: () => {
dispatch(batchGetSource());
},
startDirectoryDownload: (share) => {
dispatch(startDirectoryDownload(share));
},
openParentFolder: () => {
dispatch(openParentFolder());
},
};
};
class ContextMenuCompoment extends Component {
X = 0;
Y = 0;
state = {};
componentDidMount = () => {
window.document.addEventListener("mousemove", this.setPoint);
};
setPoint = (e) => {
this.Y = e.clientY;
this.X = e.clientX;
};
openArchiveDownload = () => {
this.props.startBatchDownload(this.props.share);
};
openDirectoryDownload = () => {
this.props.startDirectoryDownload(this.props.share);
};
openDownload = () => {
this.props.startDownload(this.props.share, this.props.selected[0]);
};
enterFolder = () => {
this.props.navigateTo(
pathJoin([this.props.path, this.props.selected[0].name])
);
};
// 暂时只对空白处右键菜单使用这个函数疑似有bug会导致的一个菜单被默认选中。
// 相关issue https://github.com/mui-org/material-ui/issues/23747
renderMenuItems = (items) => {
const res = [];
let key = 0;
["top", "center", "bottom"].forEach((position) => {
let visibleCount = 0;
items[position].forEach((item) => {
if (item.condition) {
res.push(
<MenuItem dense key={key} onClick={item.onClick}>
<ListItemIcon>{item.icon}</ListItemIcon>
<Typography variant="inherit">
{item.text}
</Typography>
</MenuItem>
);
key++;
visibleCount++;
}
});
if (visibleCount > 0 && position != "bottom") {
res.push(
<Divider key={key} className={this.props.classes.divider} />
);
key++;
}
});
return res;
};
render() {
const { classes, t } = this.props;
const user = Auth.GetUser();
const isHomePage = pathHelper.isHomePage(this.props.location.pathname);
const emptyMenuList = {
top: [
{
condition: true,
onClick: () => {
this.props.refreshFileList();
this.props.changeContextMenu(
this.props.menuType,
false
);
},
icon: <RefreshIcon />,
text: "刷新",
},
],
center: [
{
condition: true,
onClick: () => this.props.openFileSelector(),
icon: <UploadIcon />,
text: "上传文件",
},
{
condition: true,
onClick: () => this.props.openFolderSelector(),
icon: <FolderUpload />,
text: "上传目录",
},
{
condition: user.group.allowRemoteDownload,
onClick: () => this.props.openRemoteDownloadDialog(),
icon: <DownloadIcon />,
text: "离线下载",
},
],
bottom: [
{
condition: true,
onClick: () => this.props.openCreateFolderDialog(),
icon: <NewFolderIcon />,
text: "创建文件夹",
},
{
condition: true,
onClick: () => this.props.openCreateFileDialog(),
icon: <FilePlus />,
text: "创建文件",
},
],
};
return (
<div>
<Menu
keepMounted
open={this.props.menuOpen}
onClose={() =>
this.props.changeContextMenu(this.props.menuType, false)
}
anchorReference="anchorPosition"
anchorPosition={{ top: this.Y, left: this.X }}
anchorOrigin={{
vertical: "top",
horizontal: "left",
}}
transformOrigin={{
vertical: "top",
horizontal: "left",
}}
>
{this.props.menuType === "empty" && (
<div>
<MenuItem
dense
onClick={() => {
this.props.refreshFileList();
this.props.changeContextMenu(
this.props.menuType,
false
);
}}
>
<StyledListItemIcon>
<RefreshIcon />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.refresh")}
</Typography>
</MenuItem>
<Divider className={classes.divider} />
<MenuItem
dense
onClick={() => this.props.openFileSelector()}
>
<StyledListItemIcon>
<UploadIcon />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.uploadFiles")}
</Typography>
</MenuItem>
<MenuItem
dense
onClick={() => this.props.openFolderSelector()}
>
<StyledListItemIcon>
<FolderUpload />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.uploadFolder")}
</Typography>
</MenuItem>
{user.group.allowRemoteDownload && (
<MenuItem
dense
onClick={() =>
this.props.openRemoteDownloadDialog()
}
>
<StyledListItemIcon>
<DownloadIcon />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.newRemoteDownloads")}
</Typography>
</MenuItem>
)}
<Divider className={classes.divider} />
<MenuItem
dense
onClick={() =>
this.props.openCreateFolderDialog()
}
>
<StyledListItemIcon>
<NewFolderIcon />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.newFolder")}
</Typography>
</MenuItem>
<MenuItem
dense
onClick={() =>
this.props.openCreateFileDialog()
}
>
<StyledListItemIcon>
<FilePlus />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.newFile")}
</Typography>
</MenuItem>
</div>
)}
{this.props.menuType !== "empty" && (
<div>
{!this.props.isMultiple && this.props.withFolder && (
<div>
<MenuItem dense onClick={this.enterFolder}>
<StyledListItemIcon>
<OpenFolderIcon />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.enter")}
</Typography>
</MenuItem>
{isHomePage && (
<Divider className={classes.divider} />
)}
</div>
)}
{!this.props.isMultiple &&
this.props.withFile &&
(!this.props.share ||
this.props.share.preview) &&
isPreviewable(this.props.selected[0].name) && (
<div>
<MenuItem
dense
onClick={() =>
this.props.openPreview()
}
>
<StyledListItemIcon>
<OpenIcon />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.open")}
</Typography>
</MenuItem>
</div>
)}
{this.props.search && !this.props.isMultiple && (
<div>
<MenuItem
dense
onClick={() =>
this.props.openParentFolder()
}
>
<StyledListItemIcon>
<OpenFolderIcon />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.openParentFolder")}
</Typography>
</MenuItem>
</div>
)}
{!this.props.isMultiple && this.props.withFile && (
<div>
<MenuItem
dense
onClick={() =>
this.openDownload(this.props.share)
}
>
<StyledListItemIcon>
<DownloadIcon />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.download")}
</Typography>
</MenuItem>
{isHomePage && (
<Divider className={classes.divider} />
)}
</div>
)}
{(this.props.isMultiple || this.props.withFolder) &&
window.showDirectoryPicker &&
window.isSecureContext && (
<MenuItem
dense
onClick={() =>
this.openDirectoryDownload()
}
>
<StyledListItemIcon>
<FolderDownload />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.download")}
</Typography>
</MenuItem>
)}
{(this.props.isMultiple ||
this.props.withFolder) && (
<MenuItem
dense
onClick={() => this.openArchiveDownload()}
>
<StyledListItemIcon>
<DownloadIcon />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.batchDownload")}
</Typography>
</MenuItem>
)}
{isHomePage &&
user.group.sourceBatch > 0 &&
this.props.withSourceEnabled && (
<MenuItem
dense
onClick={() =>
this.props.batchGetSource()
}
>
<StyledListItemIcon>
<LinkIcon />
</StyledListItemIcon>
<Typography variant="inherit">
{this.props.isMultiple ||
(this.props.withFolder &&
!this.props.withFile)
? t(
"fileManager.getSourceLinkInBatch"
)
: t(
"fileManager.getSourceLink"
)}
</Typography>
</MenuItem>
)}
{!this.props.isMultiple &&
isHomePage &&
user.group.allowRemoteDownload &&
this.props.withFile &&
isTorrent(this.props.selected[0].name) && (
<MenuItem
dense
onClick={() =>
this.props.openTorrentDownloadDialog()
}
>
<StyledListItemIcon>
<MagnetOn />
</StyledListItemIcon>
<Typography variant="inherit">
{t(
"fileManager.createRemoteDownloadForTorrent"
)}
</Typography>
</MenuItem>
)}
{!this.props.isMultiple &&
isHomePage &&
user.group.compress &&
this.props.withFile &&
isCompressFile(this.props.selected[0].name) && (
<MenuItem
dense
onClick={() =>
this.props.openDecompressDialog()
}
>
<StyledListItemIcon>
<Unarchive />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.decompress")}
</Typography>
</MenuItem>
)}
{isHomePage && user.group.compress && (
<MenuItem
dense
onClick={() =>
this.props.openCompressDialog()
}
>
<StyledListItemIcon>
<Archive />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.compress")}
</Typography>
</MenuItem>
)}
{isHomePage && user.group.relocate && (
<MenuItem
dense
onClick={() =>
this.props.openRelocateDialog()
}
>
<StyledListItemIcon>
<Transfer />
</StyledListItemIcon>
<Typography variant="inherit">
{t("vas.migrateStoragePolicy")}
</Typography>
</MenuItem>
)}
{!this.props.isMultiple && isHomePage && (
<MenuItem
dense
onClick={() => this.props.openShareDialog()}
>
<StyledListItemIcon>
<ShareIcon />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.createShareLink")}
</Typography>
</MenuItem>
)}
{!this.props.isMultiple && isHomePage && (
<MenuItem
dense
onClick={() =>
this.props.toggleObjectInfoSidebar(true)
}
>
<StyledListItemIcon>
<InfoOutlined />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.viewDetails")}
</Typography>
</MenuItem>
)}
{!this.props.isMultiple && isHomePage && (
<Divider className={classes.divider} />
)}
{!this.props.isMultiple && isHomePage && (
<div>
<MenuItem
dense
onClick={() =>
this.props.openRenameDialog()
}
>
<StyledListItemIcon>
<RenameIcon />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.rename")}
</Typography>
</MenuItem>
{!this.props.search && (
<MenuItem
dense
onClick={() =>
this.props.openCopyDialog()
}
>
<StyledListItemIcon>
<FileCopyIcon />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.copy")}
</Typography>
</MenuItem>
)}
</div>
)}
{isHomePage && (
<div>
{!this.props.search && (
<MenuItem
dense
onClick={() =>
this.props.openMoveDialog()
}
>
<StyledListItemIcon>
<MoveIcon />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.move")}
</Typography>
</MenuItem>
)}
<Divider className={classes.divider} />
<MenuItem
dense
className={classes.propover}
onClick={() =>
this.props.openRemoveDialog()
}
>
<StyledListItemIcon>
<DeleteIcon />
</StyledListItemIcon>
<Typography variant="inherit">
{t("fileManager.delete")}
</Typography>
</MenuItem>
</div>
)}
</div>
)}
</Menu>
</div>
);
}
}
ContextMenuCompoment.propTypes = {
classes: PropTypes.object.isRequired,
menuType: PropTypes.string.isRequired,
};
const ContextMenu = connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(withRouter(withTranslation()(ContextMenuCompoment))));
export default ContextMenu;

View File

@@ -0,0 +1,85 @@
import React, { useMemo } from "react";
import { useDragLayer } from "react-dnd";
import Preview from "./Preview";
import { useSelector } from "react-redux";
const layerStyles = {
position: "fixed",
pointerEvents: "none",
zIndex: 100,
left: 0,
top: 0,
width: "100%",
height: "100%",
};
function getItemStyles(
initialOffset,
currentOffset,
pointerOffset,
viewMethod
) {
if (!initialOffset || !currentOffset) {
return {
display: "none",
};
}
let { x, y } = currentOffset;
if (viewMethod === "list") {
x += pointerOffset.x - initialOffset.x;
y += pointerOffset.y - initialOffset.y;
}
const transform = `translate(${x}px, ${y}px)`;
return {
opacity: 0.5,
transform,
WebkitTransform: transform,
};
}
const CustomDragLayer = (props) => {
const {
itemType,
isDragging,
item,
initialOffset,
currentOffset,
pointerOffset,
} = useDragLayer((monitor) => ({
item: monitor.getItem(),
itemType: monitor.getItemType(),
initialOffset: monitor.getInitialSourceClientOffset(),
currentOffset: monitor.getSourceClientOffset(),
pointerOffset: monitor.getInitialClientOffset(),
isDragging: monitor.isDragging(),
}));
const viewMethod = useSelector(
(state) => state.viewUpdate.explorerViewMethod
);
const image = useMemo(() => {
switch (itemType) {
case "object":
return <Preview object={item.object} />;
default:
return null;
}
}, [itemType, item]);
if (!isDragging) {
return null;
}
return (
<div style={layerStyles}>
<div
style={getItemStyles(
initialOffset,
currentOffset,
pointerOffset,
viewMethod
)}
>
{image}
</div>
</div>
);
};
export default CustomDragLayer;

View File

@@ -0,0 +1,49 @@
import React from "react";
import { useDrop } from "react-dnd";
import Folder from "../Folder";
import classNames from "classnames";
import TableItem from "../TableRow";
export default function FolderDropWarpper({
isListView,
folder,
onIconClick,
contextMenu,
handleClick,
handleDoubleClick,
className,
pref,
}) {
const [{ canDrop, isOver }, drop] = useDrop({
accept: "object",
drop: () => ({ folder }),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
const isActive = canDrop && isOver;
if (!isListView) {
return (
<div ref={drop}>
<Folder
folder={folder}
onIconClick={onIconClick}
isActive={isActive}
/>
</div>
);
}
return (
<TableItem
pref={pref}
dref={drop}
className={className}
onIconClick={onIconClick}
contextMenu={contextMenu}
handleClick={handleClick}
handleDoubleClick={handleDoubleClick}
file={folder}
isActive={isActive}
/>
);
}

View File

@@ -0,0 +1,74 @@
import React from "react";
import SmallIcon from "../SmallIcon";
import FileIcon from "../FileIcon";
import { useSelector } from "react-redux";
import { makeStyles } from "@material-ui/core";
import Folder from "../Folder";
const useStyles = makeStyles(() => ({
dragging: {
width: "200px",
},
cardDragged: {
position: "absolute",
"transform-origin": "bottom left",
},
}));
const diliverIcon = (object, viewMethod, classes) => {
if (object.type === "dir") {
return (
<div className={classes.dragging}>
<SmallIcon file={object} isFolder />
</div>
);
}
if (object.type === "file" && viewMethod === "icon") {
return (
<div className={classes.dragging}>
<FileIcon file={object} />
</div>
);
}
if (
(object.type === "file" && viewMethod === "smallIcon") ||
viewMethod === "list"
) {
return (
<div className={classes.dragging}>
<SmallIcon file={object} />
</div>
);
}
};
const Preview = (props) => {
const selected = useSelector((state) => state.explorer.selected);
const viewMethod = useSelector(
(state) => state.viewUpdate.explorerViewMethod
);
const classes = useStyles();
return (
<>
{selected.length === 0 &&
diliverIcon(props.object, viewMethod, classes)}
{selected.length > 0 && (
<>
{selected.slice(0, 3).map((card, i) => (
<div
key={card.id}
className={classes.cardDragged}
style={{
zIndex: selected.length - i,
transform: `rotateZ(${-i * 2.5}deg)`,
}}
>
{diliverIcon(card, viewMethod, classes)}
</div>
))}
</>
)}
</>
);
};
export default Preview;

View File

@@ -0,0 +1,63 @@
import { useRef } from "react";
import { throttle } from "lodash";
const useDragScrolling = () => {
const isScrolling = useRef(false);
const target = document.querySelector("#explorer-container");
const goDown = () => {
target.scrollTop += 10;
const { offsetHeight, scrollTop, scrollHeight } = target;
const isScrollEnd = offsetHeight + scrollTop >= scrollHeight;
if (isScrolling.current && !isScrollEnd) {
window.requestAnimationFrame(goDown);
}
};
const goUp = () => {
target.scrollTop -= 10;
if (isScrolling.current && target.scrollTop > 0) {
window.requestAnimationFrame(goUp);
}
};
const onDragOver = (event) => {
const isMouseOnTop = event.clientY < 100;
const isMouseOnDown = window.innerHeight - event.clientY < 100;
if (!isScrolling.current && (isMouseOnTop || isMouseOnDown)) {
isScrolling.current = true;
if (isMouseOnTop) {
window.requestAnimationFrame(goUp);
}
if (isMouseOnDown) {
window.requestAnimationFrame(goDown);
}
} else if (!isMouseOnTop && !isMouseOnDown) {
isScrolling.current = false;
}
};
const throttleOnDragOver = throttle(onDragOver, 300);
const addEventListenerForWindow = () => {
window.addEventListener("dragover", throttleOnDragOver, false);
};
const removeEventListenerForWindow = () => {
window.removeEventListener("dragover", throttleOnDragOver, false);
isScrolling.current = false;
};
return {
addEventListenerForWindow,
removeEventListenerForWindow,
};
};
export default useDragScrolling;

View File

@@ -0,0 +1,477 @@
import React, { useCallback, useEffect, useMemo } from "react";
import explorer, {
changeContextMenu,
openRemoveDialog,
setSelectedTarget,
} from "../../redux/explorer";
import ObjectIcon from "./ObjectIcon";
import ContextMenu from "./ContextMenu";
import classNames from "classnames";
import ImgPreivew from "./ImgPreview";
import pathHelper from "../../utils/page";
import { isMac } from "../../utils";
import {
CircularProgress,
Grid,
Paper,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Typography,
} from "@material-ui/core";
import { configure, GlobalHotKeys } from "react-hotkeys";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import Nothing from "../Placeholder/Nothing";
import { useDispatch, useSelector } from "react-redux";
import { useLocation } from "react-router";
import { usePagination } from "../../hooks/pagination";
import { makeStyles } from "@material-ui/core/styles";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
paper: {
padding: theme.spacing(2),
textAlign: "center",
color: theme.palette.text.secondary,
margin: "10px",
},
root: {
padding: "10px",
[theme.breakpoints.up("sm")]: {
height: "calc(100vh - 113px)",
},
},
rootTable: {
padding: "0px",
backgroundColor: theme.palette.background.paper.white,
[theme.breakpoints.up("sm")]: {
height: "calc(100vh - 113px)",
},
},
typeHeader: {
margin: "10px 25px",
color: "#6b6b6b",
fontWeight: "500",
},
loading: {
justifyContent: "center",
display: "flex",
marginTop: "40px",
},
errorBox: {
padding: theme.spacing(4),
},
errorMsg: {
marginTop: "10px",
},
hideAuto: {
[theme.breakpoints.down("sm")]: {
display: "none",
},
},
flexFix: {
minWidth: 0,
},
upButton: {
marginLeft: "20px",
marginTop: "10px",
marginBottom: "10px",
},
clickAway: {
height: "100%",
width: "100%",
},
rootShare: {
height: "100%",
minHeight: 500,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
gridContainer: {
[theme.breakpoints.down("sm")]: {
gridTemplateColumns:
"repeat(auto-fill,minmax(180px,1fr))!important",
},
[theme.breakpoints.up("md")]: {
gridTemplateColumns:
"repeat(auto-fill,minmax(220px,1fr))!important",
},
display: "grid!important",
},
gridItem: {
flex: "1 1 220px!important",
},
}));
const keyMap = {
DELETE_FILE: "del",
SELECT_ALL_SHOWED: `${isMac() ? "command" : "ctrl"}+a`,
SELECT_ALL: `${isMac() ? "command" : "ctrl"}+shift+a`,
DESELECT_ALL: "esc",
};
export default function Explorer({ share }) {
const { t } = useTranslation("application", { keyPrefix: "fileManager" });
const location = useLocation();
const dispatch = useDispatch();
const selected = useSelector((state) => state.explorer.selected);
const search = useSelector((state) => state.explorer.search);
const loading = useSelector((state) => state.viewUpdate.navigatorLoading);
const path = useSelector((state) => state.navigator.path);
const sortMethod = useSelector((state) => state.viewUpdate.sortMethod);
const navigatorErrorMsg = useSelector(
(state) => state.viewUpdate.navigatorErrorMsg
);
const navigatorError = useSelector(
(state) => state.viewUpdate.navigatorError
);
const viewMethod = useSelector(
(state) => state.viewUpdate.explorerViewMethod
);
const OpenRemoveDialog = useCallback(() => dispatch(openRemoveDialog()), [
dispatch,
]);
const SetSelectedTarget = useCallback(
(targets) => dispatch(setSelectedTarget(targets)),
[dispatch]
);
const ChangeContextMenu = useCallback(
(type, open) => dispatch(changeContextMenu(type, open)),
[dispatch]
);
const ChangeSortMethod = useCallback(
(method) => dispatch(explorer.actions.changeSortMethod(method)),
[dispatch]
);
const SelectAll = useCallback(
() => dispatch(explorer.actions.selectAll()),
[dispatch]
);
const { dirList, fileList, startIndex } = usePagination();
const handlers = {
DELETE_FILE: () => {
if (selected.length > 0 && !share) {
OpenRemoveDialog();
}
},
SELECT_ALL_SHOWED: (e) => {
e.preventDefault();
if (selected.length >= dirList.length + fileList.length) {
SetSelectedTarget([]);
} else {
SetSelectedTarget([...dirList, ...fileList]);
}
},
SELECT_ALL: (e) => {
e.preventDefault();
SelectAll();
},
DESELECT_ALL: (e) => {
e.preventDefault();
SetSelectedTarget([]);
},
};
useEffect(
() =>
configure({
ignoreTags: ["input", "select", "textarea"],
}),
[]
);
const contextMenu = (e) => {
e.preventDefault();
if (!search && !pathHelper.isSharePage(location.pathname)) {
if (!loading) {
ChangeContextMenu("empty", true);
}
}
};
const ClickAway = (e) => {
const element = e.target;
if (element.dataset.clickaway) {
SetSelectedTarget([]);
}
};
const classes = useStyles();
const isHomePage = pathHelper.isHomePage(location.pathname);
const showView =
!loading && (dirList.length !== 0 || fileList.length !== 0);
const listView = useMemo(
() => (
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell>
<TableSortLabel
active={
sortMethod === "namePos" ||
sortMethod === "nameRev"
}
direction={
sortMethod === "namePos" ? "asc" : "des"
}
onClick={() => {
ChangeSortMethod(
sortMethod === "namePos"
? "nameRev"
: "namePos"
);
}}
>
{t("name")}
{sortMethod === "namePos" ||
sortMethod === "nameRev" ? (
<span className={classes.visuallyHidden}>
{sortMethod === "nameRev"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell className={classes.hideAuto}>
<TableSortLabel
active={
sortMethod === "sizePos" ||
sortMethod === "sizeRes"
}
direction={
sortMethod === "sizePos" ? "asc" : "des"
}
onClick={() => {
ChangeSortMethod(
sortMethod === "sizePos"
? "sizeRes"
: "sizePos"
);
}}
>
{t("size")}
{sortMethod === "sizePos" ||
sortMethod === "sizeRes" ? (
<span className={classes.visuallyHidden}>
{sortMethod === "sizeRes"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell className={classes.hideAuto}>
<TableSortLabel
active={
sortMethod === "modifyTimePos" ||
sortMethod === "modifyTimeRev"
}
direction={
sortMethod === "modifyTimePos"
? "asc"
: "des"
}
onClick={() => {
ChangeSortMethod(
sortMethod === "modifyTimePos"
? "modifyTimeRev"
: "modifyTimePos"
);
}}
>
{t("lastModified")}
{sortMethod === "modifyTimePos" ||
sortMethod === "modifyTimeRev" ? (
<span className={classes.visuallyHidden}>
{sortMethod === "sizeRes"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{pathHelper.isMobile() && path !== "/" && (
<ObjectIcon
file={{
type: "up",
name: t("backToParentFolder"),
}}
/>
)}
{dirList.map((value, index) => (
<ObjectIcon
key={value.id}
file={value}
index={startIndex + index}
/>
))}
{fileList.map((value, index) => (
<ObjectIcon
key={value.id}
file={value}
index={startIndex + dirList.length + index}
/>
))}
</TableBody>
</Table>
),
[dirList, fileList, path, sortMethod, ChangeSortMethod, classes]
);
const normalView = useMemo(
() => (
<div className={classes.flexFix}>
{dirList.length !== 0 && (
<>
<Typography
data-clickAway={"true"}
variant="body2"
className={classes.typeHeader}
>
{t("folders")}
</Typography>
<Grid
data-clickAway={"true"}
container
spacing={0}
alignItems="flex-start"
className={classes.gridContainer}
>
{dirList.map((value, index) => (
<Grid
key={value.id}
item
className={classes.gridItem}
>
<ObjectIcon
key={value.id}
file={value}
index={startIndex + index}
/>
</Grid>
))}
</Grid>
</>
)}
{fileList.length !== 0 && (
<>
<Typography
data-clickAway={"true"}
variant="body2"
className={classes.typeHeader}
>
{t("files")}
</Typography>
<Grid
data-clickAway={"true"}
container
spacing={0}
alignItems="flex-start"
className={classes.gridContainer}
>
{fileList.map((value, index) => (
<Grid
className={classes.gridItem}
key={value.id}
item
>
<ObjectIcon
key={value.id}
index={
startIndex + dirList.length + index
}
file={value}
/>
</Grid>
))}
</Grid>
</>
)}
</div>
),
[dirList, fileList, classes]
);
const view = viewMethod === "list" ? listView : normalView;
return (
<div
onContextMenu={contextMenu}
onClick={ClickAway}
className={classNames(
{
[classes.root]: viewMethod !== "list",
[classes.rootTable]: viewMethod === "list",
[classes.rootShare]: share,
},
classes.button
)}
>
<GlobalHotKeys handlers={handlers} allowChanges keyMap={keyMap} />
<ContextMenu share={share} />
<ImgPreivew />
{navigatorError && (
<Paper elevation={1} className={classes.errorBox}>
<Typography variant="h5" component="h3">
{t("listError")}
</Typography>
<Typography
color={"textSecondary"}
className={classes.errorMsg}
>
{navigatorErrorMsg.message}
</Typography>
</Paper>
)}
{loading && !navigatorError && (
<div className={classes.loading}>
<CircularProgress />
</div>
)}
{!search &&
isHomePage &&
dirList.length === 0 &&
fileList.length === 0 &&
!loading &&
!navigatorError && (
<Nothing
primary={t("dropFileHere")}
secondary={t("orClickUploadButton")}
/>
)}
{((search &&
dirList.length === 0 &&
fileList.length === 0 &&
!loading &&
!navigatorError) ||
(dirList.length === 0 &&
fileList.length === 0 &&
!loading &&
!navigatorError &&
!isHomePage)) && <Nothing primary={t("nothingFound")} />}
{showView && view}
</div>
);
}

View File

@@ -0,0 +1,301 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import classNames from "classnames";
import { LazyLoadImage } from "react-lazy-load-image-component";
import ContentLoader from "react-content-loader";
import { baseURL } from "../../middleware/Api";
import {
ButtonBase,
Divider,
fade,
Tooltip,
Typography,
withStyles,
} from "@material-ui/core";
import TypeIcon from "./TypeIcon";
import { withRouter } from "react-router";
import pathHelper from "../../utils/page";
import statusHelper from "../../utils/page";
import CheckCircleRoundedIcon from "@material-ui/icons/CheckCircleRounded";
import Grow from "@material-ui/core/Grow";
import FileName from "./FileName";
const styles = (theme) => ({
container: {},
selected: {
"&:hover": {
border: "1px solid #d0d0d0",
},
backgroundColor: fade(
theme.palette.primary.main,
theme.palette.type === "dark" ? 0.3 : 0.18
),
},
notSelected: {
"&:hover": {
backgroundColor: theme.palette.background.default,
border: "1px solid #d0d0d0",
},
backgroundColor: theme.palette.background.paper,
},
button: {
border: "1px solid " + theme.palette.divider,
width: "100%",
borderRadius: theme.shape.borderRadius,
boxSizing: "border-box",
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",
alignItems: "initial",
display: "initial",
},
folderNameSelected: {
color:
theme.palette.type === "dark" ? "#fff" : theme.palette.primary.dark,
fontWeight: "500",
},
folderNameNotSelected: {
color: theme.palette.text.secondary,
},
folderName: {
marginTop: "15px",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
marginRight: "20px",
},
preview: {
overflow: "hidden",
height: "150px",
width: "100%",
borderRadius: "12px 12px 0 0",
backgroundColor: theme.palette.background.default,
},
previewIcon: {
overflow: "hidden",
height: "149px",
width: "100%",
borderRadius: "12px 12px 0 0",
backgroundColor: theme.palette.background.paper,
paddingTop: "50px",
},
iconBig: {
fontSize: 50,
},
picPreview: {
objectFit: "cover",
width: "100%",
height: "100%",
},
fileInfo: {
height: "50px",
display: "flex",
},
icon: {
margin: "10px 10px 10px 16px",
height: "30px",
minWidth: "30px",
backgroundColor: theme.palette.background.paper,
borderRadius: "90%",
paddingTop: "3px",
color: theme.palette.text.secondary,
},
hide: {
display: "none",
},
loadingAnimation: {
borderRadius: "12px 12px 0 0",
height: "100%",
width: "100%",
},
shareFix: {
marginLeft: "20px",
},
checkIcon: {
color: theme.palette.primary.main,
},
noDrag: {
userDrag: "none",
},
});
const mapStateToProps = (state) => {
return {
path: state.navigator.path,
selected: state.explorer.selected,
shareInfo: state.viewUpdate.shareInfo,
};
};
const mapDispatchToProps = () => {
return {};
};
class FileIconCompoment extends Component {
static defaultProps = {
share: false,
};
state = {
loading: false,
showPicIcon: false,
};
shouldComponentUpdate(nextProps, nextState, nextContext) {
const isSelectedCurrent =
this.props.selected.findIndex((value) => {
return value === this.props.file;
}) !== -1;
const isSelectedNext =
nextProps.selected.findIndex((value) => {
return value === this.props.file;
}) !== -1;
if (
nextProps.selected !== this.props.selected &&
isSelectedCurrent === isSelectedNext
) {
return false;
}
return true;
}
render() {
const { classes } = this.props;
const isSelected =
this.props.selected.findIndex((value) => {
return value === this.props.file;
}) !== -1;
const isSharePage = pathHelper.isSharePage(
this.props.location.pathname
);
const isMobile = statusHelper.isMobile();
return (
<div className={classes.container}>
<ButtonBase
focusRipple
className={classNames(
{
[classes.selected]: isSelected,
[classes.notSelected]: !isSelected,
},
classes.button
)}
>
{this.props.file.thumb && !this.state.showPicIcon && (
<div className={classes.preview}>
<LazyLoadImage
className={classNames(
{
[classes.hide]: this.state.loading,
[classes.picPreview]:
!this.state.loading,
},
classes.noDrag
)}
src={
baseURL +
(isSharePage && this.props.shareInfo
? "/share/thumb/" +
this.props.shareInfo.key +
"/" +
this.props.file.id +
"?path=" +
encodeURIComponent(
this.props.file.path
)
: "/file/thumb/" + this.props.file.id)
}
afterLoad={() =>
this.setState({ loading: false })
}
beforeLoad={() =>
this.setState({ loading: true })
}
onError={() =>
this.setState({ showPicIcon: true })
}
/>
<ContentLoader
height={150}
width={170}
className={classNames(
{
[classes.hide]: !this.state.loading,
},
classes.loadingAnimation
)}
>
<rect x="0" y="0" width="100%" height="150" />
</ContentLoader>
</div>
)}
{(!this.props.file.thumb || this.state.showPicIcon) && (
<div className={classes.previewIcon}>
<TypeIcon
className={classes.iconBig}
fileName={this.props.file.name}
/>
</div>
)}
{(!this.props.file.thumb || this.state.showPicIcon) && (
<Divider />
)}
<div className={classes.fileInfo}>
{!this.props.share && (
<div
onClick={this.props.onIconClick}
className={classNames(classes.icon, {
[classes.iconSelected]: isSelected,
[classes.iconNotSelected]: !isSelected,
})}
>
{!isSelected && (
<TypeIcon fileName={this.props.file.name} />
)}
{isSelected && (
<Grow in={isSelected}>
<CheckCircleRoundedIcon
className={classes.checkIcon}
/>
</Grow>
)}
</div>
)}
<Tooltip
title={this.props.file.name}
aria-label={this.props.file.name}
>
<Typography
variant="body2"
className={classNames(classes.folderName, {
[classes.folderNameSelected]: isSelected,
[classes.folderNameNotSelected]: !isSelected,
[classes.shareFix]: this.props.share,
})}
>
<FileName name={this.props.file.name} />
</Typography>
</Tooltip>
</div>
</ButtonBase>
</div>
);
}
}
FileIconCompoment.propTypes = {
classes: PropTypes.object.isRequired,
file: PropTypes.object.isRequired,
};
const FileIcon = connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(withRouter(FileIconCompoment)));
export default FileIcon;

View File

@@ -0,0 +1,120 @@
import React, { Component } from "react";
import Navigator from "./Navigator/Navigator";
import { DndProvider } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
import DragLayer from "./DnD/DragLayer";
import Explorer from "./Explorer";
import Modals from "./Modals";
import { connect } from "react-redux";
import { changeSubTitle } from "../../redux/viewUpdate/action";
import { withRouter } from "react-router-dom";
import pathHelper from "../../utils/page";
import SideDrawer from "./Sidebar/SideDrawer";
import classNames from "classnames";
//import { ImageLoader } from "@abslant/cd-image-loader";
import {
closeAllModals,
navigateTo,
setSelectedTarget,
toggleSnackbar,
} from "../../redux/explorer";
import PaginationFooter from "./Pagination";
import withStyles from "@material-ui/core/styles/withStyles";
const styles = (theme) => ({
root: {
display: "flex",
flexDirection: "column",
height: "calc(100vh - 64px)",
[theme.breakpoints.down("xs")]: {
height: "100%",
},
},
rootShare: {
display: "flex",
flexDirection: "column",
height: "100%",
minHeight: 500,
},
explorer: {
display: "flex",
flexDirection: "column",
overflowY: "auto",
},
});
const mapStateToProps = () => ({});
const mapDispatchToProps = (dispatch) => {
return {
changeSubTitle: (text) => {
dispatch(changeSubTitle(text));
},
setSelectedTarget: (targets) => {
dispatch(setSelectedTarget(targets));
},
toggleSnackbar: (vertical, horizontal, msg, color) => {
dispatch(toggleSnackbar(vertical, horizontal, msg, color));
},
closeAllModals: () => {
dispatch(closeAllModals());
},
navigateTo: (path) => {
dispatch(navigateTo(path));
},
};
};
class FileManager extends Component {
constructor(props) {
super(props);
this.image = React.createRef();
}
componentWillUnmount() {
this.props.setSelectedTarget([]);
this.props.closeAllModals();
this.props.navigateTo("/");
}
componentDidMount() {
if (pathHelper.isHomePage(this.props.location.pathname)) {
this.props.changeSubTitle(null);
}
}
render() {
const { classes } = this.props;
return (
<div
className={classNames({
[classes.rootShare]: this.props.share,
[classes.root]: !this.props.share,
})}
>
<DndProvider backend={HTML5Backend}>
<Modals share={this.props.share} />
<Navigator
isShare={this.props.isShare}
share={this.props.share}
/>
<div className={classes.explorer} id={"explorer-container"}>
<Explorer share={this.props.share} />
<PaginationFooter />
</div>
<DragLayer />
</DndProvider>
<SideDrawer />
</div>
);
}
}
FileManager.propTypes = {};
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(withRouter(FileManager)));

View File

@@ -0,0 +1,28 @@
import Highlighter from "react-highlight-words";
import { trimPrefix } from "../Uploader/core/utils";
import React from "react";
import { useSelector } from "react-redux";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles((theme) => ({
highlight: {
backgroundColor: theme.palette.warning.light,
},
}));
export default function FileName({ name }) {
const classes = useStyles();
const search = useSelector((state) => state.explorer.search);
if (!search) {
return name;
}
return (
<Highlighter
highlightClassName={classes.highlight}
searchWords={trimPrefix(search.keywords, "keywords/").split(" ")}
autoEscape={true}
textToHighlight={name}
/>
);
}

View File

@@ -0,0 +1,128 @@
import React from "react";
import FolderIcon from "@material-ui/icons/Folder";
import classNames from "classnames";
import {
ButtonBase,
fade,
makeStyles,
Tooltip,
Typography,
} from "@material-ui/core";
import { useSelector } from "react-redux";
import statusHelper from "../../utils/page";
import CheckCircleRoundedIcon from "@material-ui/icons/CheckCircleRounded";
const useStyles = makeStyles((theme) => ({
container: {
padding: "7px",
},
selected: {
"&:hover": {
border: "1px solid #d0d0d0",
},
backgroundColor: fade(
theme.palette.primary.main,
theme.palette.type === "dark" ? 0.3 : 0.18
),
},
notSelected: {
"&:hover": {
backgroundColor: theme.palette.background.default,
border: "1px solid #d0d0d0",
},
backgroundColor: theme.palette.background.paper,
},
button: {
height: "50px",
border: "1px solid " + theme.palette.divider,
width: "100%",
borderRadius: theme.shape.borderRadius,
boxSizing: "border-box",
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",
display: "flex",
justifyContent: "left",
alignItems: "initial",
},
icon: {
margin: "10px 10px 10px 16px",
height: "30px",
minWidth: "30px",
backgroundColor: theme.palette.background.paper,
borderRadius: "90%",
paddingTop: "3px",
color: theme.palette.text.secondary,
},
folderNameSelected: {
color:
theme.palette.type === "dark" ? "#fff" : theme.palette.primary.dark,
fontWeight: "500",
},
folderNameNotSelected: {
color: theme.palette.text.secondary,
},
folderName: {
marginTop: "15px",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
marginRight: "20px",
},
active: {
boxShadow: "0 0 0 2px " + theme.palette.primary.light,
},
checkIcon: {
color: theme.palette.primary.main,
},
}));
export default function Folder({ folder, isActive, onIconClick }) {
const selected = useSelector((state) => state.explorer.selected);
const classes = useStyles();
const isMobile = statusHelper.isMobile();
const isSelected =
selected.findIndex((value) => {
return value === folder;
}) !== -1;
return (
<ButtonBase
focusRipple
className={classNames(
{
[classes.selected]: isSelected,
[classes.notSelected]: !isSelected,
[classes.active]: isActive,
},
classes.button
)}
>
<div
onClick={onIconClick}
className={classNames(classes.icon, {
[classes.iconSelected]: isSelected,
[classes.iconNotSelected]: !isSelected,
})}
>
{!isSelected && <FolderIcon />}
{isSelected && (
<CheckCircleRoundedIcon className={classes.checkIcon} />
)}
</div>
<Tooltip title={folder.name} aria-label={folder.name}>
<Typography
variant="body2"
className={classNames(classes.folderName, {
[classes.folderNameSelected]: isSelected,
[classes.folderNameNotSelected]: !isSelected,
})}
>
{folder.name}
</Typography>
</Tooltip>
</ButtonBase>
);
}

View File

@@ -0,0 +1,137 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { baseURL } from "../../middleware/Api";
import { imgPreviewSuffix } from "../../config";
import { withStyles } from "@material-ui/core";
import pathHelper from "../../utils/page";
import { withRouter } from "react-router";
import { PhotoSlider } from "react-photo-view";
import "react-photo-view/dist/index.css";
import * as explorer from "../../redux/explorer/reducer";
import { showImgPreivew } from "../../redux/explorer";
const styles = () => ({});
const mapStateToProps = (state) => {
return {
first: state.explorer.imgPreview.first,
other: state.explorer.imgPreview.other,
};
};
const mapDispatchToProps = (dispatch) => {
return {
showImgPreivew: (first) => {
dispatch(showImgPreivew(first));
},
};
};
class ImagPreviewComponent extends Component {
state = {
items: [],
photoIndex: 0,
isOpen: false,
};
UNSAFE_componentWillReceiveProps = (nextProps) => {
const items = [];
let firstOne = 0;
if (nextProps.first.id !== "") {
if (
pathHelper.isSharePage(this.props.location.pathname) &&
!nextProps.first.path
) {
const newImg = {
intro: nextProps.first.name,
src: baseURL + "/share/preview/" + nextProps.first.key,
};
firstOne = 0;
items.push(newImg);
this.setState({
photoIndex: firstOne,
items: items,
isOpen: true,
});
return;
}
// eslint-disable-next-line
nextProps.other.map((value) => {
const fileType = value.name.split(".").pop().toLowerCase();
if (imgPreviewSuffix.indexOf(fileType) !== -1) {
let src = "";
if (pathHelper.isSharePage(this.props.location.pathname)) {
src = baseURL + "/share/preview/" + value.key;
src =
src +
"?path=" +
encodeURIComponent(
value.path === "/"
? value.path + value.name
: value.path + "/" + value.name
);
} else {
src = baseURL + "/file/preview/" + value.id;
}
const newImg = {
intro: value.name,
src: src,
};
if (
value.path === nextProps.first.path &&
value.name === nextProps.first.name
) {
firstOne = items.length;
}
items.push(newImg);
}
});
this.setState({
photoIndex: firstOne,
items: items,
isOpen: true,
});
}
};
handleClose = () => {
this.props.showImgPreivew(explorer.initState.imgPreview.first);
this.setState({
isOpen: false,
});
};
render() {
const { photoIndex, isOpen, items } = this.state;
return (
<div>
{isOpen && (
<PhotoSlider
images={items}
visible={isOpen}
onClose={() => this.handleClose()}
index={photoIndex}
onIndexChange={(n) =>
this.setState({
photoIndex: n,
})
}
/>
)}
</div>
);
}
}
ImagPreviewComponent.propTypes = {
classes: PropTypes.object.isRequired,
};
const ImgPreivew = connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(withRouter(ImagPreviewComponent)));
export default ImgPreivew;

View File

@@ -0,0 +1,793 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import PathSelector from "./PathSelector";
import API from "../../middleware/Api";
import {
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
TextField,
withStyles,
} from "@material-ui/core";
import Loading from "../Modals/Loading";
import CopyDialog from "../Modals/Copy";
import DirectoryDownloadDialog from "../Modals/DirectoryDownload";
import CreatShare from "../Modals/CreateShare";
import { withRouter } from "react-router-dom";
import PurchaseShareDialog from "../Modals/PurchaseShare";
import DecompressDialog from "../Modals/Decompress";
import CompressDialog from "../Modals/Compress";
import RelocateDialog from "../Modals/Relocate";
import {
closeAllModals,
openLoadingDialog,
refreshFileList,
refreshStorage,
setModalsLoading,
toggleSnackbar,
} from "../../redux/explorer";
import OptionSelector from "../Modals/OptionSelector";
import { Trans, withTranslation } from "react-i18next";
import RemoteDownload from "../Modals/RemoteDownload";
import Delete from "../Modals/Delete";
const styles = (theme) => ({
wrapper: {
margin: theme.spacing(1),
position: "relative",
},
buttonProgress: {
color: theme.palette.secondary.light,
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
contentFix: {
padding: "10px 24px 0px 24px",
},
});
const mapStateToProps = (state) => {
return {
path: state.navigator.path,
selected: state.explorer.selected,
modalsStatus: state.viewUpdate.modals,
modalsLoading: state.viewUpdate.modalsLoading,
dirList: state.explorer.dirList,
fileList: state.explorer.fileList,
dndSignale: state.explorer.dndSignal,
dndTarget: state.explorer.dndTarget,
dndSource: state.explorer.dndSource,
loading: state.viewUpdate.modals.loading,
loadingText: state.viewUpdate.modals.loadingText,
};
};
const mapDispatchToProps = (dispatch) => {
return {
closeAllModals: () => {
dispatch(closeAllModals());
},
toggleSnackbar: (vertical, horizontal, msg, color) => {
dispatch(toggleSnackbar(vertical, horizontal, msg, color));
},
setModalsLoading: (status) => {
dispatch(setModalsLoading(status));
},
refreshFileList: () => {
dispatch(refreshFileList());
},
refreshStorage: () => {
dispatch(refreshStorage());
},
openLoadingDialog: (text) => {
dispatch(openLoadingDialog(text));
},
};
};
class ModalsCompoment extends Component {
state = {
newFolderName: "",
newFileName: "",
newName: "",
selectedPath: "",
selectedPathName: "",
secretShare: false,
sharePwd: "",
shareUrl: "",
purchaseCallback: null,
};
handleInputChange = (e) => {
this.setState({
[e.target.id]: e.target.value,
});
};
newNameSuffix = "";
UNSAFE_componentWillReceiveProps = (nextProps) => {
if (this.props.dndSignale !== nextProps.dndSignale) {
this.dragMove(nextProps.dndSource, nextProps.dndTarget);
return;
}
if (this.props.modalsStatus.rename !== nextProps.modalsStatus.rename) {
const name = nextProps.selected[0].name;
this.setState({
newName: name,
});
return;
}
};
submitResave = (e) => {
e.preventDefault();
this.props.setModalsLoading(true);
API.post("/share/save/" + window.shareKey, {
path:
this.state.selectedPath === "//"
? "/"
: this.state.selectedPath,
})
.then(() => {
this.onClose();
this.props.toggleSnackbar(
"top",
"right",
this.props.t("vas.fileSaved"),
"success"
);
this.props.refreshFileList();
this.props.setModalsLoading(false);
})
.catch((error) => {
this.props.toggleSnackbar(
"top",
"right",
error.message,
"error"
);
this.props.setModalsLoading(false);
});
};
submitMove = (e) => {
if (e != null) {
e.preventDefault();
}
this.props.setModalsLoading(true);
const dirs = [],
items = [];
// eslint-disable-next-line
this.props.selected.map((value) => {
if (value.type === "dir") {
dirs.push(value.id);
} else {
items.push(value.id);
}
});
API.patch("/object", {
action: "move",
src_dir: this.props.selected[0].path,
src: {
dirs: dirs,
items: items,
},
dst: this.DragSelectedPath
? this.DragSelectedPath
: this.state.selectedPath === "//"
? "/"
: this.state.selectedPath,
})
.then(() => {
this.onClose();
this.props.refreshFileList();
this.props.setModalsLoading(false);
this.DragSelectedPath = "";
})
.catch((error) => {
this.props.toggleSnackbar(
"top",
"right",
error.message,
"error"
);
this.props.setModalsLoading(false);
this.DragSelectedPath = "";
})
.then(() => {
this.props.closeAllModals();
});
};
dragMove = (source, target) => {
if (this.props.selected.length === 0) {
this.props.selected[0] = source;
}
let doMove = true;
// eslint-disable-next-line
this.props.selected.map((value) => {
// 根据ID过滤
if (value.id === target.id && value.type === target.type) {
doMove = false;
// eslint-disable-next-line
return;
}
// 根据路径过滤
if (
value.path ===
target.path + (target.path === "/" ? "" : "/") + target.name
) {
doMove = false;
// eslint-disable-next-line
return;
}
});
if (doMove) {
this.DragSelectedPath =
target.path === "/"
? target.path + target.name
: target.path + "/" + target.name;
this.props.openLoadingDialog(this.props.t("modals.processing"));
this.submitMove();
}
};
submitRename = (e) => {
e.preventDefault();
this.props.setModalsLoading(true);
const newName = this.state.newName;
const src = {
dirs: [],
items: [],
};
if (this.props.selected[0].type === "dir") {
src.dirs[0] = this.props.selected[0].id;
} else {
src.items[0] = this.props.selected[0].id;
}
// 检查重名
if (
this.props.dirList.findIndex((value) => {
return value.name === newName;
}) !== -1 ||
this.props.fileList.findIndex((value) => {
return value.name === newName;
}) !== -1
) {
this.props.toggleSnackbar(
"top",
"right",
this.props.t("modals.duplicatedObjectName"),
"warning"
);
this.props.setModalsLoading(false);
} else {
API.post("/object/rename", {
action: "rename",
src: src,
new_name: newName,
})
.then(() => {
this.onClose();
this.props.refreshFileList();
this.props.setModalsLoading(false);
})
.catch((error) => {
this.props.toggleSnackbar(
"top",
"right",
error.message,
"error"
);
this.props.setModalsLoading(false);
});
}
};
submitCreateNewFolder = (e) => {
e.preventDefault();
this.props.setModalsLoading(true);
if (
this.props.dirList.findIndex((value) => {
return value.name === this.state.newFolderName;
}) !== -1
) {
this.props.toggleSnackbar(
"top",
"right",
this.props.t("modals.duplicatedFolderName"),
"warning"
);
this.props.setModalsLoading(false);
} else {
API.put("/directory", {
path:
(this.props.path === "/" ? "" : this.props.path) +
"/" +
this.state.newFolderName,
})
.then(() => {
this.onClose();
this.props.refreshFileList();
this.props.setModalsLoading(false);
})
.catch((error) => {
this.props.setModalsLoading(false);
this.props.toggleSnackbar(
"top",
"right",
error.message,
"error"
);
});
}
//this.props.toggleSnackbar();
};
submitCreateNewFile = (e) => {
e.preventDefault();
this.props.setModalsLoading(true);
if (
this.props.dirList.findIndex((value) => {
return value.name === this.state.newFileName;
}) !== -1
) {
this.props.toggleSnackbar(
"top",
"right",
this.props.t("modals.duplicatedFolderName"),
"warning"
);
this.props.setModalsLoading(false);
} else {
API.post("/file/create", {
path:
(this.props.path === "/" ? "" : this.props.path) +
"/" +
this.state.newFileName,
})
.then(() => {
this.onClose();
this.props.refreshFileList();
this.props.setModalsLoading(false);
})
.catch((error) => {
this.props.setModalsLoading(false);
this.props.toggleSnackbar(
"top",
"right",
error.message,
"error"
);
});
}
//this.props.toggleSnackbar();
};
setMoveTarget = (folder) => {
const path =
folder.path === "/"
? folder.path + folder.name
: folder.path + "/" + folder.name;
this.setState({
selectedPath: path,
selectedPathName: folder.name,
});
};
onClose = () => {
this.setState({
newFolderName: "",
newFileName: "",
newName: "",
selectedPath: "",
selectedPathName: "",
secretShare: false,
sharePwd: "",
shareUrl: "",
});
this.newNameSuffix = "";
this.props.closeAllModals();
};
handleChange = (name) => (event) => {
this.setState({ [name]: event.target.checked });
};
copySource = () => {
if (navigator.clipboard) {
navigator.clipboard.writeText(this.props.modalsStatus.getSource);
this.props.toggleSnackbar(
"top",
"right",
this.props.t("modals.linkCopied"),
"info"
);
}
};
render() {
const { classes, t } = this.props;
return (
<div>
<Loading />
<OptionSelector />
<PurchaseShareDialog />
<Dialog
open={this.props.modalsStatus.getSource}
onClose={this.onClose}
aria-labelledby="form-dialog-title"
fullWidth
>
<DialogTitle id="form-dialog-title">
{t("modals.getSourceLinkTitle")}
</DialogTitle>
<DialogContent>
<TextField
autoFocus
inputProps={{ readonly: true }}
label={t("modals.sourceLink")}
multiline
value={this.props.modalsStatus.getSource}
variant="outlined"
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={this.copySource} color="secondary">
{t("copyToClipboard", { ns: "common" })}
</Button>
<Button onClick={this.onClose}>
{t("close", { ns: "common" })}
</Button>
</DialogActions>
</Dialog>
<Dialog
open={this.props.modalsStatus.createNewFolder}
onClose={this.onClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">
{t("fileManager.newFolder")}
</DialogTitle>
<DialogContent>
<form onSubmit={this.submitCreateNewFolder}>
<TextField
autoFocus
margin="dense"
id="newFolderName"
label={t("modals.folderName")}
type="text"
value={this.state.newFolderName}
onChange={(e) => this.handleInputChange(e)}
fullWidth
/>
</form>
</DialogContent>
<DialogActions>
<Button onClick={this.onClose}>
{t("cancel", { ns: "common" })}
</Button>
<div className={classes.wrapper}>
<Button
onClick={this.submitCreateNewFolder}
color="primary"
disabled={
this.state.newFolderName === "" ||
this.props.modalsLoading
}
>
{t("modals.create")}
{this.props.modalsLoading && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</div>
</DialogActions>
</Dialog>
<Dialog
open={this.props.modalsStatus.createNewFile}
onClose={this.onClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">
{t("fileManager.newFile")}
</DialogTitle>
<DialogContent>
<form onSubmit={this.submitCreateNewFile}>
<TextField
autoFocus
margin="dense"
id="newFileName"
label={t("modals.fileName")}
type="text"
value={this.state.newFileName}
onChange={(e) => this.handleInputChange(e)}
fullWidth
/>
</form>
</DialogContent>
<DialogActions>
<Button onClick={this.onClose}>
{t("cancel", { ns: "common" })}
</Button>
<div className={classes.wrapper}>
<Button
onClick={this.submitCreateNewFile}
color="primary"
disabled={
this.state.newFileName === "" ||
this.props.modalsLoading
}
>
{t("modals.create")}
{this.props.modalsLoading && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</div>
</DialogActions>
</Dialog>
<Dialog
open={this.props.modalsStatus.rename}
onClose={this.onClose}
aria-labelledby="form-dialog-title"
maxWidth="sm"
fullWidth={true}
>
<DialogTitle id="form-dialog-title">
{t("fileManager.rename")}
</DialogTitle>
<DialogContent>
<DialogContentText>
<Trans
i18nKey="modals.renameDescription"
values={{
name:
this.props.selected.length === 1
? this.props.selected[0].name
: "",
}}
components={[<strong key={0} />]}
/>
</DialogContentText>
<form onSubmit={this.submitRename}>
<TextField
autoFocus
margin="dense"
id="newName"
label={t("modals.newName")}
type="text"
value={this.state.newName}
onChange={(e) => this.handleInputChange(e)}
fullWidth
/>
</form>
</DialogContent>
<DialogActions>
<Button onClick={this.onClose}>
{t("cancel", { ns: "common" })}
</Button>
<div className={classes.wrapper}>
<Button
onClick={this.submitRename}
color="primary"
disabled={
this.state.newName === "" ||
this.props.modalsLoading
}
>
{t("ok", { ns: "common" })}
{this.props.modalsLoading && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</div>
</DialogActions>
</Dialog>
<CopyDialog
open={this.props.modalsStatus.copy}
onClose={this.onClose}
presentPath={this.props.path}
selected={this.props.selected}
modalsLoading={this.props.modalsLoading}
/>
<Dialog
open={this.props.modalsStatus.move}
onClose={this.onClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">
{t("modals.moveToTitle")}
</DialogTitle>
<PathSelector
presentPath={this.props.path}
selected={this.props.selected}
onSelect={this.setMoveTarget}
/>
{this.state.selectedPath !== "" && (
<DialogContent className={classes.contentFix}>
<DialogContentText>
<Trans
i18nKey="modals.moveToDescription"
values={{
name: this.state.selectedPathName,
}}
components={[<strong key={0} />]}
/>
</DialogContentText>
</DialogContent>
)}
<DialogActions>
<Button onClick={this.onClose}>
{t("cancel", { ns: "common" })}
</Button>
<div className={classes.wrapper}>
<Button
onClick={this.submitMove}
color="primary"
disabled={
this.state.selectedPath === "" ||
this.props.modalsLoading
}
>
{t("ok", { ns: "common" })}
{this.props.modalsLoading && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</div>
</DialogActions>
</Dialog>
<Dialog
open={this.props.modalsStatus.resave}
onClose={this.onClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">
{t("modals.saveToTitle")}
</DialogTitle>
<PathSelector
presentPath={this.props.path}
selected={this.props.selected}
onSelect={this.setMoveTarget}
/>
{this.state.selectedPath !== "" && (
<DialogContent className={classes.contentFix}>
<DialogContentText>
<Trans
i18nKey="modals.saveToTitleDescription"
values={{
name: this.state.selectedPathName,
}}
components={[<strong key={0} />]}
/>
</DialogContentText>
</DialogContent>
)}
<DialogActions>
<Button onClick={this.onClose}>
{t("cancel", { ns: "common" })}
</Button>
<div className={classes.wrapper}>
<Button
onClick={this.submitResave}
color="primary"
disabled={
this.state.selectedPath === "" ||
this.props.modalsLoading
}
>
{t("ok", { ns: "common" })}
{this.props.modalsLoading && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</div>
</DialogActions>
</Dialog>
<Delete
open={this.props.modalsStatus.remove}
onClose={this.onClose}
modalsLoading={this.props.modalsLoading}
setModalsLoading={this.props.setModalsLoading}
selected={this.props.selected}
refreshFileList={this.props.refreshFileList}
refreshStorage={this.props.refreshStorage}
/>
<CreatShare
open={this.props.modalsStatus.share}
onClose={this.onClose}
modalsLoading={this.props.modalsLoading}
setModalsLoading={this.props.setModalsLoading}
selected={this.props.selected}
/>
<RemoteDownload
open={this.props.modalsStatus.remoteDownload}
onClose={this.onClose}
modalsLoading={this.props.modalsLoading}
setModalsLoading={this.props.setModalsLoading}
presentPath={this.props.path}
torrent={this.props.modalsStatus.remoteDownloadTorrent}
/>
<DecompressDialog
open={this.props.modalsStatus.decompress}
onClose={this.onClose}
presentPath={this.props.path}
selected={this.props.selected}
modalsLoading={this.props.modalsLoading}
/>
<CompressDialog
open={this.props.modalsStatus.compress}
onClose={this.onClose}
presentPath={this.props.path}
selected={this.props.selected}
modalsLoading={this.props.modalsLoading}
/>
<RelocateDialog
open={this.props.modalsStatus.relocate}
onClose={this.onClose}
selected={this.props.selected}
modalsLoading={this.props.modalsLoading}
/>
<DirectoryDownloadDialog
open={this.props.modalsStatus.directoryDownloading}
onClose={this.onClose}
done={this.props.modalsStatus.directoryDownloadDone}
log={this.props.modalsStatus.directoryDownloadLog}
/>
</div>
);
}
}
ModalsCompoment.propTypes = {
classes: PropTypes.object.isRequired,
};
const Modals = connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(withRouter(withTranslation()(ModalsCompoment))));
export default Modals;

View File

@@ -0,0 +1,525 @@
import {
Button,
Card,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Grid,
List,
Popover,
Slider,
withStyles,
} from "@material-ui/core";
import IconButton from "@material-ui/core/IconButton";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import MusicNote from "@material-ui/icons/MusicNote";
import PlayArrow from "@material-ui/icons/PlayArrow";
import PlayNext from "@material-ui/icons/SkipNext";
import PlayPrev from "@material-ui/icons/SkipPrevious";
import Pause from "@material-ui/icons/Pause";
import VolumeUp from '@material-ui/icons/VolumeUp';
import VolumeDown from '@material-ui/icons/VolumeDown';
import VolumeMute from '@material-ui/icons/VolumeMute';
import VolumeOff from '@material-ui/icons/VolumeOff';
import { Repeat, RepeatOne, Shuffle } from "@material-ui/icons";
import PropTypes from "prop-types";
import React, { Component } from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router";
import { audioPreviewSuffix } from "../../config";
import { baseURL } from "../../middleware/Api";
import * as explorer from "../../redux/explorer/reducer";
import pathHelper from "../../utils/page";
import {
audioPreviewSetIsOpen,
audioPreviewSetPlaying,
showAudioPreview,
} from "../../redux/explorer";
import { withTranslation } from "react-i18next";
const styles = (theme) => ({
list: {
//maxWidth: 360,
backgroundColor: theme.palette.background.paper,
position: "relative",
overflow: "auto",
maxHeight: 300,
},
slider_root: {
"vertical-align": "middle",
},
setvol: {
width: 200,
height: 28,
"line-height": "42px",
},
});
const mapStateToProps = (state) => {
return {
first: state.explorer.audioPreview.first,
other: state.explorer.audioPreview.other,
isOpen: state.explorer.audioPreview.isOpen,
playingName: state.explorer.audioPreview.playingName,
};
};
const mapDispatchToProps = (dispatch) => {
return {
showAudioPreview: (first) => {
dispatch(showAudioPreview(first));
},
audioPreviewSetIsOpen: (first) => {
dispatch(audioPreviewSetIsOpen(first));
},
audioPreviewSetPlaying: (playingName, paused) => {
dispatch(audioPreviewSetPlaying(playingName, paused));
},
};
};
class MusicPlayerComponent extends Component {
state = {
items: [],
currentIndex: 0,
//isOpen: false,
isPlay: false,
currentTime: 0,
duration: 0,
progressText: "00:00/00:00",
looptype: 0,
volume: 0.8,
openPropEl: null,
mute: false,
};
myAudioRef = React.createRef();
UNSAFE_componentWillReceiveProps = (nextProps) => {
const items = [];
let firstOne = 0;
if (nextProps.first.id !== "") {
if (
pathHelper.isSharePage(this.props.location.pathname) &&
!nextProps.first.path
) {
const newItem = {
intro: nextProps.first.name,
src: baseURL + "/share/preview/" + nextProps.first.key,
};
firstOne = 0;
items.push(newItem);
this.setState({
currentIndex: firstOne,
items: items,
//isOpen: true,
});
this.props.audioPreviewSetIsOpen(true);
this.props.showAudioPreview(
explorer.initState.audioPreview.first
);
return;
}
// eslint-disable-next-line
nextProps.other.map((value) => {
const fileType = value.name.split(".").pop().toLowerCase();
if (audioPreviewSuffix.indexOf(fileType) !== -1) {
let src = "";
if (pathHelper.isSharePage(this.props.location.pathname)) {
src = baseURL + "/share/preview/" + value.key;
src =
src +
"?path=" +
encodeURIComponent(
value.path === "/"
? value.path + value.name
: value.path + "/" + value.name
);
} else {
src = baseURL + "/file/preview/" + value.id;
}
const newItem = {
intro: value.name,
src: src,
};
if (
value.path === nextProps.first.path &&
value.name === nextProps.first.name
) {
firstOne = items.length;
}
items.push(newItem);
}
});
this.setState({
currentIndex: firstOne,
items: items,
//isOpen: true,
});
this.props.audioPreviewSetIsOpen(true);
this.props.showAudioPreview(explorer.initState.audioPreview.first);
}
};
handleItemClick = (currentIndex) => () => {
this.setState({
currentIndex: currentIndex,
});
};
handleClose = () => {
/*this.setState({
isOpen: false,
});*/
this.setState({
currentIndex: -1,
});
this.pause();
this.props.audioPreviewSetPlaying(null, false);
this.props.audioPreviewSetIsOpen(false);
};
backgroundPlay = () => {
this.props.audioPreviewSetIsOpen(false);
};
componentDidMount() {
if (this.myAudioRef.current) {
this.bindEvents(this.myAudioRef.current);
}
}
componentDidUpdate() {
if (this.myAudioRef.current) {
this.bindEvents(this.myAudioRef.current);
}
}
componentWillUnmount() {
this.unbindEvents(this.myAudioRef.current);
}
bindEvents = (ele) => {
if (ele) {
ele.addEventListener("canplay", this.readyPlay);
ele.addEventListener("ended", this.loopnext);
ele.addEventListener("timeupdate", this.timeUpdate);
}
};
unbindEvents = (ele) => {
if (ele) {
ele.removeEventListener("canplay", this.readyPlay);
ele.removeEventListener("ended", this.loopnext);
ele.removeEventListener("timeupdate", this.timeUpdate);
}
};
readyPlay = () => {
this.myAudioRef.current.volume = this.state.volume;
this.play();
};
formatTime = (s) => {
if (isNaN(s)) return "00:00";
const minute = Math.floor(s / 60);
const second = Math.floor(s % 60);
return (
`${minute}`.padStart(2, "0") + ":" + `${second}`.padStart(2, "0")
);
};
timeUpdate = () => {
const currentTime = Math.floor(this.myAudioRef.current.currentTime); //this.myAudioRef.current.currentTime;//
this.setState({
currentTime: currentTime,
duration: this.myAudioRef.current.duration,
progressText:
this.formatTime(currentTime) +
"/" +
this.formatTime(this.myAudioRef.current.duration),
});
};
play = () => {
this.myAudioRef.current.play();
this.setState({
isPlay: true
});
this.props.audioPreviewSetPlaying(
this.state.items[this.state.currentIndex].intro,
false
);
};
pause = () => {
if (this.myAudioRef.current) {
this.myAudioRef.current.pause();
}
this.setState({
isPlay: false
})
this.props.audioPreviewSetPlaying(
this.state.items[this.state.currentIndex]?.intro,
true
);
};
playOrPaues = () => {
if (this.state.isPlay) {
this.pause();
} else {
this.play();
}
};
changeLoopType = () => {
let lt = this.state.looptype + 1;
if (lt >= 3) {
lt = 0;
}
this.setState({
looptype: lt,
});
};
loopnext = () => {
let index = this.state.currentIndex;
if (this.state.looptype == 0) {
//all
index = index + 1;
if (index >= this.state.items.length) {
index = 0;
}
} else if (this.state.looptype == 1) {
//single
//index=index;
} else if (this.state.looptype == 2) {
//random
if (this.state.items.length <= 2) {
index = index + 1;
if (index >= this.state.items.length) {
index = 0;
}
} else {
while (index == this.state.currentIndex) {
index = Math.floor(Math.random() * this.state.items.length);
}
}
}
if (this.state.currentIndex == index) {
this.myAudioRef.current.currentTime = 0;
this.play();
}
this.setState({
currentIndex: index,
});
};
prev = () => {
let index = this.state.currentIndex - 1;
if (index < 0) {
index = this.state.items.length - 1;
}
this.setState({
currentIndex: index,
});
};
next = () => {
let index = this.state.currentIndex + 1;
if (index >= this.state.items.length) {
index = 0;
}
this.setState({
currentIndex: index,
});
};
handleProgress = (e, newValue) => {
this.myAudioRef.current.currentTime = newValue;
};
render() {
const { currentIndex, items } = this.state;
const { isOpen, classes, t } = this.props;
return (
<Dialog
open={isOpen}
onClose={this.backgroundPlay}
aria-labelledby="form-dialog-title"
maxWidth="xs"
fullWidth
keepMounted
>
<DialogTitle id="form-dialog-title">
{t("fileManager.musicPlayer")}
</DialogTitle>
<DialogContent>
<List className={classes.list} dense>
{items.map((value, idx) => {
const labelId = `label-${value.intro}`;
return (
<ListItem
key={value.src}
dense
button
onClick={this.handleItemClick(idx)}
selected={idx === currentIndex}
>
<ListItemIcon>
{idx === currentIndex ? (
<PlayArrow />
) : (
<MusicNote />
)}
</ListItemIcon>
<ListItemText
id={labelId}
primary={`${value.intro}`}
/>
</ListItem>
);
})}
</List>
<audio
ref={this.myAudioRef}
src={items[currentIndex]?.src}
/>
<div style={{ "padding-top": 8 }} />
<Grid container spacing={2} alignItems="center">
<Grid item xs>
<Slider
classes={{ root: classes.slider_root }}
value={this.state.currentTime}
onChange={this.handleProgress}
step={1}
min={0}
max={this.state.duration}
aria-labelledby="continuous-slider"
/>
</Grid>
<Grid item>{this.state.progressText}</Grid>
</Grid>
<Grid
container
spacing={2}
justifyContent="center"
justify="center"
>
<Grid item>
<IconButton
edge="end"
aria-label=""
onClick={this.changeLoopType}
>
{this.state.looptype === 0 ? (
<Repeat />
) : this.state.looptype === 1 ? (
<RepeatOne />
) : (
<Shuffle />
)}
</IconButton>
</Grid>
<Grid item>
<IconButton
edge="end"
aria-label=""
onClick={this.prev}
>
<PlayPrev />
</IconButton>
</Grid>
<Grid item>
<IconButton
edge="end"
aria-label=""
onClick={this.playOrPaues}
>
{this.state.isPlay ? (<Pause />) : (<PlayArrow />)}
</IconButton>
</Grid>
<Grid item>
<IconButton
edge="end"
aria-label=""
onClick={this.next}
>
<PlayNext />
</IconButton>
</Grid>
<Grid item>
<IconButton
edge="end"
aria-label=""
onClick={(e) => { this.setState({ openPropEl: e.currentTarget }) }}
onContextMenu={(e) => {
e.preventDefault();
if (this.state.mute) {
this.setState({ mute: false });
this.myAudioRef.current.muted = false;
} else {
this.setState({ mute: true });
this.myAudioRef.current.muted = true;
}
}}
>
{this.state.mute ? (
<VolumeOff />
) : this.state.volume >= 0.7 ? (
<VolumeUp />
) : this.state.volume <= 0.3 ? (
<VolumeMute />
) : (
<VolumeDown />
)}
</IconButton>
<Popover
id="volume-controller"
open={Boolean(this.state.openPropEl)}
anchorEl={this.state.openPropEl}
onClose={() => { this.setState({ openPropEl: null }) }}
anchorOrigin={{ vertical: "top", horizontal: "center" }}
transformOrigin={{ vertical: "bottom", horizontal: "center" }}
>
<Card className={classes.setvol}>
<Grid container spacing={2}>
<Grid item><VolumeDown /></Grid>
<Grid item xs><Slider
aria-labelledby="continuous-slider"
value={this.state.volume}
min={0} max={1} step={0.01} defaultValue={this.state.volume}
onChange={(e, vol) => {
this.setState({ volume: vol });
this.myAudioRef.current.volume = vol;
}}
style={{ padding: "13px 0" }}
/></Grid>
<Grid item><VolumeUp /></Grid>
</Grid>
</Card>
</Popover>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClose}>
{t("fileManager.closeAndStop")}
</Button>
<Button onClick={this.backgroundPlay}>
{t("fileManager.playInBackground")}
</Button>
</DialogActions>
</Dialog>
);
}
}
MusicPlayerComponent.propTypes = {
classes: PropTypes.object.isRequired,
};
const MusicPlayer = connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(withRouter(withTranslation()(MusicPlayerComponent))));
export default MusicPlayer;

View File

@@ -0,0 +1,51 @@
import React from "react";
import DropDownItem from "./DropDownItem";
export default function DropDown(props) {
let timer;
let first = props.folders.length;
const status = [];
for (let index = 0; index < props.folders.length; index++) {
status[index] = false;
}
const setActiveStatus = (id, value) => {
status[id] = value;
if (value) {
clearTimeout(timer);
} else {
let shouldClose = true;
status.forEach((element) => {
if (element) {
shouldClose = false;
}
});
if (shouldClose) {
if (first <= 0) {
timer = setTimeout(() => {
props.onClose();
}, 100);
} else {
first--;
}
}
}
console.log(status);
};
return (
<>
{props.folders.map((folder, id) => (
// eslint-disable-next-line react/jsx-key
<DropDownItem
key={id}
path={"/" + props.folders.slice(0, id).join("/")}
navigateTo={props.navigateTo}
id={id}
setActiveStatus={setActiveStatus}
folder={folder}
/>
))}
</>
);
}

View File

@@ -0,0 +1,54 @@
import React, { useEffect } from "react";
import { makeStyles } from "@material-ui/core";
import FolderIcon from "@material-ui/icons/Folder";
import { MenuItem, ListItemIcon, ListItemText } from "@material-ui/core";
import { useDrop } from "react-dnd";
import classNames from "classnames";
const useStyles = makeStyles((theme) => ({
active: {
border: "2px solid " + theme.palette.primary.light,
},
}));
export default function DropDownItem(props) {
const [{ canDrop, isOver }, drop] = useDrop({
accept: "object",
drop: () => {
console.log({
folder: {
id: -1,
path: props.path,
name: props.folder === "/" ? "" : props.folder,
},
});
},
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
const isActive = canDrop && isOver;
useEffect(() => {
props.setActiveStatus(props.id, isActive);
// eslint-disable-next-line
}, [isActive]);
const classes = useStyles();
return (
<MenuItem
ref={drop}
className={classNames({
[classes.active]: isActive,
})}
onClick={(e) => props.navigateTo(e, props.id)}
>
<ListItemIcon>
<FolderIcon />
</ListItemIcon>
<ListItemText primary={props.folder} />
</MenuItem>
);
}

View File

@@ -0,0 +1,500 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import RightIcon from "@material-ui/icons/KeyboardArrowRight";
import ShareIcon from "@material-ui/icons/Share";
import NewFolderIcon from "@material-ui/icons/CreateNewFolder";
import RefreshIcon from "@material-ui/icons/Refresh";
import explorer, {
drawerToggleAction,
navigateTo,
navigateUp,
openCompressDialog,
openCreateFileDialog,
openCreateFolderDialog,
openShareDialog,
refreshFileList,
setNavigatorError,
setNavigatorLoadingStatus,
setSelectedTarget,
} from "../../../redux/explorer";
import { fixUrlHash, setGetParameter } from "../../../utils/index";
import {
Divider,
ListItemIcon,
Menu,
MenuItem,
withStyles,
} from "@material-ui/core";
import PathButton from "./PathButton";
import DropDown from "./DropDown";
import pathHelper from "../../../utils/page";
import classNames from "classnames";
import Auth from "../../../middleware/Auth";
import { Archive } from "@material-ui/icons";
import { FilePlus } from "mdi-material-ui";
import SubActions from "./SubActions";
import { setCurrentPolicy } from "../../../redux/explorer/action";
import { list } from "../../../services/navigate";
import { withTranslation } from "react-i18next";
const mapStateToProps = (state) => {
return {
path: state.navigator.path,
refresh: state.navigator.refresh,
drawerDesktopOpen: state.viewUpdate.open,
viewMethod: state.viewUpdate.explorerViewMethod,
search: state.explorer.search,
sortMethod: state.viewUpdate.sortMethod,
};
};
const mapDispatchToProps = (dispatch) => {
return {
navigateToPath: (path) => {
dispatch(navigateTo(path));
},
navigateUp: () => {
dispatch(navigateUp());
},
setNavigatorError: (status, msg) => {
dispatch(setNavigatorError(status, msg));
},
updateFileList: (list) => {
dispatch(explorer.actions.updateFileList(list));
},
setNavigatorLoadingStatus: (status) => {
dispatch(setNavigatorLoadingStatus(status));
},
refreshFileList: () => {
dispatch(refreshFileList());
},
setSelectedTarget: (target) => {
dispatch(setSelectedTarget(target));
},
openCreateFolderDialog: () => {
dispatch(openCreateFolderDialog());
},
openCreateFileDialog: () => {
dispatch(openCreateFileDialog());
},
openShareDialog: () => {
dispatch(openShareDialog());
},
handleDesktopToggle: (open) => {
dispatch(drawerToggleAction(open));
},
openCompressDialog: () => {
dispatch(openCompressDialog());
},
setCurrentPolicy: (policy) => {
dispatch(setCurrentPolicy(policy));
},
};
};
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const styles = (theme) => ({
container: {
[theme.breakpoints.down("xs")]: {
display: "none",
},
backgroundColor: theme.palette.background.paper,
},
navigatorContainer: {
display: "flex",
justifyContent: "space-between",
},
nav: {
height: "48px",
padding: "5px 15px",
display: "flex",
},
optionContainer: {
paddingTop: "6px",
marginRight: "10px",
},
rightIcon: {
marginTop: "6px",
verticalAlign: "top",
color: "#868686",
},
expandMore: {
color: "#8d8d8d",
},
roundBorder: {
borderRadius: "4px 4px 0 0",
},
});
class NavigatorComponent extends Component {
search = undefined;
currentID = 0;
state = {
hidden: false,
hiddenFolders: [],
folders: [],
anchorEl: null,
hiddenMode: false,
anchorHidden: null,
};
constructor(props) {
super(props);
this.element = React.createRef();
}
componentDidMount = () => {
const url = new URL(fixUrlHash(window.location.href));
const c = url.searchParams.get("path");
this.renderPath(c === null ? "/" : c);
if (!this.props.isShare) {
// 如果是在个人文件管理页,首次加载时打开侧边栏
this.props.handleDesktopToggle(true);
}
// 后退操作时重新导航
window.onpopstate = () => {
const url = new URL(fixUrlHash(window.location.href));
const c = url.searchParams.get("path");
if (c !== null) {
this.props.navigateToPath(c);
}
};
};
renderPath = (path = null) => {
this.props.setNavigatorError(false, null);
this.setState({
folders:
path !== null
? path.substr(1).split("/")
: this.props.path.substr(1).split("/"),
});
const newPath = path !== null ? path : this.props.path;
list(
newPath,
this.props.share,
this.search ? this.search.keywords : "",
this.search ? this.search.searchPath : ""
)
.then((response) => {
this.currentID = response.data.parent;
this.props.updateFileList(response.data.objects);
this.props.setNavigatorLoadingStatus(false);
if (!this.search) {
setGetParameter("path", encodeURIComponent(newPath));
}
if (response.data.policy) {
this.props.setCurrentPolicy({
id: response.data.policy.id,
name: response.data.policy.name,
type: response.data.policy.type,
maxSize: response.data.policy.max_size,
allowedSuffix: response.data.policy.file_type,
});
}
})
.catch((error) => {
this.props.setNavigatorError(true, error);
});
this.checkOverFlow(true);
};
redresh = (path) => {
this.props.setNavigatorLoadingStatus(true);
this.props.setNavigatorError(false, "error");
this.renderPath(path);
};
UNSAFE_componentWillReceiveProps = (nextProps) => {
if (this.props.search !== nextProps.search) {
this.search = nextProps.search;
}
if (this.props.path !== nextProps.path) {
this.renderPath(nextProps.path);
}
if (this.props.refresh !== nextProps.refresh) {
this.redresh(nextProps.path);
}
};
componentWillUnmount() {
this.props.updateFileList([]);
}
componentDidUpdate = (prevProps, prevStates) => {
if (this.state.folders !== prevStates.folders) {
this.checkOverFlow(true);
}
if (this.props.drawerDesktopOpen !== prevProps.drawerDesktopOpen) {
delay(500).then(() => this.checkOverFlow());
}
};
checkOverFlow = (force) => {
if (this.overflowInitLock && !force) {
return;
}
if (this.element.current !== null) {
const hasOverflowingChildren =
this.element.current.offsetHeight <
this.element.current.scrollHeight ||
this.element.current.offsetWidth <
this.element.current.scrollWidth;
if (hasOverflowingChildren) {
this.overflowInitLock = true;
this.setState({ hiddenMode: true });
}
if (!hasOverflowingChildren && this.state.hiddenMode) {
this.setState({ hiddenMode: false });
}
}
};
navigateTo = (event, id) => {
if (id === this.state.folders.length - 1) {
//最后一个路径
this.setState({ anchorEl: event.currentTarget });
} else if (
id === -1 &&
this.state.folders.length === 1 &&
this.state.folders[0] === ""
) {
this.props.refreshFileList();
this.handleClose();
} else if (id === -1) {
this.props.navigateToPath("/");
this.handleClose();
} else {
this.props.navigateToPath(
"/" + this.state.folders.slice(0, id + 1).join("/")
);
this.handleClose();
}
};
handleClose = () => {
this.setState({ anchorEl: null, anchorHidden: null, anchorSort: null });
};
showHiddenPath = (e) => {
this.setState({ anchorHidden: e.currentTarget });
};
performAction = (e) => {
this.handleClose();
if (e === "refresh") {
this.redresh();
return;
}
const presentPath = this.props.path.split("/");
const newTarget = [
{
id: this.currentID,
type: "dir",
name: presentPath.pop(),
path: presentPath.length === 1 ? "/" : presentPath.join("/"),
},
];
//this.props.navitateUp();
switch (e) {
case "share":
this.props.setSelectedTarget(newTarget);
this.props.openShareDialog();
break;
case "newfolder":
this.props.openCreateFolderDialog();
break;
case "compress":
this.props.setSelectedTarget(newTarget);
this.props.openCompressDialog();
break;
case "newFile":
this.props.openCreateFileDialog();
break;
default:
break;
}
};
render() {
const { classes, t } = this.props;
const isHomePage = pathHelper.isHomePage(this.props.location.pathname);
const user = Auth.GetUser();
const presentFolderMenu = (
<Menu
id="presentFolderMenu"
anchorEl={this.state.anchorEl}
open={Boolean(this.state.anchorEl)}
onClose={this.handleClose}
disableAutoFocusItem={true}
>
<MenuItem onClick={() => this.performAction("refresh")}>
<ListItemIcon>
<RefreshIcon />
</ListItemIcon>
{t("fileManager.refresh")}
</MenuItem>
{!this.props.search && isHomePage && (
<div>
<Divider />
<MenuItem onClick={() => this.performAction("share")}>
<ListItemIcon>
<ShareIcon />
</ListItemIcon>
{t("fileManager.share")}
</MenuItem>
{user.group.compress && (
<MenuItem
onClick={() => this.performAction("compress")}
>
<ListItemIcon>
<Archive />
</ListItemIcon>
{t("fileManager.compress")}
</MenuItem>
)}
<Divider />
<MenuItem
onClick={() => this.performAction("newfolder")}
>
<ListItemIcon>
<NewFolderIcon />
</ListItemIcon>
{t("fileManager.newFolder")}
</MenuItem>
<MenuItem onClick={() => this.performAction("newFile")}>
<ListItemIcon>
<FilePlus />
</ListItemIcon>
{t("fileManager.newFile")}
</MenuItem>
</div>
)}
</Menu>
);
return (
<div
className={classNames(
{
[classes.roundBorder]: this.props.isShare,
},
classes.container
)}
id={"drag-layer-inherit"}
>
<div className={classes.navigatorContainer}>
<div className={classes.nav} ref={this.element}>
<span>
<PathButton
folder="/"
path="/"
onClick={(e) => this.navigateTo(e, -1)}
/>
<RightIcon className={classes.rightIcon} />
</span>
{this.state.hiddenMode && (
<span>
<PathButton
more
title={t("fileManager.showFullPath")}
onClick={this.showHiddenPath}
/>
<Menu
id="hiddenPathMenu"
anchorEl={this.state.anchorHidden}
open={Boolean(this.state.anchorHidden)}
onClose={this.handleClose}
disableAutoFocusItem={true}
>
<DropDown
onClose={this.handleClose}
folders={this.state.folders.slice(
0,
-1
)}
navigateTo={this.navigateTo}
/>
</Menu>
<RightIcon className={classes.rightIcon} />
<PathButton
folder={this.state.folders.slice(-1)}
path={
"/" +
this.state.folders
.slice(0, -1)
.join("/")
}
last={true}
onClick={(e) =>
this.navigateTo(
e,
this.state.folders.length - 1
)
}
/>
{presentFolderMenu}
</span>
)}
{!this.state.hiddenMode &&
this.state.folders.map((folder, id, folders) => (
<span key={id}>
{folder !== "" && (
<span>
<PathButton
folder={folder}
path={
"/" +
folders
.slice(0, id)
.join("/")
}
last={id === folders.length - 1}
onClick={(e) =>
this.navigateTo(e, id)
}
/>
{id === folders.length - 1 &&
presentFolderMenu}
{id !== folders.length - 1 && (
<RightIcon
className={
classes.rightIcon
}
/>
)}
</span>
)}
</span>
))}
</div>
<div className={classes.optionContainer}>
<SubActions isSmall />
</div>
</div>
<Divider />
</div>
);
}
}
NavigatorComponent.propTypes = {
classes: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
};
const Navigator = connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(withRouter(withTranslation()(NavigatorComponent))));
export default Navigator;

View File

@@ -0,0 +1,80 @@
import React, { useEffect } from "react";
import ExpandMore from "@material-ui/icons/ExpandMore";
import { Button } from "@material-ui/core";
import { makeStyles } from "@material-ui/core";
import { useDrop } from "react-dnd";
import classNames from "classnames";
import MoreIcon from "@material-ui/icons/MoreHoriz";
const useStyles = makeStyles((theme) => ({
expandMore: {
color: "#8d8d8d",
},
active: {
boxShadow: "0 0 0 2px " + theme.palette.primary.light,
},
button: {
textTransform: "none",
},
}));
export default function PathButton(props) {
const inputRef = React.useRef(null);
const [{ canDrop, isOver }, drop] = useDrop({
accept: "object",
drop: () => {
if (props.more) {
inputRef.current.click();
} else {
return {
folder: {
id: -1,
path: props.path,
name: props.folder === "/" ? "" : props.folder,
},
};
}
},
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
const isActive = canDrop && isOver;
useEffect(() => {
if (props.more && isActive) {
inputRef.current.click();
}
// eslint-disable-next-line
}, [isActive]);
const classes = useStyles();
return (
<span onClick={props.onClick} ref={inputRef}>
<Button
ref={drop}
className={classNames(
{
[classes.active]: isActive,
},
classes.button
)}
component="span"
title={props.title}
>
{props.more && <MoreIcon />}
{!props.more && (
<>
{props.folder}
{props.last && (
<ExpandMore className={classes.expandMore} />
)}
</>
)}
</Button>
</span>
);
}

View File

@@ -0,0 +1,238 @@
import React, { useCallback, useState } from "react";
import { IconButton, makeStyles, Menu, MenuItem } from "@material-ui/core";
import ViewListIcon from "@material-ui/icons/ViewList";
import ViewSmallIcon from "@material-ui/icons/ViewComfy";
import ViewModuleIcon from "@material-ui/icons/ViewModule";
import DownloadIcon from "@material-ui/icons/CloudDownload";
import SaveIcon from "@material-ui/icons/Save";
import ReportIcon from "@material-ui/icons/Report";
import Avatar from "@material-ui/core/Avatar";
import { useDispatch, useSelector } from "react-redux";
import Auth from "../../../middleware/Auth";
import { changeSortMethod, startBatchDownload } from "../../../redux/explorer/action";
import {
changeViewMethod,
openResaveDialog,
setShareUserPopover,
} from "../../../redux/explorer";
import { FormatPageBreak } from "mdi-material-ui";
import pathHelper from "../../../utils/page";
import { changePageSize } from "../../../redux/viewUpdate/action";
import Report from "../../Modals/Report";
import { useTranslation } from "react-i18next";
import Sort from "../Sort";
const useStyles = makeStyles((theme) => ({
sideButton: {
padding: "8px",
marginRight: "5px",
},
}));
// const sortOptions = [
// "A-Z",
// "Z-A",
// "oldestUploaded",
// "newestUploaded",
// "oldestModified",
// "newestModified",
// "smallest",
// "largest",
// ];
const paginationOption = ["50", "100", "200", "500", "1000"];
export default function SubActions({ isSmall, inherit }) {
const { t } = useTranslation("application", { keyPrefix: "fileManager" });
const { t: vasT } = useTranslation("application", { keyPrefix: "vas" });
const dispatch = useDispatch();
const viewMethod = useSelector(
(state) => state.viewUpdate.explorerViewMethod
);
const share = useSelector((state) => state.viewUpdate.shareInfo);
const pageSize = useSelector((state) => state.viewUpdate.pagination.size);
const OpenLoadingDialog = useCallback(
(method) => dispatch(changeViewMethod(method)),
[dispatch]
);
const ChangeSortMethod = useCallback(
(method) => dispatch(changeSortMethod(method)),
[dispatch]
);
const OpenResaveDialog = useCallback(
(key) => dispatch(openResaveDialog(key)),
[dispatch]
);
const SetShareUserPopover = useCallback(
(e) => dispatch(setShareUserPopover(e)),
[dispatch]
);
const StartBatchDownloadAll = useCallback(
() => dispatch(startBatchDownload(share)),
[dispatch, share]
);
const ChangePageSize = useCallback((e) => dispatch(changePageSize(e)), [
dispatch,
]);
// const [anchorSort, setAnchorSort] = useState(null);
const [anchorPagination, setAnchorPagination] = useState(null);
// const [selectedIndex, setSelectedIndex] = useState(0);
const [openReport, setOpenReport] = useState(false);
// const showSortOptions = (e) => {
// setAnchorSort(e.currentTarget);
// };
const showPaginationOptions = (e) => {
setAnchorPagination(e.currentTarget);
};
/** change sort */
const onChangeSort = (value) => {
ChangeSortMethod(value);
};
const handlePaginationChange = (s) => {
ChangePageSize(s);
setAnchorPagination(null);
};
const toggleViewMethod = () => {
const newMethod =
viewMethod === "icon"
? "list"
: viewMethod === "list"
? "smallIcon"
: "icon";
Auth.SetPreference("view_method", newMethod);
OpenLoadingDialog(newMethod);
};
const isMobile = pathHelper.isMobile();
const classes = useStyles();
return (
<>
<IconButton
title={t("batchDownload")}
className={classes.sideButton}
onClick={StartBatchDownloadAll}
color={inherit ? "inherit" : "default"}
>
<DownloadIcon fontSize={isSmall ? "small" : "default"} />
</IconButton>
{viewMethod === "icon" && (
<IconButton
title={t("listView")}
className={classes.sideButton}
onClick={toggleViewMethod}
color={inherit ? "inherit" : "default"}
>
<ViewListIcon fontSize={isSmall ? "small" : "default"} />
</IconButton>
)}
{viewMethod === "list" && (
<IconButton
title={t("gridViewSmall")}
className={classes.sideButton}
onClick={toggleViewMethod}
color={inherit ? "inherit" : "default"}
>
<ViewSmallIcon fontSize={isSmall ? "small" : "default"} />
</IconButton>
)}
{viewMethod === "smallIcon" && (
<IconButton
title={t("gridViewLarge")}
className={classes.sideButton}
onClick={toggleViewMethod}
color={inherit ? "inherit" : "default"}
>
<ViewModuleIcon fontSize={isSmall ? "small" : "default"} />
</IconButton>
)}
{!isMobile && (
<IconButton
title={t("paginationSize")}
className={classes.sideButton}
onClick={showPaginationOptions}
color={inherit ? "inherit" : "default"}
>
<FormatPageBreak fontSize={isSmall ? "small" : "default"} />
</IconButton>
)}
<Menu
id="sort-menu"
anchorEl={anchorPagination}
open={Boolean(anchorPagination)}
onClose={() => setAnchorPagination(null)}
>
{paginationOption.map((option, index) => (
<MenuItem
key={option}
selected={option === pageSize.toString()}
onClick={() => handlePaginationChange(parseInt(option))}
>
{t("paginationOption", { option })}
</MenuItem>
))}
<MenuItem
selected={pageSize === -1}
onClick={() => handlePaginationChange(-1)}
>
{t("noPagination")}
</MenuItem>
</Menu>
<Sort
isSmall={isSmall}
inherit={inherit}
className={classes.sideButton}
onChange={onChangeSort}
/>
{share && (
<>
<IconButton
title={vasT("saveToMyFiles")}
className={classes.sideButton}
onClick={() => OpenResaveDialog(share.key)}
color={inherit ? "inherit" : "default"}
>
<SaveIcon fontSize={isSmall ? "small" : "default"} />
</IconButton>
{!inherit && (
<>
<IconButton
title={vasT("report")}
className={classes.sideButton}
onClick={() => setOpenReport(true)}
>
<ReportIcon
fontSize={isSmall ? "small" : "default"}
/>
</IconButton>
<Report
open={openReport}
share={share}
onClose={() => setOpenReport(false)}
/>
</>
)}
</>
)}
{share && (
<IconButton
title={t("shareCreateBy", { nick: share.creator.nick })}
className={classes.sideButton}
onClick={(e) => SetShareUserPopover(e.currentTarget)}
style={{ padding: 5 }}
>
<Avatar
style={{ height: 23, width: 23 }}
src={"/api/v3/user/avatar/" + share.creator.key + "/s"}
/>
</IconButton>
)}
</>
);
}

View File

@@ -0,0 +1,261 @@
import React, { useCallback, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import statusHelper from "../../utils/page";
import FileIcon from "./FileIcon";
import SmallIcon from "./SmallIcon";
import TableItem from "./TableRow";
import classNames from "classnames";
import { makeStyles } from "@material-ui/core";
import { useDrag } from "react-dnd";
import { getEmptyImage } from "react-dnd-html5-backend";
import DropWarpper from "./DnD/DropWarpper";
import { useLocation } from "react-router-dom";
import { pathBack } from "../../utils";
import {
changeContextMenu,
dragAndDrop,
navigateTo,
openLoadingDialog,
openPreview,
selectFile,
setSelectedTarget,
toggleSnackbar,
} from "../../redux/explorer";
import useDragScrolling from "./DnD/Scrolling";
const useStyles = makeStyles(() => ({
container: {
padding: "7px",
},
fixFlex: {
minWidth: 0,
},
dragging: {
opacity: 0.4,
},
}));
export default function ObjectIcon(props) {
const path = useSelector((state) => state.navigator.path);
const shareInfo = useSelector((state) => state.viewUpdate.shareInfo);
const selected = useSelector((state) => state.explorer.selected);
const viewMethod = useSelector(
(state) => state.viewUpdate.explorerViewMethod
);
const navigatorPath = useSelector((state) => state.navigator.path);
const location = useLocation();
const dispatch = useDispatch();
const ContextMenu = useCallback(
(type, open) => dispatch(changeContextMenu(type, open)),
[dispatch]
);
const SetSelectedTarget = useCallback(
(targets) => dispatch(setSelectedTarget(targets)),
[dispatch]
);
const NavitateTo = useCallback((targets) => dispatch(navigateTo(targets)), [
dispatch,
]);
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const DragAndDrop = useCallback(
(source, target) => dispatch(dragAndDrop(source, target)),
[dispatch]
);
const OpenLoadingDialog = useCallback(
(text) => dispatch(openLoadingDialog(text)),
[dispatch]
);
const OpenPreview = useCallback((share) => dispatch(openPreview(share)), [
dispatch,
]);
const StartDownload = useCallback(
(share, file) => dispatch(StartDownload(share, file)),
[dispatch]
);
const classes = useStyles();
const contextMenu = (e) => {
if (props.file.type === "up") {
return;
}
e.preventDefault();
if (
selected.findIndex((value) => {
return value === props.file;
}) === -1
) {
SetSelectedTarget([props.file]);
}
ContextMenu("file", true);
};
const SelectFile = (e) => {
dispatch(selectFile(props.file, e, props.index));
};
const enterFolder = () => {
NavitateTo(
path === "/" ? path + props.file.name : path + "/" + props.file.name
);
};
const handleClick = (e) => {
if (props.file.type === "up") {
NavitateTo(pathBack(navigatorPath));
return;
}
SelectFile(e);
if (
props.file.type === "dir" &&
!e.ctrlKey &&
!e.metaKey &&
!e.shiftKey
) {
enterFolder();
}
};
const handleDoubleClick = () => {
if (props.file.type === "up") {
return;
}
if (props.file.type === "dir") {
enterFolder();
return;
}
OpenPreview(shareInfo);
};
const handleIconClick = (e) => {
e.stopPropagation();
if (!e.shiftKey) {
e.ctrlKey = true;
}
SelectFile(e);
return false;
};
const {
addEventListenerForWindow,
removeEventListenerForWindow,
} = useDragScrolling();
const [{ isDragging }, drag, preview] = useDrag({
item: {
object: props.file,
type: "object",
selected: [...selected],
viewMethod: viewMethod,
},
begin: () => {
addEventListenerForWindow();
},
end: (item, monitor) => {
removeEventListenerForWindow();
const dropResult = monitor.getDropResult();
if (item && dropResult) {
if (dropResult.folder) {
if (
item.object.id !== dropResult.folder.id ||
item.object.type !== dropResult.folder.type
) {
DragAndDrop(item.object, dropResult.folder);
}
}
}
},
canDrag: () => {
return (
!statusHelper.isMobile() &&
statusHelper.isHomePage(location.pathname)
);
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
useEffect(() => {
preview(getEmptyImage(), { captureDraggingState: true });
// eslint-disable-next-line
}, []);
if (viewMethod === "list") {
return (
<>
{props.file.type === "dir" && (
<DropWarpper
isListView={true}
pref={drag}
className={classNames(classes.container, {
[classes.dragging]: isDragging,
})}
onIconClick={handleIconClick}
contextMenu={contextMenu}
handleClick={handleClick}
handleDoubleClick={handleDoubleClick.bind(this)}
folder={props.file}
/>
)}
{props.file.type !== "dir" && (
<TableItem
pref={drag}
className={classNames(classes.container, {
[classes.dragging]: isDragging,
})}
onIconClick={handleIconClick}
contextMenu={contextMenu}
handleClick={handleClick}
handleDoubleClick={handleDoubleClick.bind(this)}
file={props.file}
/>
)}
</>
);
}
return (
<div
ref={drag}
className={classNames({
[classes.container]: viewMethod !== "list",
[classes.dragging]: isDragging,
})}
>
<div
className={classes.fixFlex}
onContextMenu={contextMenu}
onClick={handleClick}
onDoubleClick={handleDoubleClick.bind(this)}
>
{props.file.type === "dir" && viewMethod !== "list" && (
<DropWarpper
isListView={false}
onIconClick={handleIconClick}
folder={props.file}
/>
)}
{props.file.type === "file" && viewMethod === "icon" && (
<FileIcon
onIconClick={handleIconClick}
ref={drag}
file={props.file}
/>
)}
{props.file.type === "file" && viewMethod === "smallIcon" && (
<SmallIcon
onIconClick={handleIconClick}
file={props.file}
/>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,87 @@
import React, { useCallback, useMemo } from "react";
import { makeStyles } from "@material-ui/core/styles";
import { useDispatch, useSelector } from "react-redux";
import { Pagination } from "@material-ui/lab";
import CustomPaginationItem from "./PaginationItem";
import { setPagination } from "../../redux/viewUpdate/action";
import AutoHidden from "../Dial/AutoHidden";
import statusHelper from "../../utils/page";
import { useLocation } from "react-router-dom";
const useStyles = makeStyles((theme) => ({
root: {
position: "fixed",
bottom: 23,
/* left: 8px; */
background: theme.palette.background.paper,
borderRadius: 24,
boxShadow:
" 0px 3px 5px -1px rgb(0 0 0 / 20%), 0px 6px 10px 0px rgb(0 0 0 / 14%), 0px 1px 18px 0px rgb(0 0 0 / 12%)",
padding: "8px 4px 8px 4px",
marginLeft: 20,
},
placeholder: {
marginTop: 80,
},
}));
export default function PaginationFooter() {
const classes = useStyles();
const dispatch = useDispatch();
const files = useSelector((state) => state.explorer.fileList);
const folders = useSelector((state) => state.explorer.dirList);
const pagination = useSelector((state) => state.viewUpdate.pagination);
const loading = useSelector((state) => state.viewUpdate.navigatorLoading);
const location = useLocation();
const SetPagination = useCallback((p) => dispatch(setPagination(p)), [
dispatch,
]);
const handleChange = (event, value) => {
SetPagination({ ...pagination, page: value });
};
const count = useMemo(
() => Math.ceil((files.length + folders.length) / pagination.size),
[files, folders, pagination.size]
);
const isMobile = statusHelper.isMobile();
const isSharePage = statusHelper.isSharePage(location.pathname);
if (count > 1 && !loading) {
return (
<>
{!isMobile && !isSharePage && (
<div className={classes.placeholder} />
)}
<AutoHidden
enable
element={
isMobile || isSharePage
? null
: document.querySelector("#explorer-container")
}
>
<div className={classes.root}>
<Pagination
renderItem={(item) => (
<CustomPaginationItem
count={count}
isMobile={isMobile}
{...item}
/>
)}
color="secondary"
count={count}
page={pagination.page}
onChange={handleChange}
/>
</div>
</AutoHidden>
</>
);
}
return <div></div>;
}

View File

@@ -0,0 +1,50 @@
import React, { useEffect, useRef } from "react";
import { useDrop } from "react-dnd";
import { PaginationItem } from "@material-ui/lab";
export default function CustomPaginationItem(props) {
const inputRef = useRef(null);
const [{ canDrop, isOver }, drop] = useDrop({
accept: "object",
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
const isActive = canDrop && isOver;
useEffect(() => {
if (
isActive &&
props.onClick &&
props.type !== "start-ellipsis" &&
props.type !== "end-ellipsis"
) {
console.log("ss");
props.onClick();
}
}, [isActive, inputRef]);
if (
props.isMobile &&
(props.type === "start-ellipsis" ||
props.type === "end-ellipsis" ||
props.type === "page")
) {
if (props.selected) {
return (
<div>
{props.page} / {props.count}
</div>
);
}
return <></>;
}
return (
<div ref={inputRef}>
<PaginationItem ref={drop} {...props} />
</div>
);
}

View File

@@ -0,0 +1,268 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import FolderIcon from "@material-ui/icons/Folder";
import RightIcon from "@material-ui/icons/KeyboardArrowRight";
import UpIcon from "@material-ui/icons/ArrowUpward";
import { connect } from "react-redux";
import classNames from "classnames";
import {
IconButton,
ListItemIcon,
ListItemSecondaryAction,
ListItemText,
MenuItem,
MenuList,
withStyles,
} from "@material-ui/core";
import Sort, { sortMethodFuncs } from './Sort';
import API from "../../middleware/Api";
import { toggleSnackbar } from "../../redux/explorer";
import { withTranslation } from "react-i18next";
const mapStateToProps = (state) => {
return {
search: state.explorer.search,
};
};
const mapDispatchToProps = (dispatch) => {
return {
toggleSnackbar: (vertical, horizontal, msg, color) => {
dispatch(toggleSnackbar(vertical, horizontal, msg, color));
},
};
};
const styles = (theme) => ({
iconWhite: {
color: theme.palette.common.white,
},
selected: {
backgroundColor: theme.palette.primary.main + "!important",
"& $primary, & $icon": {
color: theme.palette.common.white,
},
},
primary: {},
icon: {},
buttonIcon: {},
selector: {
minWidth: "300px",
},
container: {
maxHeight: "330px",
overflowY: " auto",
},
sortWrapper: {
textAlign: "right",
paddingRight: "30px",
},
sortButton: {
padding: "0",
},
});
class PathSelectorCompoment extends Component {
state = {
presentPath: "/",
sortBy: '',
dirList: [],
selectedTarget: null,
};
/**
* the source dir list from api `/directory`
*
* `state.dirList` is a sorted copy of it
*/
sourceDirList = []
componentDidMount = () => {
const toBeLoad = this.props.presentPath;
this.enterFolder(!this.props.search ? toBeLoad : "/");
};
back = () => {
const paths = this.state.presentPath.split("/");
paths.pop();
const toBeLoad = paths.join("/");
this.enterFolder(toBeLoad === "" ? "/" : toBeLoad);
};
enterFolder = (toBeLoad) => {
API.get(
(this.props.api ? this.props.api : "/directory") +
encodeURIComponent(toBeLoad)
)
.then((response) => {
const dirList = response.data.objects.filter((x) => {
return (
x.type === "dir" &&
this.props.selected.findIndex((value) => {
return (
value.name === x.name && value.path === x.path
);
}) === -1
);
});
dirList.forEach((value) => {
value.displayName = value.name;
});
this.sourceDirList = dirList
this.setState({
presentPath: toBeLoad,
dirList: dirList,
selectedTarget: null,
}, this.updateDirList);
})
.catch((error) => {
this.props.toggleSnackbar(
"top",
"right",
error.message,
"warning"
);
});
};
handleSelect = (index) => {
this.setState({ selectedTarget: index });
this.props.onSelect(this.state.dirList[index]);
};
/**
* change sort type
* @param {Event} event
*/
onChangeSort = (sortBy) => {
this.setState({ sortBy }, this.updateDirList)
};
/**
* sort dir list, and handle parent dirs
*/
updateDirList = () => {
const { state, sourceDirList } = this
const { sortBy, presentPath } = state
// copy
const dirList = [...sourceDirList]
// sort
const sortMethod = sortMethodFuncs[sortBy]
if (sortMethod) dirList.sort(sortMethod)
// add root/parent dirs to top
if (presentPath === "/") {
dirList.unshift({ name: "/", path: "", displayName: "/" });
} else {
let path = presentPath;
let name = presentPath;
const displayNames = ["fileManager.currentFolder", "fileManager.backToParentFolder"];
for (let i = 0; i < 2; i++) {
const paths = path.split("/");
name = paths.pop();
name = name === "" ? "/" : name;
path = paths.join("/");
dirList.unshift({
name: name,
path: path,
displayName: this.props.t(
displayNames[i]
),
});
}
}
this.setState({ dirList })
}
render() {
const { classes, t } = this.props;
const showActionIcon = (index) => {
if (this.state.presentPath === "/") {
return index !== 0;
}
return index !== 1;
};
const actionIcon = (index) => {
if (this.state.presentPath === "/") {
return <RightIcon />;
}
if (index === 0) {
return <UpIcon />;
}
return <RightIcon />;
};
return (
<div className={classes.container}>
<div className={classes.sortWrapper}>
<Sort value={this.state.sortBy} isSmall className={classes.sortButton} onChange={this.onChangeSort} />
</div>
<MenuList className={classes.selector}>
{this.state.dirList.map((value, index) => (
<MenuItem
classes={{
selected: classes.selected,
}}
key={index}
selected={this.state.selectedTarget === index}
onClick={() => this.handleSelect(index)}
>
<ListItemIcon className={classes.icon}>
<FolderIcon />
</ListItemIcon>
<ListItemText
classes={{ primary: classes.primary }}
primary={value.displayName}
primaryTypographyProps={{
style: { whiteSpace: "normal" },
}}
/>
{showActionIcon(index) && (
<ListItemSecondaryAction
className={classes.buttonIcon}
>
<IconButton
className={classNames({
[classes.iconWhite]:
this.state.selectedTarget ===
index,
})}
onClick={() =>
index === 0
? this.back()
: this.enterFolder(
value.path === "/"
? value.path +
value.name
: value.path +
"/" +
value.name
)
}
>
{actionIcon(index)}
</IconButton>
</ListItemSecondaryAction>
)}
</MenuItem>
))}
</MenuList>
</div>
);
}
}
PathSelectorCompoment.propTypes = {
classes: PropTypes.object.isRequired,
presentPath: PropTypes.string.isRequired,
selected: PropTypes.array.isRequired,
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(withTranslation()(PathSelectorCompoment)));

View File

@@ -0,0 +1,339 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core";
import { useDispatch, useSelector } from "react-redux";
import Drawer from "@material-ui/core/Drawer";
import Toolbar from "@material-ui/core/Toolbar";
import { Clear, Folder } from "@material-ui/icons";
import Divider from "@material-ui/core/Divider";
import { setSideBar } from "../../../redux/explorer/action";
import TypeIcon from "../TypeIcon";
import Typography from "@material-ui/core/Typography";
import IconButton from "@material-ui/core/IconButton";
import Grid from "@material-ui/core/Grid";
import API from "../../../middleware/Api";
import { filename, sizeToString } from "../../../utils";
import Link from "@material-ui/core/Link";
import Tooltip from "@material-ui/core/Tooltip";
import TimeAgo from "timeago-react";
import ListLoading from "../../Placeholder/ListLoading";
import Hidden from "@material-ui/core/Hidden";
import Dialog from "@material-ui/core/Dialog";
import Slide from "@material-ui/core/Slide";
import AppBar from "@material-ui/core/AppBar";
import { formatLocalTime } from "../../../utils/datetime";
import { navigateTo, toggleSnackbar } from "../../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
const drawerWidth = 350;
const useStyles = makeStyles((theme) => ({
drawer: {
width: drawerWidth,
flexShrink: 0,
},
drawerPaper: {
width: drawerWidth,
boxShadow:
"0px 8px 10px -5px rgb(0 0 0 / 20%), 0px 16px 24px 2px rgb(0 0 0 / 14%), 0px 6px 30px 5px rgb(0 0 0 / 12%)",
},
drawerContainer: {
overflow: "auto",
},
header: {
display: "flex",
padding: theme.spacing(3),
placeContent: "space-between",
},
fileIcon: { width: 33, height: 33 },
fileIconSVG: { fontSize: 20 },
folderIcon: {
color: theme.palette.text.secondary,
width: 33,
height: 33,
},
fileName: {
marginLeft: theme.spacing(2),
marginRight: theme.spacing(2),
wordBreak: "break-all",
flexGrow: 2,
},
closeIcon: {
placeSelf: "flex-start",
marginTop: 2,
},
propsContainer: {
padding: theme.spacing(3),
},
propsLabel: {
color: theme.palette.text.secondary,
padding: theme.spacing(1),
},
propsTime: {
color: theme.palette.text.disabled,
padding: theme.spacing(1),
},
propsValue: {
padding: theme.spacing(1),
wordBreak: "break-all",
},
appBar: {
position: "relative",
},
title: {
marginLeft: theme.spacing(2),
flex: 1,
},
}));
const Transition = React.forwardRef(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />;
});
export default function SideDrawer() {
const { t } = useTranslation();
const dispatch = useDispatch();
const sideBarOpen = useSelector((state) => state.explorer.sideBarOpen);
const selected = useSelector((state) => state.explorer.selected);
const SetSideBar = useCallback((open) => dispatch(setSideBar(open)), [
dispatch,
]);
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const NavigateTo = useCallback((k) => dispatch(navigateTo(k)), [dispatch]);
const search = useSelector((state) => state.explorer.search);
const [target, setTarget] = useState(null);
const [details, setDetails] = useState(null);
const loadProps = (object) => {
API.get(
"/object/property/" +
object.id +
"?trace_root=" +
(search ? "true" : "false") +
"&is_folder=" +
(object.type === "dir").toString()
)
.then((response) => {
setDetails(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
setDetails(null);
if (sideBarOpen) {
if (selected.length !== 1) {
SetSideBar(false);
} else {
setTarget(selected[0]);
loadProps(selected[0]);
}
}
}, [selected, sideBarOpen]);
const classes = useStyles();
const propsItem = [
{
label: t("fileManager.size"),
value: (d, target) =>
sizeToString(d.size) +
t("fileManager.bytes", { bytes: d.size.toLocaleString() }),
show: (d) => true,
},
{
label: t("fileManager.storagePolicy"),
value: (d, target) => d.policy,
show: (d) => d.type === "file",
},
{
label: t("fileManager.storagePolicy"),
value: (d, target) =>
d.policy === ""
? t("fileManager.inheritedFromParent")
: d.policy,
show: (d) => d.type === "dir",
},
{
label: t("fileManager.childFolders"),
value: (d, target) =>
t("fileManager.childCount", {
num: d.child_folder_num.toLocaleString(),
}),
show: (d) => d.type === "dir",
},
{
label: t("fileManager.childFiles"),
value: (d, target) =>
t("fileManager.childCount", {
num: d.child_file_num.toLocaleString(),
}),
show: (d) => d.type === "dir",
},
{
label: t("fileManager.parentFolder"),
// eslint-disable-next-line react/display-name
value: (d, target) => {
const path = d.path === "" ? target.path : d.path;
const name = filename(path);
return (
<Tooltip title={path}>
<Link
href={"javascript:void"}
onClick={() => NavigateTo(path)}
>
{name === "" ? t("fileManager.rootFolder") : name}
</Link>
</Tooltip>
);
},
show: (d) => true,
},
{
label: t("fileManager.modifiedAt"),
value: (d, target) => formatLocalTime(d.updated_at),
show: (d) => true,
},
{
label: t("fileManager.createdAt"),
value: (d) => formatLocalTime(d.created_at),
show: (d) => true,
},
];
const content = (
<Grid container className={classes.propsContainer}>
{!details && <ListLoading />}
{details && (
<>
{propsItem.map((item) => {
if (item.show(target)) {
return (
<>
<Grid
item
xs={5}
className={classes.propsLabel}
>
{item.label}
</Grid>
<Grid
item
xs={7}
className={classes.propsValue}
>
{item.value(details, target)}
</Grid>
</>
);
}
})}
{target.type === "dir" && (
<Grid item xs={12} className={classes.propsTime}>
<Trans
i18nKey="fileManager.statisticAt"
components={[
<span key={0} />,
<TimeAgo
key={1}
datetime={details.query_date}
locale={t("timeAgoLocaleCode", {
ns: "common",
})}
/>,
<span key={2} />,
]}
/>
</Grid>
)}
</>
)}
</Grid>
);
return (
<>
<Hidden smUp>
<Dialog
fullScreen
open={sideBarOpen}
TransitionComponent={Transition}
>
{target && (
<>
<AppBar className={classes.appBar}>
<Toolbar>
<IconButton
edge="start"
color="inherit"
onClick={() => SetSideBar(false)}
aria-label="close"
>
<Clear />
</IconButton>
<Typography
variant="h6"
className={classes.title}
>
{target.name}
</Typography>
</Toolbar>
</AppBar>
{content}
</>
)}
</Dialog>
</Hidden>
<Hidden xsDown>
<Drawer
className={classes.drawer}
variant="persistent"
classes={{
paper: classes.drawerPaper,
}}
open={sideBarOpen}
anchor="right"
>
<Toolbar />
<div className={classes.drawerContainer}>
{target && (
<>
<div className={classes.header}>
{target.type === "dir" && (
<Folder
className={classes.folderIcon}
/>
)}
{target.type !== "dir" && (
<TypeIcon
isUpload
className={classes.fileIcon}
iconClassName={classes.fileIconSVG}
fileName={target.name}
/>
)}
<div className={classes.fileName}>
<Typography variant="h6" gutterBottom>
{target.name}
</Typography>
</div>
<IconButton
onClick={() => SetSideBar(false)}
className={classes.closeIcon}
aria-label="close"
size={"small"}
>
<Clear />
</IconButton>
</div>
</>
)}
<Divider />
{content}
</div>
</Drawer>
</Hidden>
</>
);
}

View File

@@ -0,0 +1,183 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import classNames from "classnames";
import {
ButtonBase,
fade,
Tooltip,
Typography,
withStyles,
} from "@material-ui/core";
import TypeIcon from "./TypeIcon";
import CheckCircleRoundedIcon from "@material-ui/icons/CheckCircleRounded";
import Grow from "@material-ui/core/Grow";
import { Folder } from "@material-ui/icons";
import FileName from "./FileName";
const styles = (theme) => ({
container: {
padding: "7px",
},
selected: {
"&:hover": {
border: "1px solid #d0d0d0",
},
backgroundColor: fade(
theme.palette.primary.main,
theme.palette.type === "dark" ? 0.3 : 0.18
),
},
notSelected: {
"&:hover": {
backgroundColor: theme.palette.background.default,
border: "1px solid #d0d0d0",
},
backgroundColor: theme.palette.background.paper,
},
button: {
height: "50px",
border: "1px solid " + theme.palette.divider,
width: "100%",
borderRadius: theme.shape.borderRadius,
boxSizing: "border-box",
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",
display: "flex",
justifyContent: "left",
alignItems: "initial",
},
icon: {
margin: "10px 10px 10px 16px",
height: "30px",
minWidth: "30px",
backgroundColor: theme.palette.background.paper,
borderRadius: "90%",
paddingTop: "3px",
color: theme.palette.text.secondary,
},
folderNameSelected: {
color:
theme.palette.type === "dark" ? "#fff" : theme.palette.primary.dark,
fontWeight: "500",
},
folderNameNotSelected: {
color: theme.palette.text.secondary,
},
folderName: {
marginTop: "15px",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
marginRight: "20px",
},
checkIcon: {
color: theme.palette.primary.main,
},
});
const mapStateToProps = (state) => {
return {
selected: state.explorer.selected,
};
};
const mapDispatchToProps = () => {
return {};
};
class SmallIconCompoment extends Component {
state = {};
shouldComponentUpdate(nextProps, nextState, nextContext) {
const isSelectedCurrent =
this.props.selected.findIndex((value) => {
return value === this.props.file;
}) !== -1;
const isSelectedNext =
nextProps.selected.findIndex((value) => {
return value === this.props.file;
}) !== -1;
if (
nextProps.selected !== this.props.selected &&
isSelectedCurrent === isSelectedNext
) {
return false;
}
return true;
}
render() {
const { classes } = this.props;
const isSelected =
this.props.selected.findIndex((value) => {
return value === this.props.file;
}) !== -1;
return (
<ButtonBase
focusRipple
className={classNames(
{
[classes.selected]: isSelected,
[classes.notSelected]: !isSelected,
},
classes.button
)}
>
<div
onClick={this.props.onIconClick}
className={classNames(classes.icon, {
[classes.iconSelected]: isSelected,
[classes.iconNotSelected]: !isSelected,
})}
>
{!isSelected && (
<>
{this.props.isFolder && <Folder />}
{!this.props.isFolder && (
<TypeIcon fileName={this.props.file.name} />
)}
</>
)}
{isSelected && (
<Grow in={isSelected}>
<CheckCircleRoundedIcon
className={classes.checkIcon}
/>
</Grow>
)}
</div>
<Tooltip
title={this.props.file.name}
aria-label={this.props.file.name}
>
<Typography
className={classNames(classes.folderName, {
[classes.folderNameSelected]: isSelected,
[classes.folderNameNotSelected]: !isSelected,
})}
variant="body2"
>
<FileName name={this.props.file.name} />
</Typography>
</Tooltip>
</ButtonBase>
);
}
}
SmallIconCompoment.propTypes = {
classes: PropTypes.object.isRequired,
file: PropTypes.object.isRequired,
};
const SmallIcon = connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(SmallIconCompoment));
export default SmallIcon;

View File

@@ -0,0 +1,105 @@
import React, { MouseEventHandler, useState } from "react";
import { IconButton, Menu, MenuItem } from "@material-ui/core";
import TextTotateVerticalIcon from "@material-ui/icons/TextRotateVertical";
import { useTranslation } from "react-i18next";
import { CloudreveFile, SortMethod } from "./../../types/index";
const SORT_OPTIONS: {
value: SortMethod;
label: string;
}[] = [
{ value: "namePos", label: "A-Z" },
{ value: "nameRev", label: "Z-A" },
{ value: "timePos", label: "oldestUploaded" },
{ value: "timeRev", label: "newestUploaded" },
{ value: "modifyTimePos", label: "oldestModified" },
{ value: "modifyTimeRev", label: "newestModified" },
{ value: "sizePos", label: "smallest" },
{ value: "sizeRes", label: "largest" },
]
export default function Sort({ value, onChange, isSmall, inherit, className }) {
const { t } = useTranslation("application", { keyPrefix: "fileManager.sortMethods" });
const [anchorSort, setAnchorSort] = useState<Element | null>(null);
const showSortOptions: MouseEventHandler<HTMLButtonElement> = (e) => {
setAnchorSort(e.currentTarget);
}
const [sortBy, setSortBy] = useState<SortMethod>(value || '')
function onChangeSort(value: SortMethod) {
setSortBy(value)
onChange(value)
setAnchorSort(null);
}
return (
<>
<IconButton
title={t("sortMethod")}
className={className}
onClick={showSortOptions}
color={inherit ? "inherit" : "default"}
>
<TextTotateVerticalIcon
fontSize={isSmall ? "small" : "default"}
/>
</IconButton>
<Menu
id="sort-menu"
anchorEl={anchorSort}
open={Boolean(anchorSort)}
onClose={() => setAnchorSort(null)}
>
{
SORT_OPTIONS.map((option, index) => (
<MenuItem
key={index}
selected={option.value === sortBy}
onClick={() => onChangeSort(option.value)}
>
{t(option.label)}
</MenuItem>
))
}
</Menu>
</>
)
}
type SortFunc = (a: CloudreveFile, b: CloudreveFile) => number;
export const sortMethodFuncs: Record<SortMethod, SortFunc> = {
sizePos: (a: CloudreveFile, b: CloudreveFile) => {
return a.size - b.size;
},
sizeRes: (a: CloudreveFile, b: CloudreveFile) => {
return b.size - a.size;
},
namePos: (a: CloudreveFile, b: CloudreveFile) => {
return a.name.localeCompare(
b.name,
navigator.languages[0] || navigator.language,
{ numeric: true, ignorePunctuation: true }
);
},
nameRev: (a: CloudreveFile, b: CloudreveFile) => {
return b.name.localeCompare(
a.name,
navigator.languages[0] || navigator.language,
{ numeric: true, ignorePunctuation: true }
);
},
timePos: (a: CloudreveFile, b: CloudreveFile) => {
return Date.parse(a.create_date) - Date.parse(b.create_date);
},
timeRev: (a: CloudreveFile, b: CloudreveFile) => {
return Date.parse(b.create_date) - Date.parse(a.create_date);
},
modifyTimePos: (a: CloudreveFile, b: CloudreveFile) => {
return Date.parse(a.date) - Date.parse(b.date);
},
modifyTimeRev: (a: CloudreveFile, b: CloudreveFile) => {
return Date.parse(b.date) - Date.parse(a.date);
},
};

View File

@@ -0,0 +1,229 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import FolderIcon from "@material-ui/icons/Folder";
import classNames from "classnames";
import { sizeToString } from "../../utils/index";
import {
fade,
TableCell,
TableRow,
Typography,
withStyles,
} from "@material-ui/core";
import TypeIcon from "./TypeIcon";
import pathHelper from "../../utils/page";
import statusHelper from "../../utils/page";
import { withRouter } from "react-router";
import KeyboardReturnIcon from "@material-ui/icons/KeyboardReturn";
import CheckCircleRoundedIcon from "@material-ui/icons/CheckCircleRounded";
import Grow from "@material-ui/core/Grow";
import { formatLocalTime } from "../../utils/datetime";
import FileName from "./FileName";
const styles = (theme) => ({
selected: {
"&:hover": {},
backgroundColor: fade(theme.palette.primary.main, 0.18),
},
selectedShared: {
"&:hover": {},
backgroundColor: fade(theme.palette.primary.main, 0.18),
},
notSelected: {
"&:hover": {
backgroundColor: theme.palette.action.hover,
},
},
icon: {
verticalAlign: "middle",
marginRight: "20px",
color: theme.palette.text.secondary,
},
tableIcon: {
marginRight: "20px",
verticalAlign: "middle",
},
folderNameSelected: {
color:
theme.palette.type === "dark" ? "#fff" : theme.palette.primary.dark,
fontWeight: "500",
userSelect: "none",
},
folderNameNotSelected: {
color: theme.palette.text.secondary,
userSelect: "none",
},
folderName: {
marginRight: "20px",
display: "flex",
alignItems: "center",
},
hideAuto: {
[theme.breakpoints.down("sm")]: {
display: "none",
},
},
tableRow: {
padding: "10px 16px",
},
checkIcon: {
color: theme.palette.primary.main,
},
active: {
backgroundColor: fade(theme.palette.primary.main, 0.1),
},
});
const mapStateToProps = (state) => {
return {
selected: state.explorer.selected,
};
};
const mapDispatchToProps = () => {
return {};
};
class TableRowCompoment extends Component {
state = {};
shouldComponentUpdate(nextProps, nextState, nextContext) {
const isSelectedCurrent =
this.props.selected.findIndex((value) => {
return value === this.props.file;
}) !== -1;
const isSelectedNext =
nextProps.selected.findIndex((value) => {
return value === this.props.file;
}) !== -1;
if (
nextProps.selected !== this.props.selected &&
isSelectedCurrent === isSelectedNext
) {
return false;
}
return true;
}
render() {
const { classes } = this.props;
const isShare = pathHelper.isSharePage(this.props.location.pathname);
let icon;
if (this.props.file.type === "dir") {
icon = <FolderIcon className={classes.icon} />;
} else if (this.props.file.type === "up") {
icon = <KeyboardReturnIcon className={classes.icon} />;
} else {
icon = (
<TypeIcon
className={classes.tableIcon}
fileName={this.props.file.name}
/>
);
}
const isSelected =
this.props.selected.findIndex((value) => {
return value === this.props.file;
}) !== -1;
const isMobile = statusHelper.isMobile();
return (
<TableRow
ref={this.props.pref}
onContextMenu={this.props.contextMenu}
onClick={this.props.handleClick}
onDoubleClick={this.props.handleDoubleClick.bind(this)}
className={classNames({
[classes.selected]: isSelected && !isShare,
[classes.selectedShared]: isSelected && isShare,
[classes.notSelected]: !isSelected,
[classes.active]: this.props.isActive,
})}
>
<TableCell
ref={this.props.dref}
component="th"
scope="row"
className={classes.tableRow}
>
<Typography
variant="body2"
className={classNames(classes.folderName, {
[classes.folderNameSelected]: isSelected,
[classes.folderNameNotSelected]: !isSelected,
})}
>
<div
onClick={
this.props.file.type !== "up"
? this.props.onIconClick
: null
}
>
{!isSelected && icon}
{isSelected && (
<Grow in={isSelected}>
<CheckCircleRoundedIcon
className={classNames(
classes.checkIcon,
classes.icon
)}
/>
</Grow>
)}
</div>
<FileName name={this.props.file.name} />
</Typography>
</TableCell>
<TableCell
className={classNames(classes.hideAuto, classes.tableRow)}
>
<Typography
variant="body2"
className={classNames(classes.folderName, {
[classes.folderNameSelected]: isSelected,
[classes.folderNameNotSelected]: !isSelected,
})}
>
{" "}
{this.props.file.type !== "dir" &&
this.props.file.type !== "up" &&
sizeToString(this.props.file.size)}
</Typography>
</TableCell>
<TableCell
className={classNames(classes.hideAuto, classes.tableRow)}
>
<Typography
variant="body2"
className={classNames(classes.folderName, {
[classes.folderNameSelected]: isSelected,
[classes.folderNameNotSelected]: !isSelected,
})}
>
{" "}
{formatLocalTime(this.props.file.date)}
</Typography>
</TableCell>
</TableRow>
);
}
}
TableRowCompoment.propTypes = {
classes: PropTypes.object.isRequired,
file: PropTypes.object.isRequired,
};
const TableItem = connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(withRouter(TableRowCompoment)));
export default TableItem;

View File

@@ -0,0 +1,162 @@
import React from "react";
import { mediaType } from "../../config";
import ImageIcon from "@material-ui/icons/PhotoSizeSelectActual";
import VideoIcon from "@material-ui/icons/Videocam";
import AudioIcon from "@material-ui/icons/Audiotrack";
import PdfIcon from "@material-ui/icons/PictureAsPdf";
import {
Android,
FileExcelBox,
FilePowerpointBox,
FileWordBox,
LanguageC,
LanguageCpp,
LanguageGo,
LanguageJavascript,
LanguagePhp,
LanguagePython,
MagnetOn,
ScriptText,
WindowRestore,
ZipBox,
} from "mdi-material-ui";
import FileShowIcon from "@material-ui/icons/InsertDriveFile";
import { lighten } from "@material-ui/core/styles";
import useTheme from "@material-ui/core/styles/useTheme";
import { Avatar } from "@material-ui/core";
import { MenuBook } from "@material-ui/icons";
const icons = {
audio: {
color: "#651fff",
icon: AudioIcon,
},
video: {
color: "#d50000",
icon: VideoIcon,
},
image: {
color: "#d32f2f",
icon: ImageIcon,
},
pdf: {
color: "#f44336",
icon: PdfIcon,
},
word: {
color: "#538ce5",
icon: FileWordBox,
},
ppt: {
color: "rgb(239, 99, 63)",
icon: FilePowerpointBox,
},
excel: {
color: "#4caf50",
icon: FileExcelBox,
},
text: {
color: "#607d8b",
icon: ScriptText,
},
torrent: {
color: "#5c6bc0",
icon: MagnetOn,
},
zip: {
color: "#f9a825",
icon: ZipBox,
},
excute: {
color: "#1a237e",
icon: WindowRestore,
},
android: {
color: "#8bc34a",
icon: Android,
},
file: {
color: "#607d8b",
icon: FileShowIcon,
},
php: {
color: "#777bb3",
icon: LanguagePhp,
},
go: {
color: "#16b3da",
icon: LanguageGo,
},
python: {
color: "#3776ab",
icon: LanguagePython,
},
c: {
color: "#a8b9cc",
icon: LanguageC,
},
cpp: {
color: "#004482",
icon: LanguageCpp,
},
js: {
color: "#f4d003",
icon: LanguageJavascript,
},
epub: {
color: "#81b315",
icon: MenuBook,
},
};
const getColor = (theme, color) =>
theme.palette.type === "light" ? color : lighten(color, 0.2);
let color;
const TypeIcon = (props) => {
const theme = useTheme();
const fileSuffix = props.fileName.split(".").pop().toLowerCase();
let fileType = "file";
Object.keys(mediaType).forEach((k) => {
if (mediaType[k].indexOf(fileSuffix) !== -1) {
fileType = k;
}
});
const IconComponent = icons[fileType].icon;
color = getColor(theme, icons[fileType].color);
if (props.getColorValue) {
props.getColorValue(color);
}
return (
<>
{props.isUpload && (
<Avatar
className={props.className}
style={{
backgroundColor: color,
}}
>
<IconComponent
className={props.iconClassName}
style={{
color: theme.palette.background.paper,
}}
/>
</Avatar>
)}
{!props.isUpload && (
<IconComponent
className={props.className}
style={{
color: color,
}}
/>
)}
</>
);
};
export default TypeIcon;

Some files were not shown because too many files have changed in this diff Show More