chore: update

This commit is contained in:
huzibaca 2024-11-09 06:56:58 +08:00
parent f5dee51e9c
commit 19bb9c7f50
5 changed files with 429 additions and 270 deletions

View File

@ -4,11 +4,14 @@ import {
useState, useState,
useRef, useRef,
SVGProps, SVGProps,
useCallback,
useMemo,
memo,
} from "react"; } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
import { useForm } from "react-hook-form"; import { useForm, UseFormRegister } from "react-hook-form";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef, Notice } from "@/components/base"; import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { isValidUrl } from "@/utils/helper"; import { isValidUrl } from "@/utils/helper";
@ -53,6 +56,36 @@ type BackupFile = IWebDavFile & {
allow_apply: boolean; allow_apply: boolean;
}; };
interface BackupTableProps {
datasource: BackupFile[];
page: number;
rowsPerPage: number;
onPageChange: (event: any, newPage: number) => void;
onRowsPerPageChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
totalCount: number;
}
interface WebDAVConfigFormProps {
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
initialValues: Partial<IWebDavConfig>;
urlRef: React.RefObject<HTMLInputElement>;
usernameRef: React.RefObject<HTMLInputElement>;
passwordRef: React.RefObject<HTMLInputElement>;
showPassword: boolean;
onShowPasswordClick: () => void;
webdavChanged: boolean;
webdavUrl: string | undefined | null;
webdavUsername: string | undefined | null;
webdavPassword: string | undefined | null;
handleBackup: () => Promise<void>;
register: UseFormRegister<IWebDavConfig>;
}
// 将魔法数字和配置提取为常量
const DEFAULT_ROWS_PER_PAGE = 5;
const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss";
const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/;
export const BackupViewer = forwardRef<DialogRef>((props, ref) => { export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -62,13 +95,16 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const usernameRef = useRef<HTMLInputElement>(null); const usernameRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null); const passwordRef = useRef<HTMLInputElement>(null);
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([]); const [backupState, setBackupState] = useState({
const [page, setPage] = useState(0); files: [] as BackupFile[],
const [rowsPerPage, setRowsPerPage] = useState(5); page: 0,
rowsPerPage: DEFAULT_ROWS_PER_PAGE,
isLoading: false,
});
const OS = getSystem(); const OS = getSystem();
const urlRef = useRef<HTMLInputElement>(null); const urlRef = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState(false);
const { register, handleSubmit, watch } = useForm<IWebDavConfig>({ const { register, handleSubmit, watch } = useForm<IWebDavConfig>({
defaultValues: { defaultValues: {
url: webdav_url, url: webdav_url,
@ -96,34 +132,36 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
})); }));
// Handle page change // Handle page change
const handleChangePage = ( const handleChangePage = useCallback(
_: React.MouseEvent<HTMLButtonElement> | null, (_: React.MouseEvent<HTMLButtonElement> | null, page: number) => {
page: number setBackupState((prev) => ({ ...prev, page }));
) => { },
console.log(page); []
setPage(page); );
};
// Handle rows per page change // Handle rows per page change
const handleChangeRowsPerPage = (event: any) => { const handleChangeRowsPerPage = useCallback(
setRowsPerPage(parseInt(event.target.value, 10)); (event: React.ChangeEvent<HTMLInputElement>) => {
setPage(0); // Reset to the first page setBackupState((prev) => ({
}; ...prev,
rowsPerPage: parseInt(event.target.value, 10),
page: 0,
}));
},
[]
);
const fetchAndSetBackupFiles = () => { const fetchAndSetBackupFiles = async () => {
setIsLoading(true); // Assuming setIsLoading is defined in your component or context to manage loading state try {
setBackupState((prev) => ({ ...prev, isLoading: true }));
getAllBackupFiles() const files = await getAllBackupFiles();
.then((files: BackupFile[]) => { setBackupState((prev) => ({ ...prev, files }));
console.log(files); } catch (error) {
setBackupFiles(files); // Assuming setBackupFiles is a state setter function in your component or context console.error("Failed to fetch backup files:", error);
}) Notice.error(t("Failed to fetch backup files"));
.catch((e) => { } finally {
console.error(e); setBackupState((prev) => ({ ...prev, isLoading: false }));
}) }
.finally(() => {
setIsLoading(false);
});
}; };
const checkForm = () => { const checkForm = () => {
@ -132,21 +170,21 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
const url = urlRef.current?.value; const url = urlRef.current?.value;
if (!url) { if (!url) {
Notice.error(t("Webdav url cannot be empty")); Notice.error(t("WebDAV URL Required"));
urlRef.current?.focus(); urlRef.current?.focus();
return; return;
} else if (!isValidUrl(url)) { } else if (!isValidUrl(url)) {
Notice.error(t("Webdav address must be url")); Notice.error(t("Invalid WebDAV URL"));
urlRef.current?.focus(); urlRef.current?.focus();
return; return;
} }
if (!username) { if (!username) {
Notice.error(t("Username cannot be empty")); Notice.error(t("Username Required"));
usernameRef.current?.focus(); usernameRef.current?.focus();
return; return;
} }
if (!password) { if (!password) {
Notice.error(t("Password cannot be empty")); Notice.error(t("Password Required"));
passwordRef.current?.focus(); passwordRef.current?.focus();
return; return;
} }
@ -154,7 +192,7 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
const submit = async (data: IWebDavConfig) => { const submit = async (data: IWebDavConfig) => {
checkForm(); checkForm();
setIsLoading(true); setBackupState((prev) => ({ ...prev, isLoading: true }));
await saveWebdavConfig(data.url, data.username, data.password) await saveWebdavConfig(data.url, data.username, data.password)
.then(() => { .then(() => {
mutateVerge( mutateVerge(
@ -165,36 +203,34 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
}, },
false false
); );
Notice.success(t("Webdav Config Saved Successfully"), 1500); Notice.success(t("WebDAV Config Saved"));
}) })
.catch((e) => { .catch((e) => {
Notice.error(t("Webdav Config Save Failed", { error: e }), 3000); Notice.error(t("WebDAV Config Save Failed", { error: e }), 3000);
}) })
.finally(() => { .finally(() => {
setIsLoading(false); setBackupState((prev) => ({ ...prev, isLoading: false }));
fetchAndSetBackupFiles(); fetchAndSetBackupFiles();
}); });
}; };
const handleClickShowPassword = () => { const handleClickShowPassword = useCallback(() => {
setShowPassword(!showPassword); setShowPassword((prev) => !prev);
}; }, []);
const handleBackup = useLockFn(async () => { const handleBackup = useLockFn(async () => {
checkForm(); try {
setIsLoading(true); checkForm();
await createWebdavBackup() setBackupState((prev) => ({ ...prev, isLoading: true }));
.then(() => { await createWebdavBackup();
Notice.success(t("Backup Successfully"), 1500); Notice.success(t("Backup Created"));
}) await fetchAndSetBackupFiles();
.finally(() => { } catch (error) {
setIsLoading(false); console.error("Backup failed:", error);
fetchAndSetBackupFiles(); Notice.error(t("Backup Failed", { error }));
}) } finally {
.catch((e) => { setBackupState((prev) => ({ ...prev, isLoading: false }));
console.log(e, "backup failed"); }
Notice.error(t("Backup Failed", { error: e }), 3000);
});
}); });
const getAllBackupFiles = async () => { const getAllBackupFiles = async () => {
@ -202,10 +238,8 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
return files return files
.map((file) => { .map((file) => {
const platform = file.filename.split("-")[0]; const platform = file.filename.split("-")[0];
const fileBackupTimeStr = file.filename.match( const fileBackupTimeStr = file.filename.match(FILENAME_PATTERN)!;
/\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/ const backupTime = dayjs(fileBackupTimeStr[0], DATE_FORMAT);
)!;
const backupTime = dayjs(fileBackupTimeStr[0], "YYYY-MM-DD_HH-mm-ss");
const allowApply = OS === platform; const allowApply = OS === platform;
return { return {
...file, ...file,
@ -217,10 +251,17 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
.sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)); .sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1));
}; };
const datasource = backupFiles.slice( const datasource = useMemo(() => {
page * rowsPerPage, return backupState.files.slice(
page * rowsPerPage + rowsPerPage backupState.page * backupState.rowsPerPage,
); backupState.page * backupState.rowsPerPage + backupState.rowsPerPage
);
}, [backupState.files, backupState.page, backupState.rowsPerPage]);
const onFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
handleSubmit(submit)(e);
};
return ( return (
<BaseDialog <BaseDialog
@ -234,207 +275,261 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
> >
<Box sx={{ maxWidth: 800 }}> <Box sx={{ maxWidth: 800 }}>
<BaseLoadingOverlay isLoading={isLoading} /> <BaseLoadingOverlay isLoading={backupState.isLoading} />
<Paper elevation={2} sx={{ padding: 2 }}> <Paper elevation={2} sx={{ padding: 2 }}>
<form onSubmit={handleSubmit(submit)}> <WebDAVConfigForm
<Grid container spacing={2}> onSubmit={onFormSubmit}
<Grid item xs={12} sm={9}> initialValues={{
<Grid container spacing={2}> url: webdav_url,
{/* WebDAV Server Address */} username: webdav_username,
<Grid item xs={12}> password: webdav_password,
<TextField }}
fullWidth urlRef={urlRef}
label="WebDAV Server URL" usernameRef={usernameRef}
variant="outlined" passwordRef={passwordRef}
size="small" showPassword={showPassword}
{...register("url")} onShowPasswordClick={handleClickShowPassword}
autoCorrect="off" webdavChanged={webdavChanged}
autoCapitalize="off" webdavUrl={webdav_url}
spellCheck="false" webdavUsername={webdav_username}
inputRef={urlRef} webdavPassword={webdav_password}
/> handleBackup={handleBackup}
</Grid> register={register}
/>
{/* 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 }} /> <Divider sx={{ marginY: 2 }} />
<TableContainer component={Paper}> <BackupTable
<Table> datasource={datasource}
<TableHead> page={backupState.page}
<TableRow> rowsPerPage={backupState.rowsPerPage}
<TableCell></TableCell> onPageChange={(_, page) => handleChangePage(null, page)}
<TableCell></TableCell> onRowsPerPageChange={handleChangeRowsPerPage}
<TableCell align="right"></TableCell> totalCount={backupState.files.length}
</TableRow> />
</TableHead>
<TableBody>
{datasource.length > 0 ? (
datasource?.map((file, index) => (
<TableRow key={index}>
<TableCell component="th" scope="row">
{file.platform === "windows" ? (
<WindowsIcon className="h-full w-full" />
) : file.platform === "linux" ? (
<LinuxIcon className="h-full w-full" />
) : (
<MacIcon className="h-full w-full" />
)}
{file.filename}
</TableCell>
<TableCell align="center">
{file.backup_time.fromNow()}
</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={3} align="center">
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: 150,
}}
>
<Typography
variant="body1"
color="textSecondary"
align="center"
>
</Typography>
</Box>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[]}
component="div"
count={backupFiles.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
labelRowsPerPage=""
/>
</TableContainer>
</Paper> </Paper>
</Box> </Box>
</BaseDialog> </BaseDialog>
); );
}); });
const BackupTable = memo(
({
datasource,
page,
rowsPerPage,
onPageChange,
onRowsPerPageChange,
totalCount,
}: BackupTableProps) => {
const { t } = useTranslation();
return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Filename")}</TableCell>
<TableCell>{t("Time")}</TableCell>
<TableCell align="right">{t("Actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{datasource.length > 0 ? (
datasource?.map((file, index) => (
<TableRow key={index}>
<TableCell component="th" scope="row">
{file.platform === "windows" ? (
<WindowsIcon className="h-full w-full" />
) : file.platform === "linux" ? (
<LinuxIcon className="h-full w-full" />
) : (
<MacIcon className="h-full w-full" />
)}
{file.filename}
</TableCell>
<TableCell align="center">
{file.backup_time.fromNow()}
</TableCell>
<TableCell align="right">
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<IconButton
color="secondary"
aria-label={t("Delete")}
size="small"
title={t("Delete Backup")}
>
<DeleteIcon />
</IconButton>
<Divider
orientation="vertical"
flexItem
sx={{ mx: 1, height: 24 }}
/>
<IconButton
color="primary"
aria-label={t("Restore")}
size="small"
title={t("Restore Backup")}
>
<RestoreIcon />
</IconButton>
</Box>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={3} align="center">
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: 150,
}}
>
<Typography
variant="body1"
color="textSecondary"
align="center"
>
{t("No Backups")}
</Typography>
</Box>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
rowsPerPageOptions={[]}
component="div"
count={totalCount}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={onPageChange}
onRowsPerPageChange={onRowsPerPageChange}
labelRowsPerPage={t("Rows per page")}
/>
</TableContainer>
);
}
);
const WebDAVConfigForm = memo(
({
onSubmit,
initialValues,
urlRef,
usernameRef,
passwordRef,
showPassword,
onShowPasswordClick,
webdavChanged,
webdavUrl,
webdavUsername,
webdavPassword,
handleBackup,
register,
}: WebDAVConfigFormProps) => {
const { t } = useTranslation();
return (
<form onSubmit={onSubmit}>
<Grid container spacing={2}>
<Grid item xs={12} sm={9}>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
fullWidth
label={t("WebDAV Server URL")}
variant="outlined"
size="small"
{...register("url")}
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={urlRef}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Username")}
variant="outlined"
size="small"
{...register("username")}
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
inputRef={usernameRef}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("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={onShowPasswordClick} 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 ||
webdavUrl === null ||
webdavUsername == null ||
webdavPassword == null ? (
<Button
variant="contained"
color="primary"
sx={{ height: "100%" }}
type="submit"
>
{t("Save")}
</Button>
) : (
<Button
variant="contained"
color="success"
sx={{ height: "100%" }}
onClick={handleBackup}
type="button"
>
{t("Backup")}
</Button>
)}
</Stack>
</Grid>
</Grid>
</form>
);
}
);
export function LinuxIcon(props: SVGProps<SVGSVGElement>) { export function LinuxIcon(props: SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg

View File

@ -371,5 +371,24 @@
"Switched to _clash Core": "Switched to {{core}} Core", "Switched to _clash Core": "Switched to {{core}} Core",
"GeoData Updated": "GeoData Updated", "GeoData Updated": "GeoData Updated",
"Currently on the Latest Version": "Currently on the Latest Version", "Currently on the Latest Version": "Currently on the Latest Version",
"Import Subscription Successful": "Import subscription successful" "Import Subscription Successful": "Import subscription successful",
"WebDAV Server URL": "WebDAV Server URL",
"Username": "Username",
"Password": "Password",
"Backup": "Backup",
"Filename": "Filename",
"Actions": "Actions",
"Restore": "Restore",
"No Backups": "No backups available",
"WebDAV URL Required": "WebDAV URL cannot be empty",
"Invalid WebDAV URL": "Invalid WebDAV URL format",
"Username Required": "Username cannot be empty",
"Password Required": "Password cannot be empty",
"Failed to Fetch Backups": "Failed to fetch backup files",
"WebDAV Config Saved": "WebDAV configuration saved successfully",
"WebDAV Config Save Failed": "Failed to save WebDAV configuration",
"Backup Created": "Backup created successfully",
"Backup Failed": "Failed to create backup",
"Delete Backup": "Delete Backup",
"Restore Backup": "Restore Backup"
} }

View File

@ -214,7 +214,7 @@
"Install": "نصب", "Install": "نصب",
"Uninstall": "حذف نصب", "Uninstall": "حذف نصب",
"Disable Service Mode": "غیرفعال کردن حالت سرویس", "Disable Service Mode": "غیرفعال کردن حالت سرویس",
"System Proxy": "پراکسی سیستم", "System Proxy": "پراکسی س<EFBFBD><EFBFBD>ستم",
"System Proxy Info": "به امکانات تنظیم پروکسی سیستم عامل دسترسی پیدا کنید. اگر فعال‌سازی ناموفق بود، پروکسی سیستم عامل را به‌صورت دستی تغییر دهید", "System Proxy Info": "به امکانات تنظیم پروکسی سیستم عامل دسترسی پیدا کنید. اگر فعال‌سازی ناموفق بود، پروکسی سیستم عامل را به‌صورت دستی تغییر دهید",
"System Proxy Setting": "تنظیمات پراکسی سیستم", "System Proxy Setting": "تنظیمات پراکسی سیستم",
"Current System Proxy": "پراکسی سیستم فعلی", "Current System Proxy": "پراکسی سیستم فعلی",
@ -324,7 +324,7 @@
"Default Latency Test Info": "فقط برای تست درخواست‌های کلاینت HTTP استفاده می‌شود و بر فایل پیکربندی تأثیری نخواهد داشت", "Default Latency Test Info": "فقط برای تست درخواست‌های کلاینت HTTP استفاده می‌شود و بر فایل پیکربندی تأثیری نخواهد داشت",
"Default Latency Timeout": "زمان انتظار تأخیر پیش‌فرض", "Default Latency Timeout": "زمان انتظار تأخیر پیش‌فرض",
"Hotkey Setting": "تنظیمات کلیدهای میانبر", "Hotkey Setting": "تنظیمات کلیدهای میانبر",
"open_or_close_dashboard": "باز/بستن داشبورد", "open_or_close_dashboard": "باز/بستن داشبرد",
"clash_mode_rule": "حالت قانون", "clash_mode_rule": "حالت قانون",
"clash_mode_global": "حالت جهانی", "clash_mode_global": "حالت جهانی",
"clash_mode_direct": "حالت مستقیم", "clash_mode_direct": "حالت مستقیم",
@ -369,5 +369,24 @@
"Switched to _clash Core": "تغییر به هسته {{core}}", "Switched to _clash Core": "تغییر به هسته {{core}}",
"GeoData Updated": "GeoData به‌روزرسانی شد", "GeoData Updated": "GeoData به‌روزرسانی شد",
"Currently on the Latest Version": "در حال حاضر در آخرین نسخه", "Currently on the Latest Version": "در حال حاضر در آخرین نسخه",
"Import Subscription Successfully": "عضویت با موفقیت وارد شد" "Import Subscription Successfully": "عضویت با موفقیت وارد شد",
"WebDAV Server URL": "آدرس سرور WebDAV",
"Username": "نام کاربری",
"Password": "رمز عبور",
"Backup": "پشتیبان‌گیری",
"Filename": "نام فایل",
"Actions": "عملیات",
"Restore": "بازیابی",
"No Backups": "هیچ پشتیبانی موجود نیست",
"WebDAV URL Required": "آدرس WebDAV نمی‌تواند خالی باشد",
"Invalid WebDAV URL": "فرمت آدرس WebDAV نامعتبر است",
"Username Required": "نام کاربری نمی‌تواند خالی باشد",
"Password Required": "رمز عبور نمی‌تواند خالی باشد",
"Failed to Fetch Backups": "دریافت فایل‌های پشتیبان ناموفق بود",
"WebDAV Config Saved": "پیکربندی WebDAV با موفقیت ذخیره شد",
"WebDAV Config Save Failed": "ذخیره پیکربندی WebDAV ناموفق بود",
"Backup Created": "پشتیبان با موفقیت ایجاد شد",
"Backup Failed": "ایجاد پشتیبان ناموفق بود",
"Delete Backup": "حذف پشتیبان",
"Restore Backup": "بازیابی پشتیبان"
} }

View File

@ -203,7 +203,7 @@
"DNS Hijack": "DNS-перехват", "DNS Hijack": "DNS-перехват",
"MTU": "Максимальная единица передачи", "MTU": "Максимальная единица передачи",
"Service Mode": "Режим сервиса", "Service Mode": "Режим сервиса",
"Service Mode Info": "Установите сервисный режим перед включением режима TUN. Процесс ядра, запущенный службой, может получить разрешение на установку виртуальной сетевой карты (режим TUN).", "Service Mode Info": "Установите серви<EFBFBD><EFBFBD>ный режим перед включением режима TUN. Процесс ядра, запущенный службой, может получить разрешение на установку витуальной сетевой карты (режим TUN).",
"Current State": "Текущее состояние", "Current State": "Текущее состояние",
"pending": "Ожидающий", "pending": "Ожидающий",
"installed": "Установленный", "installed": "Установленный",
@ -239,7 +239,7 @@
"Manual": "Документация", "Manual": "Документация",
"Github Repo": "GitHub репозиторий", "Github Repo": "GitHub репозиторий",
"Clash Setting": "Настройки Clash", "Clash Setting": "Настройки Clash",
"Allow Lan": "Разрешить локальную сеть", "Allow Lan": "Разрешить локльную сеть",
"Network Interface": "Сетевой интерфейс", "Network Interface": "Сетевой интерфейс",
"Ip Address": "IP адрес", "Ip Address": "IP адрес",
"Mac Address": "MAC адрес", "Mac Address": "MAC адрес",
@ -314,7 +314,7 @@
"Auto Close Connections Info": "Завершить установленные соединения при изменении выбора группы прокси или режима прокси", "Auto Close Connections Info": "Завершить установленные соединения при изменении выбора группы прокси или режима прокси",
"Auto Check Update": "Автоматическая проверка обновлений", "Auto Check Update": "Автоматическая проверка обновлений",
"Enable Builtin Enhanced": "Включить встроенные улучшения", "Enable Builtin Enhanced": "Включить встроенные улучшения",
"Enable Builtin Enhanced Info": "Обработка совместимости для файла конфигурации", "Enable Builtin Enhanced Info": "Обработк совместимости для файла конфигурации",
"Proxy Layout Columns": "Количество столбцов в макете прокси", "Proxy Layout Columns": "Количество столбцов в макете прокси",
"Auto Columns": "Авто колонки", "Auto Columns": "Авто колонки",
"Auto Log Clean": "Автоматическая очистка журналов", "Auto Log Clean": "Автоматическая очистка журналов",
@ -369,5 +369,11 @@
"Switched to _clash Core": "Переключено на ядра {{core}}", "Switched to _clash Core": "Переключено на ядра {{core}}",
"GeoData Updated": "GeoData Обновлена", "GeoData Updated": "GeoData Обновлена",
"Currently on the Latest Version": "В настоящее время используется последняя версия", "Currently on the Latest Version": "В настоящее время используется последняя версия",
"Import subscription successful": "Импорт подписки успешно" "Import subscription successful": "Импорт подписки успешно",
"WebDAV Server URL": "URL-адрес сервера WebDAV",
"Username": "Имя пользователя",
"Password": "Пароль",
"Backup": "Резервное копирование",
"Delete Backup": "Удалить резервную копию",
"Restore Backup": "Восстановить резервную копию"
} }

View File

@ -95,7 +95,7 @@
"PROCESS-PATH-REGEX": "正则匹配完整进程路径", "PROCESS-PATH-REGEX": "正则匹配完整进程路径",
"NETWORK": "匹配传输协议(tcp/udp)", "NETWORK": "匹配传输协议(tcp/udp)",
"UID": "匹配Linux USER ID", "UID": "匹配Linux USER ID",
"IN-TYPE": "匹配站类型", "IN-TYPE": "匹配站类型",
"IN-USER": "匹配入站用户名", "IN-USER": "匹配入站用户名",
"IN-NAME": "匹配入站名称", "IN-NAME": "匹配入站名称",
"SUB-RULE": "子规则", "SUB-RULE": "子规则",
@ -128,7 +128,7 @@
"Routing Mark": "路由标记", "Routing Mark": "路由标记",
"Include All": "引入所有出站代理、代理集合", "Include All": "引入所有出站代理、代理集合",
"Include All Providers": "引入所有代理集合", "Include All Providers": "引入所有代理集合",
"Include All Proxies": "入所有出站代理", "Include All Proxies": "入所有出站代理",
"Exclude Filter": "排除节点", "Exclude Filter": "排除节点",
"Exclude Type": "排除节点类型", "Exclude Type": "排除节点类型",
"Disable UDP": "禁用UDP", "Disable UDP": "禁用UDP",
@ -161,7 +161,7 @@
"Script Console": "脚本控制台输出", "Script Console": "脚本控制台输出",
"To Top": "移到最前", "To Top": "移到最前",
"To End": "移到末尾", "To End": "移到末尾",
"Connections": "接", "Connections": "接",
"Table View": "表格视图", "Table View": "表格视图",
"List View": "列表视图", "List View": "列表视图",
"Close All": "关闭全部", "Close All": "关闭全部",
@ -197,7 +197,7 @@
"Reset to Default": "重置为默认值", "Reset to Default": "重置为默认值",
"Tun Mode Info": "Tun(虚拟网卡)模式接管系统所有流量,启用时无须打开系统代理", "Tun Mode Info": "Tun(虚拟网卡)模式接管系统所有流量,启用时无须打开系统代理",
"Stack": "Tun 模式堆栈", "Stack": "Tun 模式堆栈",
"System and Mixed Can Only be Used in Service Mode": "System 和 Mixed 只能在服务模式下使用", "System and Mixed Can Only be Used in Service Mode": "System 和 Mixed <EFBFBD><EFBFBD><EFBFBD>能在服务模式使用",
"Device": "Tun 网卡名称", "Device": "Tun 网卡名称",
"Auto Route": "自动设置全局路由", "Auto Route": "自动设置全局路由",
"Strict Route": "严格路由", "Strict Route": "严格路由",
@ -361,7 +361,7 @@
"Service Uninstalled Successfully": "已成功卸载服务", "Service Uninstalled Successfully": "已成功卸载服务",
"Proxy Daemon Duration Cannot be Less than 1 Second": "代理守护间隔时间不得低于1秒", "Proxy Daemon Duration Cannot be Less than 1 Second": "代理守护间隔时间不得低于1秒",
"Invalid Bypass Format": "无效的代理绕过格式", "Invalid Bypass Format": "无效的代理绕过格式",
"Clash Port Modified": "Clash 口已修改", "Clash Port Modified": "Clash <EFBFBD><EFBFBD>口已修改",
"Port Conflict": "端口冲突", "Port Conflict": "端口冲突",
"Restart Application to Apply Modifications": "重启Verge以应用修改", "Restart Application to Apply Modifications": "重启Verge以应用修改",
"External Controller Address Modified": "外部控制器监听地址已修改", "External Controller Address Modified": "外部控制器监听地址已修改",
@ -371,5 +371,25 @@
"Switched to _clash Core": "已切换至 {{core}} 内核", "Switched to _clash Core": "已切换至 {{core}} 内核",
"GeoData Updated": "已更新 GeoData", "GeoData Updated": "已更新 GeoData",
"Currently on the Latest Version": "当前已是最新版本", "Currently on the Latest Version": "当前已是最新版本",
"Import Subscription Successful": "导入订阅成功" "Import Subscription Successful": "导入订阅成功",
"WebDAV Server URL": "WebDAV 服务器地址",
"Username": "用户名",
"Password": "密码",
"Backup": "备份",
"Filename": "文件名称",
"Actions": "操作",
"Restore": "恢复",
"No Backups": "暂无备份",
"WebDAV URL Required": "WebDAV 地址不能为空",
"Invalid WebDAV URL": "无效的 WebDAV 地址格式",
"Username Required": "用户名不能为空",
"Password Required": "密码不能为空",
"Failed to Fetch Backups": "获取备份文件失败",
"WebDAV Config Saved": "WebDAV 配置保存成功",
"WebDAV Config Save Failed": "WebDAV 配置保存失败",
"Backup Created": "备份创建成功",
"Backup Failed": "备份创建失败",
"Delete Backup": "删除备份",
"Restore Backup": "恢复备份"
} }