profile-viewer.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import {
  2. forwardRef,
  3. useEffect,
  4. useImperativeHandle,
  5. useRef,
  6. useState,
  7. } from "react";
  8. import { useLockFn } from "ahooks";
  9. import { useTranslation } from "react-i18next";
  10. import { useForm, Controller } from "react-hook-form";
  11. import {
  12. Box,
  13. FormControl,
  14. InputAdornment,
  15. InputLabel,
  16. MenuItem,
  17. Select,
  18. Switch,
  19. styled,
  20. TextField,
  21. } from "@mui/material";
  22. import { createProfile, patchProfile } from "@/services/cmds";
  23. import { BaseDialog, Notice } from "@/components/base";
  24. import { version } from "@root/package.json";
  25. import { FileInput } from "./file-input";
  26. interface Props {
  27. onChange: () => void;
  28. }
  29. export interface ProfileViewerRef {
  30. create: () => void;
  31. edit: (item: IProfileItem) => void;
  32. }
  33. // create or edit the profile
  34. // remote / local / merge / script
  35. export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
  36. (props, ref) => {
  37. const { t } = useTranslation();
  38. const [open, setOpen] = useState(false);
  39. const [openType, setOpenType] = useState<"new" | "edit">("new");
  40. const [loading, setLoading] = useState(false);
  41. // file input
  42. const fileDataRef = useRef<string | null>(null);
  43. const { control, watch, register, ...formIns } = useForm<IProfileItem>({
  44. defaultValues: {
  45. type: "remote",
  46. name: "",
  47. desc: "",
  48. url: "",
  49. option: {
  50. // user_agent: "",
  51. with_proxy: false,
  52. self_proxy: false,
  53. },
  54. },
  55. });
  56. useImperativeHandle(ref, () => ({
  57. create: () => {
  58. setOpenType("new");
  59. setOpen(true);
  60. },
  61. edit: (item) => {
  62. if (item) {
  63. Object.entries(item).forEach(([key, value]) => {
  64. formIns.setValue(key as any, value);
  65. });
  66. }
  67. setOpenType("edit");
  68. setOpen(true);
  69. },
  70. }));
  71. const selfProxy = watch("option.self_proxy");
  72. const withProxy = watch("option.with_proxy");
  73. useEffect(() => {
  74. if (selfProxy) formIns.setValue("option.with_proxy", false);
  75. }, [selfProxy]);
  76. useEffect(() => {
  77. if (withProxy) formIns.setValue("option.self_proxy", false);
  78. }, [withProxy]);
  79. const handleOk = useLockFn(
  80. formIns.handleSubmit(async (form) => {
  81. setLoading(true);
  82. try {
  83. if (!form.type) throw new Error("`Type` should not be null");
  84. if (form.type === "remote" && !form.url) {
  85. throw new Error("The URL should not be null");
  86. }
  87. if (form.type !== "remote" && form.type !== "local") {
  88. delete form.option;
  89. }
  90. if (form.option?.update_interval) {
  91. form.option.update_interval = +form.option.update_interval;
  92. }
  93. const name = form.name || `${form.type} file`;
  94. const item = { ...form, name };
  95. // 创建
  96. if (openType === "new") {
  97. await createProfile(item, fileDataRef.current);
  98. }
  99. // 编辑
  100. else {
  101. if (!form.uid) throw new Error("UID not found");
  102. await patchProfile(form.uid, item);
  103. }
  104. setOpen(false);
  105. setLoading(false);
  106. setTimeout(() => formIns.reset(), 500);
  107. fileDataRef.current = null;
  108. props.onChange();
  109. } catch (err: any) {
  110. Notice.error(err.message || err.toString());
  111. setLoading(false);
  112. }
  113. })
  114. );
  115. const handleClose = () => {
  116. setOpen(false);
  117. fileDataRef.current = null;
  118. setTimeout(() => formIns.reset(), 500);
  119. };
  120. const text = {
  121. fullWidth: true,
  122. size: "small",
  123. margin: "normal",
  124. variant: "outlined",
  125. autoComplete: "off",
  126. autoCorrect: "off",
  127. } as const;
  128. const formType = watch("type");
  129. const isRemote = formType === "remote";
  130. const isLocal = formType === "local";
  131. return (
  132. <BaseDialog
  133. open={open}
  134. title={openType === "new" ? t("Create Profile") : t("Edit Profile")}
  135. contentSx={{ width: 375, pb: 0, maxHeight: "80%" }}
  136. okBtn={t("Save")}
  137. cancelBtn={t("Cancel")}
  138. onClose={handleClose}
  139. onCancel={handleClose}
  140. onOk={handleOk}
  141. loading={loading}
  142. >
  143. <Controller
  144. name="type"
  145. control={control}
  146. render={({ field }) => (
  147. <FormControl size="small" fullWidth sx={{ mt: 1, mb: 1 }}>
  148. <InputLabel>{t("Type")}</InputLabel>
  149. <Select {...field} autoFocus label={t("Type")}>
  150. <MenuItem value="remote">Remote</MenuItem>
  151. <MenuItem value="local">Local</MenuItem>
  152. <MenuItem value="script">Script</MenuItem>
  153. <MenuItem value="merge">Merge</MenuItem>
  154. </Select>
  155. </FormControl>
  156. )}
  157. />
  158. <Controller
  159. name="name"
  160. control={control}
  161. render={({ field }) => (
  162. <TextField {...text} {...field} label={t("Name")} />
  163. )}
  164. />
  165. <Controller
  166. name="desc"
  167. control={control}
  168. render={({ field }) => (
  169. <TextField {...text} {...field} label={t("Descriptions")} />
  170. )}
  171. />
  172. {isRemote && (
  173. <>
  174. <Controller
  175. name="url"
  176. control={control}
  177. render={({ field }) => (
  178. <TextField
  179. {...text}
  180. {...field}
  181. multiline
  182. label={t("Subscription URL")}
  183. />
  184. )}
  185. />
  186. <Controller
  187. name="option.user_agent"
  188. control={control}
  189. render={({ field }) => (
  190. <TextField
  191. {...text}
  192. {...field}
  193. placeholder={`clash-verge/v${version}`}
  194. label="User Agent"
  195. />
  196. )}
  197. />
  198. </>
  199. )}
  200. {(isRemote || isLocal) && (
  201. <Controller
  202. name="option.update_interval"
  203. control={control}
  204. render={({ field }) => (
  205. <TextField
  206. {...text}
  207. {...field}
  208. onChange={(e) => {
  209. e.target.value = e.target.value
  210. ?.replace(/\D/, "")
  211. .slice(0, 10);
  212. field.onChange(e);
  213. }}
  214. label={t("Update Interval")}
  215. InputProps={{
  216. endAdornment: (
  217. <InputAdornment position="end">mins</InputAdornment>
  218. ),
  219. }}
  220. />
  221. )}
  222. />
  223. )}
  224. {isLocal && openType === "new" && (
  225. <FileInput onChange={(val) => (fileDataRef.current = val)} />
  226. )}
  227. {isRemote && (
  228. <>
  229. <Controller
  230. name="option.with_proxy"
  231. control={control}
  232. render={({ field }) => (
  233. <StyledBox>
  234. <InputLabel>{t("Use System Proxy")}</InputLabel>
  235. <Switch checked={field.value} {...field} color="primary" />
  236. </StyledBox>
  237. )}
  238. />
  239. <Controller
  240. name="option.self_proxy"
  241. control={control}
  242. render={({ field }) => (
  243. <StyledBox>
  244. <InputLabel>{t("Use Clash Proxy")}</InputLabel>
  245. <Switch checked={field.value} {...field} color="primary" />
  246. </StyledBox>
  247. )}
  248. />
  249. <Controller
  250. name="option.danger_accept_invalid_certs"
  251. control={control}
  252. render={({ field }) => (
  253. <StyledBox>
  254. <InputLabel>{t("Accept Invalid Certs (Danger)")}</InputLabel>
  255. <Switch checked={field.value} {...field} color="primary" />
  256. </StyledBox>
  257. )}
  258. />
  259. </>
  260. )}
  261. </BaseDialog>
  262. );
  263. }
  264. );
  265. const StyledBox = styled(Box)(() => ({
  266. margin: "8px 0 8px 8px",
  267. display: "flex",
  268. alignItems: "center",
  269. justifyContent: "space-between",
  270. }));