diff --git a/src/components/setting/mods/clash-field-viewer.tsx b/src/components/setting/mods/clash-field-viewer.tsx new file mode 100644 index 0000000..e91dcb4 --- /dev/null +++ b/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([]); + + 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 ( + setOpen(false)}> + {t("Clash Field")} + + + {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 ( + + handleChange(item)} + /> + {item} + + {inConfigUse && !inValid && } + {!inSelect && inConfig && } + + ); + })} + + + + {handleFields.map((item) => ( + + + {item} + + ))} + + + + + + + + ); +}; + +function WarnIcon() { + return ( + + + + ); +} + +function InfoIcon() { + return ( + + + + ); +} + +export default ClashFieldViewer; diff --git a/src/components/setting/setting-clash.tsx b/src/components/setting/setting-clash.tsx index 0f24891..28625e1 100644 --- a/src/components/setting/setting-clash.tsx +++ b/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) => { @@ -73,6 +75,7 @@ const SettingClash = ({ onError }: Props) => { return ( + { - - webUIHandler.current.open()} - > - - - - { + + webUIHandler.current.open()} + > + + + + + + fieldHandler.current.open()} + > + + + + }> {clashVer} diff --git a/src/locales/zh.json b/src/locales/zh.json index 10bac9d..b8f2c08 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -59,6 +59,7 @@ "theme.light": "浅色", "theme.dark": "深色", "theme.system": "系统", + "Clash Field": "Clash 字段", "Back": "返回", "Save": "保存", diff --git a/src/services/enhance.ts b/src/services/enhance.ts index 6c1c7c6..484164f 100644 --- a/src/services/enhance.ts +++ b/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; private resultMap: Map; + // 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) { 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) => { diff --git a/src/utils/ignore-case.ts b/src/utils/ignore-case.ts index aadf22e..f6533f6 100644 --- a/src/utils/ignore-case.ts +++ b/src/utils/ignore-case.ts @@ -2,7 +2,7 @@ type TData = Record; export default function ignoreCase(data: TData): TData { - if (!data) return data; + if (!data) return {}; const newData = {} as TData;