chore: update

This commit is contained in:
huzibaca
2024-11-08 21:46:15 +08:00
Unverified
parent 2887a2b6d3
commit c22e4e5e2c
20 changed files with 887 additions and 4 deletions

View File

@@ -0,0 +1,33 @@
import React from "react";
import { Box, CircularProgress } from "@mui/material";
export interface BaseLoadingOverlayProps {
isLoading: boolean;
}
export const BaseLoadingOverlay: React.FC<BaseLoadingOverlayProps> = ({
isLoading,
}) => {
if (!isLoading) return null;
return (
<Box
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(255, 255, 255, 0.7)",
zIndex: 1000,
}}
>
<CircularProgress />
</Box>
);
};
export default BaseLoadingOverlay;

View File

@@ -5,3 +5,4 @@ export { BaseLoading } from "./base-loading";
export { BaseErrorBoundary } from "./base-error-boundary";
export { Notice } from "./base-notice";
export { Switch } from "./base-switch";
export { BaseLoadingOverlay } from "./base-loading-overlay";

View File

@@ -0,0 +1,330 @@
import { forwardRef, useImperativeHandle, useState, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import { Typography } from "@mui/material";
import { useForm } from "react-hook-form";
import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { isValidUrl } from "@/utils/helper";
import { BaseLoadingOverlay } from "@/components/base";
import {
TextField,
Button,
Grid,
Box,
Paper,
Stack,
IconButton,
InputAdornment,
Divider,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@mui/material";
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import DeleteIcon from "@mui/icons-material/Delete";
import RestoreIcon from "@mui/icons-material/Restore";
import { createWebdavBackup, saveWebdavConfig } from "@/services/cmds";
import { save } from "@tauri-apps/plugin-dialog";
export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const { verge, mutateVerge } = useVerge();
const { webdav_url, webdav_username, webdav_password } = verge || {};
const [showPassword, setShowPassword] = useState(false);
const usernameRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const urlRef = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState(false);
const { register, handleSubmit, watch } = useForm<IWebDavConfig>({
defaultValues: {
url: webdav_url,
username: webdav_username,
password: webdav_password,
},
});
const url = watch("url");
const username = watch("username");
const password = watch("password");
const webdavChanged =
webdav_url !== url ||
webdav_username !== username ||
webdav_password !== password;
// const backups = [] as any[];
const backups = [
{ name: "backup1.zip" },
{ name: "backup2.zip" },
{ name: "backup3.zip" },
];
useImperativeHandle(ref, () => ({
open: () => {
setOpen(true);
},
close: () => setOpen(false),
}));
const checkForm = () => {
const username = usernameRef.current?.value;
const password = passwordRef.current?.value;
const url = urlRef.current?.value;
if (!url) {
Notice.error(t("Webdav url cannot be empty"));
urlRef.current?.focus();
return;
} else if (!isValidUrl(url)) {
Notice.error(t("Webdav address must be url"));
urlRef.current?.focus();
return;
}
if (!username) {
Notice.error(t("Username cannot be empty"));
usernameRef.current?.focus();
return;
}
if (!password) {
Notice.error(t("Password cannot be empty"));
passwordRef.current?.focus();
return;
}
};
const submit = async (data: IWebDavConfig) => {
checkForm();
setIsLoading(true);
await saveWebdavConfig(data.url, data.username, data.password)
.then(() => {
mutateVerge(
{
webdav_url: data.url,
webdav_username: data.username,
webdav_password: data.password,
},
false
);
Notice.success(t("Webdav Config Saved Successfully"), 1500);
})
.catch((e) => {
Notice.error(t("Webdav Config Save Failed", { error: e }), 3000);
})
.finally(() => {
setIsLoading(false);
});
};
const handleClickShowPassword = () => {
setShowPassword(!showPassword);
};
const handleBackup = useLockFn(async () => {
checkForm();
setIsLoading(true);
await createWebdavBackup()
.then(() => {
Notice.success(t("Backup Successfully"), 1500);
})
.finally(() => {
setIsLoading(false);
})
.catch((e) => {
console.log(e, "backup failed");
Notice.error(t("Backup Failed", { error: e }), 3000);
});
});
return (
<BaseDialog
open={open}
title={t("Backup Setting")}
contentSx={{ width: 600, maxHeight: 800 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
disableFooter={true}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
<Box sx={{ maxWidth: 800 }}>
<BaseLoadingOverlay isLoading={isLoading} />
<Paper elevation={2} sx={{ padding: 2 }}>
<form onSubmit={handleSubmit(submit)}>
<Grid container spacing={2}>
<Grid item xs={12} sm={9}>
<Grid container spacing={2}>
{/* WebDAV Server Address */}
<Grid item xs={12}>
<TextField
fullWidth
label="WebDAV Server URL"
variant="outlined"
size="small"
{...register("url")}
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={urlRef}
/>
</Grid>
{/* Username and Password */}
<Grid item xs={6}>
<TextField
label="Username"
variant="outlined"
size="small"
{...register("username")}
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={usernameRef}
/>
</Grid>
<Grid item xs={6}>
<TextField
label="Password"
type={showPassword ? "text" : "password"}
variant="outlined"
size="small"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={passwordRef}
{...register("password")}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleClickShowPassword}
edge="end"
>
{showPassword ? (
<VisibilityOff />
) : (
<Visibility />
)}
</IconButton>
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
</Grid>
<Grid item xs={12} sm={3}>
<Stack
direction="column"
justifyContent="center"
alignItems="stretch"
sx={{ height: "100%" }}
>
{webdavChanged ||
webdav_url === null ||
webdav_username == null ||
webdav_password == null ? (
<Button
variant="contained"
color="primary"
sx={{ height: "100%" }}
type="submit"
>
Save
</Button>
) : (
<Button
variant="contained"
color="success"
sx={{ height: "100%" }}
onClick={handleBackup}
type="button"
>
Backup
</Button>
)}
</Stack>
</Grid>
</Grid>
</form>
<Divider sx={{ marginY: 2 }} />
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell></TableCell>
<TableCell align="right"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{backups.length > 0 ? (
backups?.map((backup, index) => (
<TableRow key={index}>
<TableCell component="th" scope="row">
{backup.name}
</TableCell>
<TableCell align="right">
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<IconButton
color="secondary"
aria-label="delete"
size="small"
>
<DeleteIcon />
</IconButton>
<Divider
orientation="vertical"
flexItem
sx={{ mx: 1, height: 24 }}
/>
<IconButton
color="primary"
aria-label="restore"
size="small"
>
<RestoreIcon />
</IconButton>
</Box>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={2} align="center">
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: 150,
}}
>
<Typography variant="body1" color="textSecondary">
</Typography>
</Box>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</Paper>
</Box>
</BaseDialog>
);
});

View File

@@ -23,6 +23,7 @@ import { ThemeViewer } from "./mods/theme-viewer";
import { GuardState } from "./mods/guard-state";
import { LayoutViewer } from "./mods/layout-viewer";
import { UpdateViewer } from "./mods/update-viewer";
import { BackupViewer } from "./mods/backup-viewer";
import getSystem from "@/utils/get-system";
import { routers } from "@/pages/_routers";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
@@ -52,6 +53,7 @@ const SettingVerge = ({ onError }: Props) => {
const themeRef = useRef<DialogRef>(null);
const layoutRef = useRef<DialogRef>(null);
const updateRef = useRef<DialogRef>(null);
const backupRef = useRef<DialogRef>(null);
const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false);
@@ -83,6 +85,7 @@ const SettingVerge = ({ onError }: Props) => {
<MiscViewer ref={miscRef} />
<LayoutViewer ref={layoutRef} />
<UpdateViewer ref={updateRef} />
<BackupViewer ref={backupRef} />
<SettingItem label={t("Language")}>
<GuardState
@@ -238,6 +241,11 @@ const SettingVerge = ({ onError }: Props) => {
label={t("Hotkey Setting")}
/>
<SettingItem
onClick={() => backupRef.current?.open()}
label={t("Backup Setting")}
/>
<SettingItem
onClick={() => configRef.current?.open()}
label={t("Runtime Config")}

View File

@@ -332,6 +332,7 @@
"clash_mode_direct": "Direct Mode",
"toggle_system_proxy": "Enable/Disable System Proxy",
"toggle_tun_mode": "Enable/Disable Tun Mode",
"Backup Setting": "Backup Setting",
"Runtime Config": "Runtime Config",
"Open Conf Dir": "Open Conf Dir",
"Open Core Dir": "Open Core Dir",

View File

@@ -330,6 +330,7 @@
"clash_mode_direct": "حالت مستقیم",
"toggle_system_proxy": "فعال/غیرفعال کردن پراکسی سیستم",
"toggle_tun_mode": "فعال/غیرفعال کردن حالت Tun",
"Backup Setting": "تنظیمات پشتیبان",
"Runtime Config": "پیکربندی زمان اجرا",
"Open Conf Dir": "باز کردن پوشه برنامه",
"Open Core Dir": "باز کردن پوشه هسته",

View File

@@ -330,6 +330,7 @@
"clash_mode_direct": "Прямой режим",
"toggle_system_proxy": "Включить/Отключить системный прокси",
"toggle_tun_mode": "Включить/Отключить режим туннеля",
"Backup Setting": "Настройки резервного копирования",
"Runtime Config": "Используемый конфиг",
"Open Conf Dir": "Открыть папку приложения",
"Open Core Dir": "Открыть папку ядра",

View File

@@ -332,6 +332,7 @@
"clash_mode_direct": "直连模式",
"toggle_system_proxy": "打开/关闭系统代理",
"toggle_tun_mode": "打开/关闭 Tun 模式",
"Backup Setting": "备份设置",
"Runtime Config": "当前配置",
"Open Conf Dir": "配置目录",
"Open Core Dir": "内核目录",

View File

@@ -236,3 +236,26 @@ export async function getNetworkInterfaces() {
export async function getNetworkInterfacesInfo() {
return invoke<INetworkInterface[]>("get_network_interfaces_info");
}
export async function createWebdavBackup() {
return invoke<void>("create_webdav_backup");
}
export async function saveWebdavConfig(
url: string,
username: string,
password: String
) {
return invoke<void>("save_webdav_config", {
url,
username,
password,
});
}
export async function listWebDavBackup() {
let list: IWebDavFile[] = await invoke<IWebDavFile[]>("list_webdav_backup");
list.map((item) => {
item.filename = item.href.split("/").pop() as string;
});
return list;
}

View File

@@ -744,4 +744,22 @@ interface IVergeConfig {
auto_log_clean?: 0 | 1 | 2 | 3;
proxy_layout_column?: number;
test_list?: IVergeTestItem[];
webdav_url?: string;
webdav_username?: string;
webdav_password?: string;
}
interface IWebDavFile {
filename: string;
href: string;
last_modified: string;
content_length: number;
content_type: string;
tag: string;
}
interface IWebDavConfig {
url: string;
username: string;
password: string;
}

9
src/utils/helper.ts Normal file
View File

@@ -0,0 +1,9 @@
export const isValidUrl = (url: string) => {
try {
new URL(url);
return true;
} catch (e) {
console.log(e);
return false;
}
};