feat(thumb): generator settings and test button

This commit is contained in:
Aaron Liu 2023-04-07 19:35:00 +08:00
parent bc47a382bd
commit dc7f69cbfb
4 changed files with 346 additions and 6 deletions

View File

@ -213,7 +213,32 @@
"officePreviewServiceSrcB64Des": " Base64 编码后的文件 URL",
"officePreviewServiceName": "文件名",
"thumbnails": "缩略图",
"localOnlyInfo": "以下设置只针对本机存储策略有效。",
"thumbnailDoc": "有关配置缩略图的更多信息,请参阅 <0>官方文档</0>。",
"thumbnailDocLink":"https://docs.cloudreve.org/use/thumbnails",
"thumbnailBasic": "基本设置",
"generators": "生成器",
"thumbMaxSize": "最大原始文件尺寸",
"thumbMaxSizeDes": "可生成缩略图的最大原始文件的大小,超出此大小的文件不会生成缩略图",
"generatorProxyWarning": "默认情况下,非本机存储策略只会使用“存储策略原生”生成器。你可以通过开启“生成器代理”功能扩展第三方存储策略的缩略图能力。",
"policyBuiltin": "存储策略原生",
"policyBuiltinDes": "使用存储提供方原生的图像处理接口。对于本机和 S3 策略,这一生成器不可用,将会自动顺沿其他生成器。对于其他存储策略,支持的原始图像格式和大小限制请参考 Cloudreve 文档。",
"cloudreveBuiltin":"Cloudreve 内置",
"cloudreveBuiltinDes": "使用 Cloudreve 内置的图像处理能力,仅支持 PNG、JPEG、GIF 格式的图片。",
"libreOffice": "LibreOffice",
"libreOfficeDes": "使用 LibreOffice 生成 Office 文档的缩略图。这一生成器依赖于任一其他图像生成器Cloudreve 内置 或 VIPS。",
"vips": "VIPS",
"vipsDes": "使用 libvips 处理缩略图图像,支持更多图像格式,资源消耗更低。",
"thumbDependencyWarning": "LibreOffice 生成器依赖于 Cloudreve 内置 或 VIPS 生成器,请开启其中任一生成器。",
"ffmpeg": "FFmpeg",
"ffmpegDes": "使用 FFmpeg 生成视频缩略图。",
"executable": "可执行文件",
"executableDes": "第三方生成器可执行文件的地址或命令",
"executableTest": "测试",
"executableTestSuccess": "生成器正常,版本:{{version}}",
"generatorExts": "可用扩展名",
"generatorExtsDes": "此生成器可用的文件扩展名列表,多个请使用半角逗号 , 隔开",
"ffmpegSeek": "缩略图截取位置",
"ffmpegSeekDes": "定义缩略图截取的时间,推荐选择较小值以加速生成过程。如果超出视频实际长度,会导致缩略图截取失败。",
"thumbWidth": "缩略图宽度",
"thumbHeight": "缩略图高度",
"thumbSuffix": "缩略图文件后缀",

View File

@ -15,6 +15,7 @@ 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";
const useStyles = makeStyles((theme) => ({
root: {
@ -58,6 +59,20 @@ export default function ImageSetting() {
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) => {
@ -388,12 +403,26 @@ export default function ImageSetting() {
<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}>
<Alert severity="info">{t("localOnlyInfo")}</Alert>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
@ -510,6 +539,26 @@ export default function ImageSetting() {
</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
@ -529,6 +578,24 @@ export default function ImageSetting() {
</FormControl>
</div>
</div>
<Typography variant="subtitle1" gutterBottom>
{t("generators")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Alert severity="info">
{t("generatorProxyWarning")}
</Alert>
</div>
<div className={classes.form}>
<ThumbGenerators
options={options}
setOptions={setOptions}
/>
</div>
</div>
</div>
<div className={classes.root}>

View File

@ -103,7 +103,7 @@ export default function Mail() {
const sendTestMail = () => {
setLoading(true);
API.post("/admin/mailTest", {
API.post("/admin/test/mail", {
to: tesInput,
})
.then(() => {

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>
);
}