From e66a89208d1041a9a3eebc62cabe891a92dba4dc Mon Sep 17 00:00:00 2001
From: GyDi <zzzgydi@gmail.com>
Date: Sun, 6 Nov 2022 23:23:26 +0800
Subject: [PATCH] feat: support to change external controller

---
 src-tauri/src/core/mod.rs                     |  17 +--
 .../setting/mods/controller-viewer.tsx        | 101 ++++++++++++++++++
 src/components/setting/setting-clash.tsx      |  16 ++-
 src/locales/zh.json                           |   1 +
 src/services/types.d.ts                       |   6 +-
 5 files changed, 131 insertions(+), 10 deletions(-)
 create mode 100644 src/components/setting/mods/controller-viewer.tsx

diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs
index e0133b4..0a419bd 100644
--- a/src-tauri/src/core/mod.rs
+++ b/src-tauri/src/core/mod.rs
@@ -112,18 +112,21 @@ impl Core {
   /// Patch Clash
   /// handle the clash config changed
   pub fn patch_clash(&self, patch: Mapping) -> Result<()> {
-    let has_port = patch.contains_key(&Value::from("mixed-port"));
-    let has_mode = patch.contains_key(&Value::from("mode"));
+    let patch_cloned = patch.clone();
+    let clash_mode = patch.get("mode");
+    let mixed_port = patch.get("mixed-port");
+    let external = patch.get("external-controller");
+    let secret = patch.get("secret");
 
-    let port = {
+    let valid_port = {
       let global = Data::global();
       let mut clash = global.clash.lock();
-      clash.patch_config(patch)?;
-      clash.info.port.clone()
+      clash.patch_config(patch_cloned)?;
+      clash.info.port.is_some()
     };
 
     // todo: port check
-    if has_port && port.is_some() {
+    if (mixed_port.is_some() && valid_port) || external.is_some() || secret.is_some() {
       let mut service = self.service.lock();
       service.restart()?;
       drop(service);
@@ -134,7 +137,7 @@ impl Core {
       sysopt.init_sysproxy()?;
     }
 
-    if has_mode {
+    if clash_mode.is_some() {
       let handle = self.handle.lock();
       handle.update_systray_part()?;
     }
diff --git a/src/components/setting/mods/controller-viewer.tsx b/src/components/setting/mods/controller-viewer.tsx
new file mode 100644
index 0000000..ec4c58a
--- /dev/null
+++ b/src/components/setting/mods/controller-viewer.tsx
@@ -0,0 +1,101 @@
+import useSWR from "swr";
+import { useState } from "react";
+import { useLockFn } from "ahooks";
+import { useTranslation } from "react-i18next";
+import {
+  Button,
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogTitle,
+  List,
+  ListItem,
+  ListItemText,
+  TextField,
+} from "@mui/material";
+import { getClashInfo, patchClashConfig } from "@/services/cmds";
+import { ModalHandler } from "@/hooks/use-modal-handler";
+import { getAxios } from "@/services/api";
+import Notice from "@/components/base/base-notice";
+
+interface Props {
+  handler: ModalHandler;
+}
+
+const ControllerViewer = ({ handler }: Props) => {
+  const { t } = useTranslation();
+  const [open, setOpen] = useState(false);
+
+  const { data: clashInfo, mutate } = useSWR("getClashInfo", getClashInfo);
+  const [controller, setController] = useState(clashInfo?.server || "");
+  const [secret, setSecret] = useState(clashInfo?.secret || "");
+
+  if (handler) {
+    handler.current = {
+      open: () => {
+        setOpen(true);
+        setController(clashInfo?.server || "");
+        setSecret(clashInfo?.secret || "");
+      },
+      close: () => setOpen(false),
+    };
+  }
+
+  const onSave = useLockFn(async () => {
+    try {
+      await patchClashConfig({ "external-controller": controller, secret });
+      mutate();
+      // 刷新接口
+      getAxios(true);
+      Notice.success("Change Clash Config successfully!", 1000);
+      setOpen(false);
+    } catch (err) {
+      console.log(err);
+    }
+  });
+
+  return (
+    <Dialog open={open} onClose={() => setOpen(false)}>
+      <DialogTitle>{t("Clash Port")}</DialogTitle>
+
+      <DialogContent sx={{ width: 400 }}>
+        <List>
+          <ListItem sx={{ padding: "5px 2px" }}>
+            <ListItemText primary="External Controller" />
+            <TextField
+              size="small"
+              autoComplete="off"
+              sx={{ width: 175 }}
+              value={controller}
+              placeholder="Required"
+              onChange={(e) => setController(e.target.value)}
+            />
+          </ListItem>
+
+          <ListItem sx={{ padding: "5px 2px" }}>
+            <ListItemText primary="Core Secret" />
+            <TextField
+              size="small"
+              autoComplete="off"
+              sx={{ width: 175 }}
+              value={secret}
+              placeholder="Recommanded"
+              onChange={(e) => setSecret(e.target.value)}
+            />
+          </ListItem>
+        </List>
+      </DialogContent>
+
+      <DialogActions>
+        <Button variant="outlined" onClick={() => setOpen(false)}>
+          {t("Cancel")}
+        </Button>
+        <Button onClick={onSave} variant="contained">
+          {t("Save")}
+        </Button>
+      </DialogActions>
+    </Dialog>
+  );
+};
+
+export default ControllerViewer;
diff --git a/src/components/setting/setting-clash.tsx b/src/components/setting/setting-clash.tsx
index 3ba8cd1..8ddad18 100644
--- a/src/components/setting/setting-clash.tsx
+++ b/src/components/setting/setting-clash.tsx
@@ -18,6 +18,7 @@ import CoreSwitch from "./mods/core-switch";
 import WebUIViewer from "./mods/web-ui-viewer";
 import ClashFieldViewer from "./mods/clash-field-viewer";
 import ClashPortViewer from "./mods/clash-port-viewer";
+import ControllerViewer from "./mods/controller-viewer";
 
 interface Props {
   onError: (err: Error) => void;
@@ -42,6 +43,7 @@ const SettingClash = ({ onError }: Props) => {
   const webUIHandler = useModalHandler();
   const fieldHandler = useModalHandler();
   const portHandler = useModalHandler();
+  const controllerHandler = useModalHandler();
 
   const onSwitchFormat = (_e: any, value: boolean) => value;
   const onChangeData = (patch: Partial<ApiType.ConfigData>) => {
@@ -62,6 +64,7 @@ const SettingClash = ({ onError }: Props) => {
       <WebUIViewer handler={webUIHandler} onError={onError} />
       <ClashFieldViewer handler={fieldHandler} />
       <ClashPortViewer handler={portHandler} />
+      <ControllerViewer handler={controllerHandler} />
 
       <SettingItem label={t("Allow Lan")}>
         <GuardState
@@ -113,7 +116,7 @@ const SettingClash = ({ onError }: Props) => {
           autoComplete="off"
           size="small"
           value={mixedPort ?? 0}
-          sx={{ width: 100, input: { py: "7.5px" } }}
+          sx={{ width: 100, input: { py: "7.5px", cursor: "pointer" } }}
           onClick={(e) => {
             portHandler.current.open();
             (e.target as any).blur();
@@ -121,6 +124,17 @@ const SettingClash = ({ onError }: Props) => {
         />
       </SettingItem>
 
+      <SettingItem label={t("External Controller")}>
+        <IconButton
+          color="inherit"
+          size="small"
+          sx={{ my: "2px" }}
+          onClick={() => controllerHandler.current.open()}
+        >
+          <ArrowForward />
+        </IconButton>
+      </SettingItem>
+
       <SettingItem label={t("Web UI")}>
         <IconButton
           color="inherit"
diff --git a/src/locales/zh.json b/src/locales/zh.json
index d511354..a1fac5e 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -60,6 +60,7 @@
   "IPv6": "IPv6",
   "Log Level": "日志等级",
   "Mixed Port": "端口设置",
+  "External Controller": "外部控制",
   "Clash Core": "Clash 内核",
   "Tun Mode": "Tun 模式",
   "Service Mode": "服务模式",
diff --git a/src/services/types.d.ts b/src/services/types.d.ts
index e537af4..804d343 100644
--- a/src/services/types.d.ts
+++ b/src/services/types.d.ts
@@ -13,6 +13,8 @@ declare namespace ApiType {
     "redir-port": number;
     "socks-port": number;
     "tproxy-port": number;
+    "external-controller": string;
+    secret: string;
   }
 
   interface RuleItem {
@@ -95,8 +97,8 @@ declare namespace CmdType {
 
   interface ClashInfo {
     status: string;
-    port?: string;
-    server?: string;
+    port?: string; // clash mixed port
+    server?: string; // external-controller
     secret?: string;
   }
 
-- 
GitLab