profile-new.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import { useRef, useState } from "react";
  2. import { mutate } from "swr";
  3. import { useTranslation } from "react-i18next";
  4. import { useLockFn, useSetState } from "ahooks";
  5. import {
  6. Button,
  7. Collapse,
  8. Dialog,
  9. DialogActions,
  10. DialogContent,
  11. DialogTitle,
  12. FormControl,
  13. FormControlLabel,
  14. IconButton,
  15. InputLabel,
  16. MenuItem,
  17. Select,
  18. Switch,
  19. TextField,
  20. } from "@mui/material";
  21. import { Settings } from "@mui/icons-material";
  22. import { createProfile } from "@/services/cmds";
  23. import { Notice } from "@/components/base";
  24. import FileInput from "./file-input";
  25. interface Props {
  26. open: boolean;
  27. onClose: () => void;
  28. }
  29. // create a new profile
  30. // remote / local file / merge / script
  31. const ProfileNew = (props: Props) => {
  32. const { open, onClose } = props;
  33. const { t } = useTranslation();
  34. const [form, setForm] = useSetState({
  35. type: "remote",
  36. name: "",
  37. desc: "",
  38. url: "",
  39. });
  40. const [showOpt, setShowOpt] = useState(false);
  41. // can add more option
  42. const [option, setOption] = useSetState({
  43. user_agent: "",
  44. with_proxy: false,
  45. self_proxy: false,
  46. });
  47. // file input
  48. const fileDataRef = useRef<string | null>(null);
  49. const onCreate = useLockFn(async () => {
  50. if (!form.type) {
  51. Notice.error("`Type` should not be null");
  52. return;
  53. }
  54. try {
  55. const name = form.name || `${form.type} file`;
  56. if (form.type === "remote" && !form.url) {
  57. throw new Error("The URL should not be null");
  58. }
  59. const option_ = form.type === "remote" ? option : undefined;
  60. const item = { ...form, name, option: option_ };
  61. const fileData = form.type === "local" ? fileDataRef.current : null;
  62. await createProfile(item, fileData);
  63. setForm({ type: "remote", name: "", desc: "", url: "" });
  64. setOption({ user_agent: "" });
  65. setShowOpt(false);
  66. fileDataRef.current = null;
  67. mutate("getProfiles");
  68. onClose();
  69. } catch (err: any) {
  70. Notice.error(err.message || err.toString());
  71. }
  72. });
  73. const textFieldProps = {
  74. fullWidth: true,
  75. size: "small",
  76. margin: "normal",
  77. variant: "outlined",
  78. } as const;
  79. return (
  80. <Dialog open={open} onClose={onClose}>
  81. <DialogTitle sx={{ pb: 0.5 }}>{t("Create Profile")}</DialogTitle>
  82. <DialogContent sx={{ width: 336, pb: 1 }}>
  83. <FormControl size="small" fullWidth sx={{ mt: 2, mb: 1 }}>
  84. <InputLabel>Type</InputLabel>
  85. <Select
  86. autoFocus
  87. label={t("Type")}
  88. value={form.type}
  89. onChange={(e) => setForm({ type: e.target.value })}
  90. >
  91. <MenuItem value="remote">Remote</MenuItem>
  92. <MenuItem value="local">Local</MenuItem>
  93. <MenuItem value="script">Script</MenuItem>
  94. <MenuItem value="merge">Merge</MenuItem>
  95. </Select>
  96. </FormControl>
  97. <TextField
  98. {...textFieldProps}
  99. label={t("Name")}
  100. autoComplete="off"
  101. value={form.name}
  102. onChange={(e) => setForm({ name: e.target.value })}
  103. />
  104. <TextField
  105. {...textFieldProps}
  106. label={t("Descriptions")}
  107. autoComplete="off"
  108. value={form.desc}
  109. onChange={(e) => setForm({ desc: e.target.value })}
  110. />
  111. {form.type === "remote" && (
  112. <TextField
  113. {...textFieldProps}
  114. label={t("Subscription URL")}
  115. autoComplete="off"
  116. value={form.url}
  117. onChange={(e) => setForm({ url: e.target.value })}
  118. />
  119. )}
  120. {form.type === "local" && (
  121. <FileInput onChange={(val) => (fileDataRef.current = val)} />
  122. )}
  123. <Collapse
  124. in={form.type === "remote" && showOpt}
  125. timeout="auto"
  126. unmountOnExit
  127. >
  128. <TextField
  129. {...textFieldProps}
  130. label="User Agent"
  131. autoComplete="off"
  132. value={option.user_agent}
  133. onChange={(e) => setOption({ user_agent: e.target.value })}
  134. />
  135. <FormControlLabel
  136. label={t("Use System Proxy")}
  137. labelPlacement="start"
  138. sx={{ ml: 0, my: 1 }}
  139. control={
  140. <Switch
  141. color="primary"
  142. checked={option.with_proxy}
  143. onChange={(_e, c) =>
  144. setOption((o) => ({
  145. self_proxy: c ? false : o.self_proxy,
  146. with_proxy: c,
  147. }))
  148. }
  149. />
  150. }
  151. />
  152. <FormControlLabel
  153. label={t("Use Clash Proxy")}
  154. labelPlacement="start"
  155. sx={{ ml: 0, my: 1 }}
  156. control={
  157. <Switch
  158. color="primary"
  159. checked={option.self_proxy}
  160. onChange={(_e, c) =>
  161. setOption((o) => ({
  162. with_proxy: c ? false : o.with_proxy,
  163. self_proxy: c,
  164. }))
  165. }
  166. />
  167. }
  168. />
  169. </Collapse>
  170. </DialogContent>
  171. <DialogActions sx={{ px: 2, pb: 2, position: "relative" }}>
  172. {form.type === "remote" && (
  173. <IconButton
  174. size="small"
  175. color="inherit"
  176. sx={{ position: "absolute", left: 18 }}
  177. onClick={() => setShowOpt((o) => !o)}
  178. >
  179. <Settings />
  180. </IconButton>
  181. )}
  182. <Button onClick={onClose} variant="outlined">
  183. {t("Cancel")}
  184. </Button>
  185. <Button onClick={onCreate} variant="contained">
  186. {t("Save")}
  187. </Button>
  188. </DialogActions>
  189. </Dialog>
  190. );
  191. };
  192. export default ProfileNew;