Browse Source

feat: support edit rules file

MystiPanda 11 months ago
parent
commit
fa4ac00504
3 changed files with 305 additions and 224 deletions
  1. 301 224
      src/components/profile/rules-editor-viewer.tsx
  2. 2 0
      src/locales/en.json
  3. 2 0
      src/locales/zh.json

+ 301 - 224
src/components/profile/rules-editor-viewer.tsx

@@ -36,6 +36,8 @@ import getSystem from "@/utils/get-system";
 import { RuleItem } from "@/components/profile/rule-item";
 import { BaseSearchBox } from "../base/base-search-box";
 import { Virtuoso } from "react-virtuoso";
+import MonacoEditor from "react-monaco-editor";
+import { useThemeMode } from "@/services/states";
 
 interface Props {
   profileUid: string;
@@ -230,8 +232,11 @@ const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
 export const RulesEditorViewer = (props: Props) => {
   const { title, profileUid, property, open, onClose, onSave } = props;
   const { t } = useTranslation();
+  const themeMode = useThemeMode();
 
   const [prevData, setPrevData] = useState("");
+  const [currData, setCurrData] = useState("");
+  const [visible, setVisible] = useState(true);
   const [match, setMatch] = useState(() => (_: string) => true);
 
   const [ruleType, setRuleType] = useState<(typeof rules)[number]>(rules[0]);
@@ -291,9 +296,28 @@ export const RulesEditorViewer = (props: Props) => {
     setPrependSeq(obj.prepend || []);
     setAppendSeq(obj.append || []);
     setDeleteSeq(obj.delete || []);
+
     setPrevData(data);
+    setCurrData(data);
   };
 
+  useEffect(() => {
+    if (currData === "") return;
+    if (visible !== true) return;
+
+    let obj = yaml.load(currData) as { prepend: []; append: []; delete: [] };
+    setPrependSeq(obj.prepend || []);
+    setAppendSeq(obj.append || []);
+    setDeleteSeq(obj.delete || []);
+  }, [visible]);
+
+  useEffect(() => {
+    if (prependSeq && appendSeq && deleteSeq)
+      setCurrData(
+        yaml.dump({ prepend: prependSeq, append: appendSeq, delete: deleteSeq })
+      );
+  }, [prependSeq, appendSeq, deleteSeq]);
+
   const fetchProfile = async () => {
     let data = await readProfileFile(profileUid);
     let groupsObj = yaml.load(data) as { "proxy-groups": [] };
@@ -338,11 +362,6 @@ export const RulesEditorViewer = (props: Props) => {
 
   const handleSave = useLockFn(async () => {
     try {
-      let currData = yaml.dump({
-        prepend: prependSeq,
-        append: appendSeq,
-        delete: deleteSeq,
-      });
       await saveProfileFile(property, currData);
       onSave?.(prevData, currData);
       onClose();
@@ -353,234 +372,292 @@ export const RulesEditorViewer = (props: Props) => {
 
   return (
     <Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
-      <DialogTitle>{title ?? t("Edit Rules")}</DialogTitle>
+      <DialogTitle>
+        {
+          <Box display="flex" justifyContent="space-between">
+            {t("Edit Rules")}
+            <Box>
+              <Button
+                variant="contained"
+                size="small"
+                onClick={() => {
+                  setVisible((prev) => !prev);
+                }}
+              >
+                {visible ? t("Advanced") : t("Visible")}
+              </Button>
+            </Box>
+          </Box>
+        }
+      </DialogTitle>
 
       <DialogContent sx={{ display: "flex", width: "auto", height: "100vh" }}>
-        <List
-          sx={{
-            height: "calc(100% - 16px)",
-            width: "50%",
-            padding: "0 10px",
-          }}
-        >
-          <Item>
-            <ListItemText primary={t("Rule Type")} />
-            <Autocomplete
-              size="small"
-              sx={{ minWidth: "240px" }}
-              renderInput={(params) => <TextField {...params} />}
-              options={rules}
-              value={ruleType}
-              getOptionLabel={(option) => option.name}
-              renderOption={(props, option) => (
-                <li {...props} title={t(option.name)}>
-                  {option.name}
-                </li>
-              )}
-              onChange={(_, value) => value && setRuleType(value)}
-            />
-          </Item>
-          <Item sx={{ display: !(ruleType.required ?? true) ? "none" : "" }}>
-            <ListItemText primary={t("Rule Content")} />
+        {visible ? (
+          <>
+            <List
+              sx={{
+                height: "calc(100% - 16px)",
+                width: "50%",
+                padding: "0 10px",
+              }}
+            >
+              <Item>
+                <ListItemText primary={t("Rule Type")} />
+                <Autocomplete
+                  size="small"
+                  sx={{ minWidth: "240px" }}
+                  renderInput={(params) => <TextField {...params} />}
+                  options={rules}
+                  value={ruleType}
+                  getOptionLabel={(option) => option.name}
+                  renderOption={(props, option) => (
+                    <li {...props} title={t(option.name)}>
+                      {option.name}
+                    </li>
+                  )}
+                  onChange={(_, value) => value && setRuleType(value)}
+                />
+              </Item>
+              <Item
+                sx={{ display: !(ruleType.required ?? true) ? "none" : "" }}
+              >
+                <ListItemText primary={t("Rule Content")} />
 
-            {ruleType.name === "RULE-SET" && (
-              <Autocomplete
-                size="small"
-                sx={{ minWidth: "240px" }}
-                renderInput={(params) => <TextField {...params} />}
-                options={ruleSetList}
-                value={ruleContent}
-                onChange={(_, value) => value && setRuleContent(value)}
-              />
-            )}
-            {ruleType.name === "SUB-RULE" && (
-              <Autocomplete
-                size="small"
-                sx={{ minWidth: "240px" }}
-                renderInput={(params) => <TextField {...params} />}
-                options={subRuleList}
-                value={ruleContent}
-                onChange={(_, value) => value && setRuleContent(value)}
-              />
-            )}
-            {ruleType.name !== "RULE-SET" && ruleType.name !== "SUB-RULE" && (
-              <TextField
-                autoComplete="off"
-                size="small"
-                sx={{ minWidth: "240px" }}
-                value={ruleContent}
-                required={ruleType.required ?? true}
-                error={(ruleType.required ?? true) && !ruleContent}
-                placeholder={ruleType.example}
-                onChange={(e) => setRuleContent(e.target.value)}
-              />
-            )}
-          </Item>
-          <Item>
-            <ListItemText primary={t("Proxy Policy")} />
-            <Autocomplete
-              size="small"
-              sx={{ minWidth: "240px" }}
-              renderInput={(params) => <TextField {...params} />}
-              options={proxyPolicyList}
-              value={proxyPolicy}
-              renderOption={(props, option) => (
-                <li {...props} title={t(option)}>
-                  {option}
-                </li>
+                {ruleType.name === "RULE-SET" && (
+                  <Autocomplete
+                    size="small"
+                    sx={{ minWidth: "240px" }}
+                    renderInput={(params) => <TextField {...params} />}
+                    options={ruleSetList}
+                    value={ruleContent}
+                    onChange={(_, value) => value && setRuleContent(value)}
+                  />
+                )}
+                {ruleType.name === "SUB-RULE" && (
+                  <Autocomplete
+                    size="small"
+                    sx={{ minWidth: "240px" }}
+                    renderInput={(params) => <TextField {...params} />}
+                    options={subRuleList}
+                    value={ruleContent}
+                    onChange={(_, value) => value && setRuleContent(value)}
+                  />
+                )}
+                {ruleType.name !== "RULE-SET" &&
+                  ruleType.name !== "SUB-RULE" && (
+                    <TextField
+                      autoComplete="off"
+                      size="small"
+                      sx={{ minWidth: "240px" }}
+                      value={ruleContent}
+                      required={ruleType.required ?? true}
+                      error={(ruleType.required ?? true) && !ruleContent}
+                      placeholder={ruleType.example}
+                      onChange={(e) => setRuleContent(e.target.value)}
+                    />
+                  )}
+              </Item>
+              <Item>
+                <ListItemText primary={t("Proxy Policy")} />
+                <Autocomplete
+                  size="small"
+                  sx={{ minWidth: "240px" }}
+                  renderInput={(params) => <TextField {...params} />}
+                  options={proxyPolicyList}
+                  value={proxyPolicy}
+                  renderOption={(props, option) => (
+                    <li {...props} title={t(option)}>
+                      {option}
+                    </li>
+                  )}
+                  onChange={(_, value) => value && setProxyPolicy(value)}
+                />
+              </Item>
+              {ruleType.noResolve && (
+                <Item>
+                  <ListItemText primary={t("No Resolve")} />
+                  <Switch
+                    checked={noResolve}
+                    onChange={() => setNoResolve(!noResolve)}
+                  />
+                </Item>
               )}
-              onChange={(_, value) => value && setProxyPolicy(value)}
-            />
-          </Item>
-          {ruleType.noResolve && (
-            <Item>
-              <ListItemText primary={t("No Resolve")} />
-              <Switch
-                checked={noResolve}
-                onChange={() => setNoResolve(!noResolve)}
-              />
-            </Item>
-          )}
-          <Item>
-            <Button
-              fullWidth
-              variant="contained"
-              onClick={() => {
-                try {
-                  let raw = validateRule();
-                  if (prependSeq.includes(raw)) return;
-                  setPrependSeq([...prependSeq, raw]);
-                } catch (err: any) {
-                  Notice.error(err.message || err.toString());
-                }
+              <Item>
+                <Button
+                  fullWidth
+                  variant="contained"
+                  onClick={() => {
+                    try {
+                      let raw = validateRule();
+                      if (prependSeq.includes(raw)) return;
+                      setPrependSeq([...prependSeq, raw]);
+                    } catch (err: any) {
+                      Notice.error(err.message || err.toString());
+                    }
+                  }}
+                >
+                  {t("Prepend Rule")}
+                </Button>
+              </Item>
+              <Item>
+                <Button
+                  fullWidth
+                  variant="contained"
+                  onClick={() => {
+                    try {
+                      let raw = validateRule();
+                      if (appendSeq.includes(raw)) return;
+                      setAppendSeq([...appendSeq, raw]);
+                    } catch (err: any) {
+                      Notice.error(err.message || err.toString());
+                    }
+                  }}
+                >
+                  {t("Append Rule")}
+                </Button>
+              </Item>
+            </List>
+
+            <List
+              sx={{
+                height: "calc(100% - 16px)",
+                width: "50%",
+                padding: "0 10px",
               }}
             >
-              {t("Prepend Rule")}
-            </Button>
-          </Item>
-          <Item>
-            <Button
-              fullWidth
-              variant="contained"
-              onClick={() => {
-                try {
-                  let raw = validateRule();
-                  if (appendSeq.includes(raw)) return;
-                  setAppendSeq([...appendSeq, raw]);
-                } catch (err: any) {
-                  Notice.error(err.message || err.toString());
+              <BaseSearchBox
+                matchCase={false}
+                onSearch={(match) => setMatch(() => match)}
+              />
+              <Virtuoso
+                style={{ height: "calc(100% - 16px)", marginTop: "8px" }}
+                totalCount={
+                  filteredRuleList.length +
+                  (prependSeq.length > 0 ? 1 : 0) +
+                  (appendSeq.length > 0 ? 1 : 0)
                 }
-              }}
-            >
-              {t("Append Rule")}
-            </Button>
-          </Item>
-        </List>
-
-        <List
-          sx={{ height: "calc(100% - 16px)", width: "50%", padding: "0 10px" }}
-        >
-          <BaseSearchBox
-            matchCase={false}
-            onSearch={(match) => setMatch(() => match)}
-          />
-          <Virtuoso
-            style={{ height: "calc(100% - 16px)", marginTop: "8px" }}
-            totalCount={
-              filteredRuleList.length +
-              (prependSeq.length > 0 ? 1 : 0) +
-              (appendSeq.length > 0 ? 1 : 0)
-            }
-            increaseViewportBy={256}
-            itemContent={(index) => {
-              let shift = prependSeq.length > 0 ? 1 : 0;
-              if (prependSeq.length > 0 && index === 0) {
-                return (
-                  <DndContext
-                    sensors={sensors}
-                    collisionDetection={closestCenter}
-                    onDragEnd={onPrependDragEnd}
-                  >
-                    <SortableContext
-                      items={prependSeq.map((x) => {
-                        return x;
-                      })}
-                    >
-                      {prependSeq.map((item, index) => {
-                        return (
-                          <RuleItem
-                            key={`${item}-${index}`}
-                            type="prepend"
-                            ruleRaw={item}
-                            onDelete={() => {
-                              setPrependSeq(
-                                prependSeq.filter((v) => v !== item)
-                              );
-                            }}
-                          />
-                        );
-                      })}
-                    </SortableContext>
-                  </DndContext>
-                );
-              } else if (index < filteredRuleList.length + shift) {
-                let newIndex = index - shift;
-                return (
-                  <RuleItem
-                    key={`${filteredRuleList[newIndex]}-${index}`}
-                    type={
-                      deleteSeq.includes(filteredRuleList[newIndex])
-                        ? "delete"
-                        : "original"
-                    }
-                    ruleRaw={filteredRuleList[newIndex]}
-                    onDelete={() => {
-                      if (deleteSeq.includes(filteredRuleList[newIndex])) {
-                        setDeleteSeq(
-                          deleteSeq.filter(
-                            (v) => v !== filteredRuleList[newIndex]
-                          )
-                        );
-                      } else {
-                        setDeleteSeq((prev) => [
-                          ...prev,
-                          filteredRuleList[newIndex],
-                        ]);
-                      }
-                    }}
-                  />
-                );
-              } else {
-                return (
-                  <DndContext
-                    sensors={sensors}
-                    collisionDetection={closestCenter}
-                    onDragEnd={onAppendDragEnd}
-                  >
-                    <SortableContext
-                      items={appendSeq.map((x) => {
-                        return x;
-                      })}
-                    >
-                      {appendSeq.map((item, index) => {
-                        return (
-                          <RuleItem
-                            key={`${item}-${index}`}
-                            type="append"
-                            ruleRaw={item}
-                            onDelete={() => {
-                              setAppendSeq(appendSeq.filter((v) => v !== item));
-                            }}
-                          />
-                        );
-                      })}
-                    </SortableContext>
-                  </DndContext>
-                );
-              }
+                increaseViewportBy={256}
+                itemContent={(index) => {
+                  let shift = prependSeq.length > 0 ? 1 : 0;
+                  if (prependSeq.length > 0 && index === 0) {
+                    return (
+                      <DndContext
+                        sensors={sensors}
+                        collisionDetection={closestCenter}
+                        onDragEnd={onPrependDragEnd}
+                      >
+                        <SortableContext
+                          items={prependSeq.map((x) => {
+                            return x;
+                          })}
+                        >
+                          {prependSeq.map((item, index) => {
+                            return (
+                              <RuleItem
+                                key={`${item}-${index}`}
+                                type="prepend"
+                                ruleRaw={item}
+                                onDelete={() => {
+                                  setPrependSeq(
+                                    prependSeq.filter((v) => v !== item)
+                                  );
+                                }}
+                              />
+                            );
+                          })}
+                        </SortableContext>
+                      </DndContext>
+                    );
+                  } else if (index < filteredRuleList.length + shift) {
+                    let newIndex = index - shift;
+                    return (
+                      <RuleItem
+                        key={`${filteredRuleList[newIndex]}-${index}`}
+                        type={
+                          deleteSeq.includes(filteredRuleList[newIndex])
+                            ? "delete"
+                            : "original"
+                        }
+                        ruleRaw={filteredRuleList[newIndex]}
+                        onDelete={() => {
+                          if (deleteSeq.includes(filteredRuleList[newIndex])) {
+                            setDeleteSeq(
+                              deleteSeq.filter(
+                                (v) => v !== filteredRuleList[newIndex]
+                              )
+                            );
+                          } else {
+                            setDeleteSeq((prev) => [
+                              ...prev,
+                              filteredRuleList[newIndex],
+                            ]);
+                          }
+                        }}
+                      />
+                    );
+                  } else {
+                    return (
+                      <DndContext
+                        sensors={sensors}
+                        collisionDetection={closestCenter}
+                        onDragEnd={onAppendDragEnd}
+                      >
+                        <SortableContext
+                          items={appendSeq.map((x) => {
+                            return x;
+                          })}
+                        >
+                          {appendSeq.map((item, index) => {
+                            return (
+                              <RuleItem
+                                key={`${item}-${index}`}
+                                type="append"
+                                ruleRaw={item}
+                                onDelete={() => {
+                                  setAppendSeq(
+                                    appendSeq.filter((v) => v !== item)
+                                  );
+                                }}
+                              />
+                            );
+                          })}
+                        </SortableContext>
+                      </DndContext>
+                    );
+                  }
+                }}
+              />
+            </List>
+          </>
+        ) : (
+          <MonacoEditor
+            height="100%"
+            language="yaml"
+            value={currData}
+            theme={themeMode === "light" ? "vs" : "vs-dark"}
+            options={{
+              tabSize: 2, // 根据语言类型设置缩进大小
+              minimap: {
+                enabled: document.documentElement.clientWidth >= 1500, // 超过一定宽度显示minimap滚动条
+              },
+              mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
+              quickSuggestions: {
+                strings: true, // 字符串类型的建议
+                comments: true, // 注释类型的建议
+                other: true, // 其他类型的建议
+              },
+              padding: {
+                top: 33, // 顶部padding防止遮挡snippets
+              },
+              fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
+                getSystem() === "windows" ? ", twemoji mozilla" : ""
+              }`,
+              fontLigatures: true, // 连字符
+              smoothScrolling: true, // 平滑滚动
             }}
+            onChange={(value) => setCurrData(value)}
           />
-        </List>
+        )}
       </DialogContent>
 
       <DialogActions>

+ 2 - 0
src/locales/en.json

@@ -64,6 +64,8 @@
   "Delete Rule": "Delete Rule",
   "Rule Condition Required": "Rule Condition Required",
   "Invalid Rule": "Invalid Rule",
+  "Advanced": "Advanced",
+  "Visible": "Visible",
   "DOMAIN": "Matches the full domain name",
   "DOMAIN-SUFFIX": "Matches the domain suffix",
   "DOMAIN-KEYWORD": "Matches the domain keyword",

+ 2 - 0
src/locales/zh.json

@@ -64,6 +64,8 @@
   "Delete Rule": "删除规则",
   "Rule Condition Required": "规则条件缺失",
   "Invalid Rule": "无效规则",
+  "Advanced": "高级",
+  "Visible": "可视化",
   "DOMAIN": "匹配完整域名",
   "DOMAIN-SUFFIX": "匹配域名后缀",
   "DOMAIN-KEYWORD": "匹配域名关键字",