Ver Fonte

feat: clash field viewer wip

GyDi há 2 anos atrás
pai
commit
066b08040a

+ 169 - 0
src/components/setting/mods/clash-field-viewer.tsx

@@ -0,0 +1,169 @@
+import useSWR from "swr";
+import { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import {
+  Button,
+  Checkbox,
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogTitle,
+  Divider,
+  Stack,
+  Tooltip,
+  Typography,
+} from "@mui/material";
+import { changeProfileValid, getProfiles } from "@/services/cmds";
+import { ModalHandler } from "@/hooks/use-modal-handler";
+import enhance, {
+  DEFAULT_FIELDS,
+  HANDLE_FIELDS,
+  USE_FLAG_FIELDS,
+} from "@/services/enhance";
+import { BuildCircleRounded, InfoRounded } from "@mui/icons-material";
+
+interface Props {
+  handler: ModalHandler;
+  onError: (err: Error) => void;
+}
+
+const fieldSorter = (a: string, b: string) => {
+  if (a.includes("-") === a.includes("-")) {
+    if (a.length === b.length) return a.localeCompare(b);
+    return a.length - b.length;
+  } else if (a.includes("-")) return 1;
+  else if (b.includes("-")) return -1;
+  return 0;
+};
+
+const useFields = [...USE_FLAG_FIELDS].sort(fieldSorter);
+const handleFields = [...HANDLE_FIELDS, ...DEFAULT_FIELDS].sort(fieldSorter);
+
+const ClashFieldViewer = ({ handler, onError }: Props) => {
+  const { t } = useTranslation();
+
+  const { data, mutate } = useSWR("getProfiles", getProfiles);
+
+  const [open, setOpen] = useState(false);
+  const [selected, setSelected] = useState<string[]>([]);
+
+  const { config: enhanceConfig, use: enhanceUse } = enhance.getFieldsState();
+
+  if (handler) {
+    handler.current = {
+      open: () => setOpen(true),
+      close: () => setOpen(false),
+    };
+  }
+
+  console.log("render");
+
+  useEffect(() => {
+    if (open) mutate();
+  }, [open]);
+
+  useEffect(() => {
+    setSelected([...(data?.valid || []), ...enhanceUse]);
+  }, [data?.valid, enhanceUse]);
+
+  const handleChange = (item: string) => {
+    if (!item) return;
+
+    setSelected((old) =>
+      old.includes(item) ? old.filter((e) => e !== item) : [...old, item]
+    );
+  };
+
+  const handleSave = async () => {
+    setOpen(false);
+
+    const oldSet = new Set([...(data?.valid || []), ...enhanceUse]);
+    const curSet = new Set(selected.concat([...oldSet]));
+
+    if (curSet.size === oldSet.size) return;
+
+    try {
+      await changeProfileValid([...new Set(selected)]);
+      mutate();
+    } catch (err: any) {
+      onError(err);
+    }
+  };
+
+  return (
+    <Dialog open={open} onClose={() => setOpen(false)}>
+      <DialogTitle>{t("Clash Field")}</DialogTitle>
+
+      <DialogContent
+        sx={{
+          pb: 0,
+          width: 320,
+          height: 300,
+          overflowY: "auto",
+          userSelect: "text",
+        }}
+      >
+        {useFields.map((item) => {
+          const inSelect = selected.includes(item);
+          const inConfig = enhanceConfig.includes(item);
+          const inConfigUse = enhanceUse.includes(item);
+          const inValid = data?.valid?.includes(item);
+
+          return (
+            <Stack key={item} mb={0.5} direction="row" alignItems="center">
+              <Checkbox
+                checked={inSelect}
+                size="small"
+                sx={{ p: 0.5 }}
+                onChange={() => handleChange(item)}
+              />
+              <Typography width="100%">{item}</Typography>
+
+              {inConfigUse && !inValid && <InfoIcon />}
+              {!inSelect && inConfig && <WarnIcon />}
+            </Stack>
+          );
+        })}
+
+        <Divider sx={{ my: 0.5 }} />
+
+        {handleFields.map((item) => (
+          <Stack key={item} mb={0.5} direction="row" alignItems="center">
+            <Checkbox defaultChecked disabled size="small" sx={{ p: 0.5 }} />
+            <Typography>{item}</Typography>
+          </Stack>
+        ))}
+      </DialogContent>
+
+      <DialogActions>
+        <Button variant="outlined" onClick={() => setOpen(false)}>
+          {t("Back")}
+        </Button>
+        <Button variant="contained" onClick={handleSave}>
+          {t("Save")}
+        </Button>
+      </DialogActions>
+    </Dialog>
+  );
+};
+
+function WarnIcon() {
+  return (
+    <Tooltip title="The field exists in the config but not enabled.">
+      <InfoRounded color="warning" sx={{ cursor: "pointer", opacity: 0.5 }} />
+    </Tooltip>
+  );
+}
+
+function InfoIcon() {
+  return (
+    <Tooltip title="This field is provided by Merge Profile.">
+      <BuildCircleRounded
+        color="info"
+        sx={{ cursor: "pointer", opacity: 0.5 }}
+      />
+    </Tooltip>
+  );
+}
+
+export default ClashFieldViewer;

+ 25 - 11
src/components/setting/setting-clash.tsx

@@ -19,6 +19,7 @@ import Notice from "../base/base-notice";
 import GuardState from "./mods/guard-state";
 import CoreSwitch from "./mods/core-switch";
 import WebUIViewer from "./mods/web-ui-viewer";
+import ClashFieldViewer from "./mods/clash-field-viewer";
 
 interface Props {
   onError: (err: Error) => void;
@@ -40,6 +41,7 @@ const SettingClash = ({ onError }: Props) => {
   const setGlobalClashPort = useSetRecoilState(atomClashPort);
 
   const webUIHandler = useModalHandler();
+  const fieldHandler = useModalHandler();
 
   const onSwitchFormat = (_e: any, value: boolean) => value;
   const onChangeData = (patch: Partial<ApiType.ConfigData>) => {
@@ -73,6 +75,7 @@ const SettingClash = ({ onError }: Props) => {
   return (
     <SettingList title={t("Clash Setting")}>
       <WebUIViewer handler={webUIHandler} onError={onError} />
+      <ClashFieldViewer handler={fieldHandler} onError={onError} />
 
       <SettingItem label={t("Allow Lan")}>
         <GuardState
@@ -100,17 +103,6 @@ const SettingClash = ({ onError }: Props) => {
         </GuardState>
       </SettingItem>
 
-      <SettingItem label={t("Web UI")}>
-        <IconButton
-          color="inherit"
-          size="small"
-          sx={{ my: "2px" }}
-          onClick={() => webUIHandler.current.open()}
-        >
-          <ArrowForward />
-        </IconButton>
-      </SettingItem>
-
       <SettingItem label={t("Log Level")}>
         <GuardState
           value={logLevel ?? "info"}
@@ -146,6 +138,28 @@ const SettingClash = ({ onError }: Props) => {
         </GuardState>
       </SettingItem>
 
+      <SettingItem label={t("Web UI")}>
+        <IconButton
+          color="inherit"
+          size="small"
+          sx={{ my: "2px" }}
+          onClick={() => webUIHandler.current.open()}
+        >
+          <ArrowForward />
+        </IconButton>
+      </SettingItem>
+
+      <SettingItem label={t("Clash Field")}>
+        <IconButton
+          color="inherit"
+          size="small"
+          sx={{ my: "2px" }}
+          onClick={() => fieldHandler.current.open()}
+        >
+          <ArrowForward />
+        </IconButton>
+      </SettingItem>
+
       <SettingItem label={t("Clash Core")} extra={<CoreSwitch />}>
         <Typography sx={{ py: "7px" }}>{clashVer}</Typography>
       </SettingItem>

+ 1 - 0
src/locales/zh.json

@@ -59,6 +59,7 @@
   "theme.light": "浅色",
   "theme.dark": "深色",
   "theme.system": "系统",
+  "Clash Field": "Clash 字段",
 
   "Back": "返回",
   "Save": "保存",

+ 16 - 0
src/services/enhance.ts

@@ -12,6 +12,7 @@ export const HANDLE_FIELDS = [
   "mode",
   "log-level",
   "ipv6",
+  "secret",
   "external-controller",
 ];
 
@@ -131,6 +132,12 @@ class Enhance {
   private listenMap: Map<string, EListener>;
   private resultMap: Map<string, EStatus>;
 
+  // record current config fields
+  private fieldsState = {
+    config: [] as string[],
+    use: [] as string[],
+  };
+
   constructor() {
     this.listenMap = new Map();
     this.resultMap = new Map();
@@ -148,6 +155,11 @@ class Enhance {
     return this.resultMap.get(uid);
   }
 
+  // get the running field state
+  getFieldsState() {
+    return this.fieldsState;
+  }
+
   async enhanceHandler(event: Event<unknown>) {
     const payload = event.payload as CmdType.EnhancedPayload;
 
@@ -220,6 +232,10 @@ class Enhance {
 
     pdata = ignoreCase(pdata);
 
+    // save the fields state
+    this.fieldsState.config = Object.keys(pdata);
+    this.fieldsState.use = [...useList];
+
     // filter the data
     const filterData: typeof pdata = {};
     Object.keys(pdata).forEach((key: any) => {

+ 1 - 1
src/utils/ignore-case.ts

@@ -2,7 +2,7 @@
 type TData = Record<string, any>;
 
 export default function ignoreCase(data: TData): TData {
-  if (!data) return data;
+  if (!data) return {};
 
   const newData = {} as TData;