proxies-editor-viewer.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. import { ReactNode, useEffect, useMemo, useState } from "react";
  2. import { useLockFn } from "ahooks";
  3. import yaml from "js-yaml";
  4. import { useTranslation } from "react-i18next";
  5. import {
  6. DndContext,
  7. closestCenter,
  8. KeyboardSensor,
  9. PointerSensor,
  10. useSensor,
  11. useSensors,
  12. DragEndEvent,
  13. } from "@dnd-kit/core";
  14. import {
  15. SortableContext,
  16. sortableKeyboardCoordinates,
  17. } from "@dnd-kit/sortable";
  18. import {
  19. Box,
  20. Button,
  21. Dialog,
  22. DialogActions,
  23. DialogContent,
  24. DialogTitle,
  25. List,
  26. ListItem,
  27. ListItemText,
  28. TextField,
  29. styled,
  30. } from "@mui/material";
  31. import { ProxyItem } from "@/components/profile/proxy-item";
  32. import { readProfileFile, saveProfileFile } from "@/services/cmds";
  33. import { Notice } from "@/components/base";
  34. import getSystem from "@/utils/get-system";
  35. import { BaseSearchBox } from "../base/base-search-box";
  36. import { Virtuoso } from "react-virtuoso";
  37. import MonacoEditor from "react-monaco-editor";
  38. import { useThemeMode } from "@/services/states";
  39. import parseUri from "@/utils/uri-parser";
  40. interface Props {
  41. profileUid: string;
  42. property: string;
  43. open: boolean;
  44. onClose: () => void;
  45. onSave?: (prev?: string, curr?: string) => void;
  46. }
  47. export const ProxiesEditorViewer = (props: Props) => {
  48. const { profileUid, property, open, onClose, onSave } = props;
  49. const { t } = useTranslation();
  50. const themeMode = useThemeMode();
  51. const [prevData, setPrevData] = useState("");
  52. const [currData, setCurrData] = useState("");
  53. const [visualization, setVisualization] = useState(true);
  54. const [match, setMatch] = useState(() => (_: string) => true);
  55. const [proxyUri, setProxyUri] = useState<string>("");
  56. const [proxyList, setProxyList] = useState<IProxyConfig[]>([]);
  57. const [prependSeq, setPrependSeq] = useState<IProxyConfig[]>([]);
  58. const [appendSeq, setAppendSeq] = useState<IProxyConfig[]>([]);
  59. const [deleteSeq, setDeleteSeq] = useState<string[]>([]);
  60. const filteredProxyList = useMemo(
  61. () => proxyList.filter((proxy) => match(proxy.name)),
  62. [proxyList, match]
  63. );
  64. const sensors = useSensors(
  65. useSensor(PointerSensor),
  66. useSensor(KeyboardSensor, {
  67. coordinateGetter: sortableKeyboardCoordinates,
  68. })
  69. );
  70. const reorder = (
  71. list: IProxyConfig[],
  72. startIndex: number,
  73. endIndex: number
  74. ) => {
  75. const result = Array.from(list);
  76. const [removed] = result.splice(startIndex, 1);
  77. result.splice(endIndex, 0, removed);
  78. return result;
  79. };
  80. const onPrependDragEnd = async (event: DragEndEvent) => {
  81. const { active, over } = event;
  82. if (over) {
  83. if (active.id !== over.id) {
  84. let activeIndex = 0;
  85. let overIndex = 0;
  86. prependSeq.forEach((item, index) => {
  87. if (item.name === active.id) {
  88. activeIndex = index;
  89. }
  90. if (item.name === over.id) {
  91. overIndex = index;
  92. }
  93. });
  94. setPrependSeq(reorder(prependSeq, activeIndex, overIndex));
  95. }
  96. }
  97. };
  98. const onAppendDragEnd = async (event: DragEndEvent) => {
  99. const { active, over } = event;
  100. if (over) {
  101. if (active.id !== over.id) {
  102. let activeIndex = 0;
  103. let overIndex = 0;
  104. appendSeq.forEach((item, index) => {
  105. if (item.name === active.id) {
  106. activeIndex = index;
  107. }
  108. if (item.name === over.id) {
  109. overIndex = index;
  110. }
  111. });
  112. setAppendSeq(reorder(appendSeq, activeIndex, overIndex));
  113. }
  114. }
  115. };
  116. const fetchProfile = async () => {
  117. let data = await readProfileFile(profileUid);
  118. let originProxiesObj = yaml.load(data) as {
  119. proxies: IProxyConfig[];
  120. } | null;
  121. setProxyList(originProxiesObj?.proxies || []);
  122. };
  123. const fetchContent = async () => {
  124. let data = await readProfileFile(property);
  125. let obj = yaml.load(data) as ISeqProfileConfig | null;
  126. setPrependSeq(obj?.prepend || []);
  127. setAppendSeq(obj?.append || []);
  128. setDeleteSeq(obj?.delete || []);
  129. setPrevData(data);
  130. setCurrData(data);
  131. };
  132. useEffect(() => {
  133. if (currData === "") return;
  134. if (visualization !== true) return;
  135. let obj = yaml.load(currData) as {
  136. prepend: [];
  137. append: [];
  138. delete: [];
  139. } | null;
  140. setPrependSeq(obj?.prepend || []);
  141. setAppendSeq(obj?.append || []);
  142. setDeleteSeq(obj?.delete || []);
  143. }, [visualization]);
  144. useEffect(() => {
  145. if (prependSeq && appendSeq && deleteSeq)
  146. setCurrData(
  147. yaml.dump(
  148. { prepend: prependSeq, append: appendSeq, delete: deleteSeq },
  149. {
  150. forceQuotes: true,
  151. }
  152. )
  153. );
  154. }, [prependSeq, appendSeq, deleteSeq]);
  155. useEffect(() => {
  156. if (!open) return;
  157. fetchContent();
  158. fetchProfile();
  159. }, [open]);
  160. const handleSave = useLockFn(async () => {
  161. try {
  162. await saveProfileFile(property, currData);
  163. onSave?.(prevData, currData);
  164. onClose();
  165. } catch (err: any) {
  166. Notice.error(err.message || err.toString());
  167. }
  168. });
  169. return (
  170. <Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
  171. <DialogTitle>
  172. {
  173. <Box display="flex" justifyContent="space-between">
  174. {t("Edit Proxies")}
  175. <Box>
  176. <Button
  177. variant="contained"
  178. size="small"
  179. onClick={() => {
  180. setVisualization((prev) => !prev);
  181. }}
  182. >
  183. {visualization ? t("Advanced") : t("Visualization")}
  184. </Button>
  185. </Box>
  186. </Box>
  187. }
  188. </DialogTitle>
  189. <DialogContent
  190. sx={{ display: "flex", width: "auto", height: "calc(100vh - 185px)" }}
  191. >
  192. {visualization ? (
  193. <>
  194. <List
  195. sx={{
  196. width: "50%",
  197. padding: "0 10px",
  198. }}
  199. >
  200. <Box
  201. sx={{
  202. height: "calc(100% - 80px)",
  203. overflowY: "auto",
  204. }}
  205. >
  206. <Item>
  207. <TextField
  208. autoComplete="off"
  209. placeholder={t("Use newlines for multiple uri")}
  210. fullWidth
  211. minRows={8}
  212. multiline
  213. size="small"
  214. onChange={(e) => setProxyUri(e.target.value)}
  215. />
  216. </Item>
  217. </Box>
  218. <Item>
  219. <Button
  220. fullWidth
  221. variant="contained"
  222. onClick={() => {
  223. let proxies = [] as IProxyConfig[];
  224. proxyUri
  225. .trim()
  226. .split("\n")
  227. .forEach((uri) => {
  228. try {
  229. let proxy = parseUri(uri.trim());
  230. proxies.push(proxy);
  231. } catch (err: any) {
  232. Notice.error(err.message || err.toString());
  233. }
  234. });
  235. setPrependSeq([...prependSeq, ...proxies]);
  236. }}
  237. >
  238. {t("Prepend Proxy")}
  239. </Button>
  240. </Item>
  241. <Item>
  242. <Button
  243. fullWidth
  244. variant="contained"
  245. onClick={() => {
  246. let proxies = [] as IProxyConfig[];
  247. proxyUri
  248. .trim()
  249. .split("\n")
  250. .forEach((uri) => {
  251. try {
  252. let proxy = parseUri(uri.trim());
  253. proxies.push(proxy);
  254. } catch (err: any) {
  255. Notice.error(err.message || err.toString());
  256. }
  257. });
  258. setAppendSeq([...appendSeq, ...proxies]);
  259. }}
  260. >
  261. {t("Append Proxy")}
  262. </Button>
  263. </Item>
  264. </List>
  265. <List
  266. sx={{
  267. width: "50%",
  268. padding: "0 10px",
  269. }}
  270. >
  271. <BaseSearchBox
  272. matchCase={false}
  273. onSearch={(match) => setMatch(() => match)}
  274. />
  275. <Virtuoso
  276. style={{ height: "calc(100% - 24px)", marginTop: "8px" }}
  277. totalCount={
  278. filteredProxyList.length +
  279. (prependSeq.length > 0 ? 1 : 0) +
  280. (appendSeq.length > 0 ? 1 : 0)
  281. }
  282. increaseViewportBy={256}
  283. itemContent={(index) => {
  284. let shift = prependSeq.length > 0 ? 1 : 0;
  285. if (prependSeq.length > 0 && index === 0) {
  286. return (
  287. <DndContext
  288. sensors={sensors}
  289. collisionDetection={closestCenter}
  290. onDragEnd={onPrependDragEnd}
  291. >
  292. <SortableContext
  293. items={prependSeq.map((x) => {
  294. return x.name;
  295. })}
  296. >
  297. {prependSeq.map((item, index) => {
  298. return (
  299. <ProxyItem
  300. key={`${item.name}-${index}`}
  301. type="prepend"
  302. proxy={item}
  303. onDelete={() => {
  304. setPrependSeq(
  305. prependSeq.filter(
  306. (v) => v.name !== item.name
  307. )
  308. );
  309. }}
  310. />
  311. );
  312. })}
  313. </SortableContext>
  314. </DndContext>
  315. );
  316. } else if (index < filteredProxyList.length + shift) {
  317. let newIndex = index - shift;
  318. return (
  319. <ProxyItem
  320. key={`${filteredProxyList[newIndex].name}-${index}`}
  321. type={
  322. deleteSeq.includes(filteredProxyList[newIndex].name)
  323. ? "delete"
  324. : "original"
  325. }
  326. proxy={filteredProxyList[newIndex]}
  327. onDelete={() => {
  328. if (
  329. deleteSeq.includes(filteredProxyList[newIndex].name)
  330. ) {
  331. setDeleteSeq(
  332. deleteSeq.filter(
  333. (v) => v !== filteredProxyList[newIndex].name
  334. )
  335. );
  336. } else {
  337. setDeleteSeq((prev) => [
  338. ...prev,
  339. filteredProxyList[newIndex].name,
  340. ]);
  341. }
  342. }}
  343. />
  344. );
  345. } else {
  346. return (
  347. <DndContext
  348. sensors={sensors}
  349. collisionDetection={closestCenter}
  350. onDragEnd={onAppendDragEnd}
  351. >
  352. <SortableContext
  353. items={appendSeq.map((x) => {
  354. return x.name;
  355. })}
  356. >
  357. {appendSeq.map((item, index) => {
  358. return (
  359. <ProxyItem
  360. key={`${item.name}-${index}`}
  361. type="append"
  362. proxy={item}
  363. onDelete={() => {
  364. setAppendSeq(
  365. appendSeq.filter(
  366. (v) => v.name !== item.name
  367. )
  368. );
  369. }}
  370. />
  371. );
  372. })}
  373. </SortableContext>
  374. </DndContext>
  375. );
  376. }
  377. }}
  378. />
  379. </List>
  380. </>
  381. ) : (
  382. <MonacoEditor
  383. height="100%"
  384. language="yaml"
  385. value={currData}
  386. theme={themeMode === "light" ? "vs" : "vs-dark"}
  387. options={{
  388. tabSize: 2, // 根据语言类型设置缩进大小
  389. minimap: {
  390. enabled: document.documentElement.clientWidth >= 1500, // 超过一定宽度显示minimap滚动条
  391. },
  392. mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
  393. quickSuggestions: {
  394. strings: true, // 字符串类型的建议
  395. comments: true, // 注释类型的建议
  396. other: true, // 其他类型的建议
  397. },
  398. padding: {
  399. top: 33, // 顶部padding防止遮挡snippets
  400. },
  401. fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
  402. getSystem() === "windows" ? ", twemoji mozilla" : ""
  403. }`,
  404. fontLigatures: true, // 连字符
  405. smoothScrolling: true, // 平滑滚动
  406. }}
  407. onChange={(value) => setCurrData(value)}
  408. />
  409. )}
  410. </DialogContent>
  411. <DialogActions>
  412. <Button onClick={onClose} variant="outlined">
  413. {t("Cancel")}
  414. </Button>
  415. <Button onClick={handleSave} variant="contained">
  416. {t("Save")}
  417. </Button>
  418. </DialogActions>
  419. </Dialog>
  420. );
  421. };
  422. const Item = styled(ListItem)(() => ({
  423. padding: "5px 2px",
  424. }));