chore(deps): update npm dependencies (#4939)
* chore(deps): update npm dependencies * Refactor components to use function syntax instead of forwardRef for better type handling and clarity. Updated imports and adjusted prop types accordingly across multiple viewer components including TrafficGraph, ProfileViewer, BackupViewer, ClashCoreViewer, ControllerViewer, DnsViewer, LiteModeViewer, NetworkInterfaceViewer, ThemeViewer, TunViewer, UpdateViewer, and WebUIViewer. --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
8b3bc18ea8
commit
600b0b52f4
@@ -9,14 +9,9 @@ import {
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import type { Ref } from "react";
|
||||
import { useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { BaseDialog, Switch } from "@/components/base";
|
||||
@@ -38,304 +33,279 @@ export interface ProfileViewerRef {
|
||||
|
||||
// create or edit the profile
|
||||
// remote / local
|
||||
export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
||||
(props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openType, setOpenType] = useState<"new" | "edit">("new");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { profiles } = useProfiles();
|
||||
type ProfileViewerProps = Props & { ref?: Ref<ProfileViewerRef> };
|
||||
|
||||
// file input
|
||||
const fileDataRef = useRef<string | null>(null);
|
||||
export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openType, setOpenType] = useState<"new" | "edit">("new");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { profiles } = useProfiles();
|
||||
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
register: _register,
|
||||
...formIns
|
||||
} = useForm<IProfileItem>({
|
||||
defaultValues: {
|
||||
type: "remote",
|
||||
name: "",
|
||||
desc: "",
|
||||
url: "",
|
||||
option: {
|
||||
with_proxy: false,
|
||||
self_proxy: false,
|
||||
},
|
||||
// file input
|
||||
const fileDataRef = useRef<string | null>(null);
|
||||
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
register: _register,
|
||||
...formIns
|
||||
} = useForm<IProfileItem>({
|
||||
defaultValues: {
|
||||
type: "remote",
|
||||
name: "",
|
||||
desc: "",
|
||||
url: "",
|
||||
option: {
|
||||
with_proxy: false,
|
||||
self_proxy: false,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
create: () => {
|
||||
setOpenType("new");
|
||||
setOpen(true);
|
||||
},
|
||||
edit: (item) => {
|
||||
if (item) {
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
formIns.setValue(key as any, value);
|
||||
});
|
||||
}
|
||||
setOpenType("edit");
|
||||
setOpen(true);
|
||||
},
|
||||
}));
|
||||
useImperativeHandle(ref, () => ({
|
||||
create: () => {
|
||||
setOpenType("new");
|
||||
setOpen(true);
|
||||
},
|
||||
edit: (item: IProfileItem) => {
|
||||
if (item) {
|
||||
Object.entries(item).forEach(([key, value]) => {
|
||||
formIns.setValue(key as any, value);
|
||||
});
|
||||
}
|
||||
setOpenType("edit");
|
||||
setOpen(true);
|
||||
},
|
||||
}));
|
||||
|
||||
const selfProxy = watch("option.self_proxy");
|
||||
const withProxy = watch("option.with_proxy");
|
||||
const selfProxy = watch("option.self_proxy");
|
||||
const withProxy = watch("option.with_proxy");
|
||||
|
||||
useEffect(() => {
|
||||
if (selfProxy) formIns.setValue("option.with_proxy", false);
|
||||
}, [selfProxy]);
|
||||
useEffect(() => {
|
||||
if (selfProxy) formIns.setValue("option.with_proxy", false);
|
||||
}, [selfProxy]);
|
||||
|
||||
useEffect(() => {
|
||||
if (withProxy) formIns.setValue("option.self_proxy", false);
|
||||
}, [withProxy]);
|
||||
useEffect(() => {
|
||||
if (withProxy) formIns.setValue("option.self_proxy", false);
|
||||
}, [withProxy]);
|
||||
|
||||
const handleOk = useLockFn(
|
||||
formIns.handleSubmit(async (form) => {
|
||||
if (form.option?.timeout_seconds) {
|
||||
form.option.timeout_seconds = +form.option.timeout_seconds;
|
||||
const handleOk = useLockFn(
|
||||
formIns.handleSubmit(async (form) => {
|
||||
if (form.option?.timeout_seconds) {
|
||||
form.option.timeout_seconds = +form.option.timeout_seconds;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// 基本验证
|
||||
if (!form.type) throw new Error("`Type` should not be null");
|
||||
if (form.type === "remote" && !form.url) {
|
||||
throw new Error("The URL should not be null");
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// 基本验证
|
||||
if (!form.type) throw new Error("`Type` should not be null");
|
||||
if (form.type === "remote" && !form.url) {
|
||||
throw new Error("The URL should not be null");
|
||||
}
|
||||
// 处理表单数据
|
||||
if (form.option?.update_interval) {
|
||||
form.option.update_interval = +form.option.update_interval;
|
||||
} else {
|
||||
delete form.option?.update_interval;
|
||||
}
|
||||
if (form.option?.user_agent === "") {
|
||||
delete form.option.user_agent;
|
||||
}
|
||||
|
||||
// 处理表单数据
|
||||
if (form.option?.update_interval) {
|
||||
form.option.update_interval = +form.option.update_interval;
|
||||
const name = form.name || `${form.type} file`;
|
||||
const item = { ...form, name };
|
||||
const isRemote = form.type === "remote";
|
||||
const isUpdate = openType === "edit";
|
||||
|
||||
// 判断是否是当前激活的配置
|
||||
const isActivating = isUpdate && form.uid === (profiles?.current ?? "");
|
||||
|
||||
// 保存原始代理设置以便回退成功后恢复
|
||||
const originalOptions = {
|
||||
with_proxy: form.option?.with_proxy,
|
||||
self_proxy: form.option?.self_proxy,
|
||||
};
|
||||
|
||||
// 执行创建或更新操作,本地配置不需要回退机制
|
||||
if (!isRemote) {
|
||||
if (openType === "new") {
|
||||
await createProfile(item, fileDataRef.current);
|
||||
} else {
|
||||
delete form.option?.update_interval;
|
||||
if (!form.uid) throw new Error("UID not found");
|
||||
await patchProfile(form.uid, item);
|
||||
}
|
||||
if (form.option?.user_agent === "") {
|
||||
delete form.option.user_agent;
|
||||
}
|
||||
|
||||
const name = form.name || `${form.type} file`;
|
||||
const item = { ...form, name };
|
||||
const isRemote = form.type === "remote";
|
||||
const isUpdate = openType === "edit";
|
||||
|
||||
// 判断是否是当前激活的配置
|
||||
const isActivating =
|
||||
isUpdate && form.uid === (profiles?.current ?? "");
|
||||
|
||||
// 保存原始代理设置以便回退成功后恢复
|
||||
const originalOptions = {
|
||||
with_proxy: form.option?.with_proxy,
|
||||
self_proxy: form.option?.self_proxy,
|
||||
};
|
||||
|
||||
// 执行创建或更新操作,本地配置不需要回退机制
|
||||
if (!isRemote) {
|
||||
} else {
|
||||
// 远程配置使用回退机制
|
||||
try {
|
||||
// 尝试正常操作
|
||||
if (openType === "new") {
|
||||
await createProfile(item, fileDataRef.current);
|
||||
} else {
|
||||
if (!form.uid) throw new Error("UID not found");
|
||||
await patchProfile(form.uid, item);
|
||||
}
|
||||
} else {
|
||||
// 远程配置使用回退机制
|
||||
try {
|
||||
// 尝试正常操作
|
||||
if (openType === "new") {
|
||||
await createProfile(item, fileDataRef.current);
|
||||
} else {
|
||||
if (!form.uid) throw new Error("UID not found");
|
||||
await patchProfile(form.uid, item);
|
||||
}
|
||||
} catch {
|
||||
// 首次创建/更新失败,尝试使用自身代理
|
||||
showNotice(
|
||||
"info",
|
||||
t("Profile creation failed, retrying with Clash proxy..."),
|
||||
);
|
||||
} catch {
|
||||
// 首次创建/更新失败,尝试使用自身代理
|
||||
showNotice(
|
||||
"info",
|
||||
t("Profile creation failed, retrying with Clash proxy..."),
|
||||
);
|
||||
|
||||
// 使用自身代理的配置
|
||||
const retryItem = {
|
||||
...item,
|
||||
option: {
|
||||
...item.option,
|
||||
with_proxy: false,
|
||||
self_proxy: true,
|
||||
},
|
||||
};
|
||||
// 使用自身代理的配置
|
||||
const retryItem = {
|
||||
...item,
|
||||
option: {
|
||||
...item.option,
|
||||
with_proxy: false,
|
||||
self_proxy: true,
|
||||
},
|
||||
};
|
||||
|
||||
// 使用自身代理再次尝试
|
||||
if (openType === "new") {
|
||||
await createProfile(retryItem, fileDataRef.current);
|
||||
} else {
|
||||
if (!form.uid) throw new Error("UID not found");
|
||||
await patchProfile(form.uid, retryItem);
|
||||
// 使用自身代理再次尝试
|
||||
if (openType === "new") {
|
||||
await createProfile(retryItem, fileDataRef.current);
|
||||
} else {
|
||||
if (!form.uid) throw new Error("UID not found");
|
||||
await patchProfile(form.uid, retryItem);
|
||||
|
||||
// 编辑模式下恢复原始代理设置
|
||||
await patchProfile(form.uid, { option: originalOptions });
|
||||
}
|
||||
|
||||
showNotice(
|
||||
"success",
|
||||
t("Profile creation succeeded with Clash proxy"),
|
||||
);
|
||||
// 编辑模式下恢复原始代理设置
|
||||
await patchProfile(form.uid, { option: originalOptions });
|
||||
}
|
||||
|
||||
showNotice(
|
||||
"success",
|
||||
t("Profile creation succeeded with Clash proxy"),
|
||||
);
|
||||
}
|
||||
|
||||
// 成功后的操作
|
||||
setOpen(false);
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
fileDataRef.current = null;
|
||||
|
||||
// 优化:UI先关闭,异步通知父组件
|
||||
setTimeout(() => {
|
||||
props.onChange(isActivating);
|
||||
}, 0);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
try {
|
||||
// 成功后的操作
|
||||
setOpen(false);
|
||||
fileDataRef.current = null;
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
} catch (e) {
|
||||
console.warn("[ProfileViewer] handleClose error:", e);
|
||||
fileDataRef.current = null;
|
||||
|
||||
// 优化:UI先关闭,异步通知父组件
|
||||
setTimeout(() => {
|
||||
onChange(isActivating);
|
||||
}, 0);
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const text = {
|
||||
fullWidth: true,
|
||||
size: "small",
|
||||
margin: "normal",
|
||||
variant: "outlined",
|
||||
autoComplete: "off",
|
||||
autoCorrect: "off",
|
||||
} as const;
|
||||
const handleClose = () => {
|
||||
try {
|
||||
setOpen(false);
|
||||
fileDataRef.current = null;
|
||||
setTimeout(() => formIns.reset(), 500);
|
||||
} catch (e) {
|
||||
console.warn("[ProfileViewer] handleClose error:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const formType = watch("type");
|
||||
const isRemote = formType === "remote";
|
||||
const isLocal = formType === "local";
|
||||
const text = {
|
||||
fullWidth: true,
|
||||
size: "small",
|
||||
margin: "normal",
|
||||
variant: "outlined",
|
||||
autoComplete: "off",
|
||||
autoCorrect: "off",
|
||||
} as const;
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={openType === "new" ? t("Create Profile") : t("Edit Profile")}
|
||||
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
onOk={handleOk}
|
||||
loading={loading}
|
||||
>
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl size="small" fullWidth sx={{ mt: 1, mb: 1 }}>
|
||||
<InputLabel>{t("Type")}</InputLabel>
|
||||
<Select {...field} autoFocus label={t("Type")}>
|
||||
<MenuItem value="remote">Remote</MenuItem>
|
||||
<MenuItem value="local">Local</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
const formType = watch("type");
|
||||
const isRemote = formType === "remote";
|
||||
const isLocal = formType === "local";
|
||||
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField {...text} {...field} label={t("Name")} />
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="desc"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField {...text} {...field} label={t("Descriptions")} />
|
||||
)}
|
||||
/>
|
||||
|
||||
{isRemote && (
|
||||
<>
|
||||
<Controller
|
||||
name="url"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
multiline
|
||||
label={t("Subscription URL")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="option.user_agent"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
placeholder={`clash-verge/v${version}`}
|
||||
label="User Agent"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="option.timeout_seconds"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
type="number"
|
||||
placeholder="60"
|
||||
label={t("HTTP Request Timeout")}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{t("seconds")}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={openType === "new" ? t("Create Profile") : t("Edit Profile")}
|
||||
contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
|
||||
okBtn={t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
onOk={handleOk}
|
||||
loading={loading}
|
||||
>
|
||||
<Controller
|
||||
name="type"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl size="small" fullWidth sx={{ mt: 1, mb: 1 }}>
|
||||
<InputLabel>{t("Type")}</InputLabel>
|
||||
<Select {...field} autoFocus label={t("Type")}>
|
||||
<MenuItem value="remote">Remote</MenuItem>
|
||||
<MenuItem value="local">Local</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{(isRemote || isLocal) && (
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField {...text} {...field} label={t("Name")} />
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="desc"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField {...text} {...field} label={t("Descriptions")} />
|
||||
)}
|
||||
/>
|
||||
|
||||
{isRemote && (
|
||||
<>
|
||||
<Controller
|
||||
name="option.update_interval"
|
||||
name="url"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
multiline
|
||||
label={t("Subscription URL")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="option.user_agent"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
placeholder={`clash-verge/v${version}`}
|
||||
label="User Agent"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="option.timeout_seconds"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
type="number"
|
||||
label={t("Update Interval")}
|
||||
placeholder="60"
|
||||
label={t("HTTP Request Timeout")}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{t("mins")}
|
||||
{t("seconds")}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
@@ -343,57 +313,79 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isLocal && openType === "new" && (
|
||||
<FileInput
|
||||
onChange={(file, val) => {
|
||||
formIns.setValue("name", formIns.getValues("name") || file.name);
|
||||
fileDataRef.current = val;
|
||||
}}
|
||||
{(isRemote || isLocal) && (
|
||||
<Controller
|
||||
name="option.update_interval"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...text}
|
||||
{...field}
|
||||
type="number"
|
||||
label={t("Update Interval")}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{t("mins")}</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isLocal && openType === "new" && (
|
||||
<FileInput
|
||||
onChange={(file, val) => {
|
||||
formIns.setValue("name", formIns.getValues("name") || file.name);
|
||||
fileDataRef.current = val;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isRemote && (
|
||||
<>
|
||||
<Controller
|
||||
name="option.with_proxy"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Use System Proxy")}</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isRemote && (
|
||||
<>
|
||||
<Controller
|
||||
name="option.with_proxy"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Use System Proxy")}</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="option.self_proxy"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Use Clash Proxy")}</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="option.self_proxy"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Use Clash Proxy")}</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="option.danger_accept_invalid_certs"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Accept Invalid Certs (Danger)")}</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
);
|
||||
<Controller
|
||||
name="option.danger_accept_invalid_certs"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledBox>
|
||||
<InputLabel>{t("Accept Invalid Certs (Danger)")}</InputLabel>
|
||||
<Switch checked={field.value} {...field} color="primary" />
|
||||
</StyledBox>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledBox = styled(Box)(() => ({
|
||||
margin: "8px 0 8px 8px",
|
||||
|
||||
Reference in New Issue
Block a user