From 6b368953f42208224440ce6d0b130d48403e58bb Mon Sep 17 00:00:00 2001
From: GyDi <segydi@foxmail.com>
Date: Mon, 25 Apr 2022 16:12:04 +0800
Subject: [PATCH] feat: windows service mode ui

---
 src-tauri/src/cmds.rs                     |  14 ++-
 src-tauri/src/core/service.rs             |  33 +++++--
 src-tauri/src/main.rs                     |   1 +
 src/components/setting/service-mode.tsx   | 104 ++++++++++++++++++++++
 src/components/setting/setting-system.tsx |  81 ++++++++++++++++-
 src/locales/en.json                       |   1 +
 src/locales/zh.json                       |   1 +
 src/services/cmds.ts                      |  13 ++-
 8 files changed, 234 insertions(+), 14 deletions(-)
 create mode 100644 src/components/setting/service-mode.tsx

diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs
index 55d37f3..d4ca786 100644
--- a/src-tauri/src/cmds.rs
+++ b/src-tauri/src/cmds.rs
@@ -251,6 +251,11 @@ pub mod service {
     wrap_err!(crate::core::Service::start_service().await)
   }
 
+  #[tauri::command]
+  pub async fn stop_service() -> Result<(), String> {
+    wrap_err!(crate::core::Service::stop_service().await)
+  }
+
   #[tauri::command]
   pub async fn check_service() -> Result<JsonResponse, String> {
     wrap_err!(crate::core::Service::check_service().await)
@@ -258,11 +263,13 @@ pub mod service {
 
   #[tauri::command]
   pub async fn install_service() -> Result<(), String> {
-    wrap_err!(crate::core::Service::install_service().await)
+    wrap_err!(crate::core::Service::install_service().await)?;
+    wrap_err!(crate::core::Service::start_service().await)
   }
 
   #[tauri::command]
   pub async fn uninstall_service() -> Result<(), String> {
+    log_if_err!(crate::core::Service::stop_service().await);
     wrap_err!(crate::core::Service::uninstall_service().await)
   }
 }
@@ -276,6 +283,11 @@ pub mod service {
     Ok(())
   }
 
+  #[tauri::command]
+  pub async fn stop_service() -> Result<(), String> {
+    Ok(())
+  }
+
   #[tauri::command]
   pub async fn check_service() -> Result<(), String> {
     Ok(())
diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs
index 798bee7..62b10ef 100644
--- a/src-tauri/src/core/service.rs
+++ b/src-tauri/src/core/service.rs
@@ -294,15 +294,32 @@ pub mod win_service {
       }
     }
 
+    /// stop service
+    pub async fn stop_service() -> Result<()> {
+      let url = format!("{SERVICE_URL}/stop_service");
+      let res = reqwest::Client::new()
+        .post(url)
+        .send()
+        .await?
+        .json::<JsonResponse>()
+        .await
+        .context("failed to connect to the Clash Verge Service")?;
+
+      if res.code != 0 {
+        bail!(res.msg);
+      }
+
+      Ok(())
+    }
+
     /// check the windows service status
     pub async fn check_service() -> Result<JsonResponse> {
       let url = format!("{SERVICE_URL}/get_clash");
       let response = reqwest::get(url)
-        .await
-        .context("failed to connect to the Clash Verge Service")?
+        .await?
         .json::<JsonResponse>()
         .await
-        .context("failed to parse the Clash Verge Service response")?;
+        .context("failed to connect to the Clash Verge Service")?;
 
       Ok(response)
     }
@@ -335,11 +352,10 @@ pub mod win_service {
         .post(url)
         .json(&map)
         .send()
-        .await
-        .context("failed to connect to the Clash Verge Service")?
+        .await?
         .json::<JsonResponse>()
         .await
-        .context("failed to parse the Clash Verge Service response")?;
+        .context("failed to connect to the Clash Verge Service")?;
 
       if res.code != 0 {
         bail!(res.msg);
@@ -354,11 +370,10 @@ pub mod win_service {
       let res = reqwest::Client::new()
         .post(url)
         .send()
-        .await
-        .context("failed to connect to the Clash Verge Service")?
+        .await?
         .json::<JsonResponse>()
         .await
-        .context("failed to parse the Clash Verge Service response")?;
+        .context("failed to connect to the Clash Verge Service")?;
 
       if res.code != 0 {
         bail!(res.msg);
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index 11b9b54..8f17feb 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -129,6 +129,7 @@ fn main() -> std::io::Result<()> {
       cmds::save_profile_file,
       // service mode
       cmds::service::start_service,
+      cmds::service::stop_service,
       cmds::service::check_service,
       cmds::service::install_service,
       cmds::service::uninstall_service,
diff --git a/src/components/setting/service-mode.tsx b/src/components/setting/service-mode.tsx
new file mode 100644
index 0000000..768af80
--- /dev/null
+++ b/src/components/setting/service-mode.tsx
@@ -0,0 +1,104 @@
+import useSWR, { useSWRConfig } from "swr";
+import { useLockFn } from "ahooks";
+import { useTranslation } from "react-i18next";
+import {
+  Button,
+  Dialog,
+  DialogContent,
+  DialogTitle,
+  Stack,
+  Typography,
+} from "@mui/material";
+import {
+  checkService,
+  installService,
+  uninstallService,
+  patchVergeConfig,
+} from "../../services/cmds";
+import Notice from "../base/base-notice";
+import noop from "../../utils/noop";
+
+interface Props {
+  open: boolean;
+  enable: boolean;
+  onClose: () => void;
+  onError?: (err: Error) => void;
+}
+
+const ServiceMode = (props: Props) => {
+  const { open, enable, onClose, onError = noop } = props;
+
+  const { t } = useTranslation();
+  const { mutate } = useSWRConfig();
+  const { data: status } = useSWR("checkService", checkService, {
+    revalidateIfStale: true,
+    shouldRetryOnError: false,
+  });
+
+  const state = status != null ? status : "pending";
+
+  const onInstall = useLockFn(async () => {
+    try {
+      await installService();
+      mutate("checkService");
+      Notice.success("Service installed successfully");
+      onClose();
+    } catch (err: any) {
+      mutate("checkService");
+      onError(err);
+    }
+  });
+
+  const onUninstall = useLockFn(async () => {
+    try {
+      if (state === "active" && enable) {
+        await patchVergeConfig({ enable_service_mode: false });
+      }
+
+      await uninstallService();
+      Notice.success("Service uninstalled successfully");
+      mutate("checkService");
+      onClose();
+    } catch (err: any) {
+      mutate("checkService");
+      onError(err);
+    }
+  });
+
+  return (
+    <Dialog open={open} onClose={onClose}>
+      <DialogTitle>{t("Service Mode")}</DialogTitle>
+
+      <DialogContent sx={{ width: 360, userSelect: "text" }}>
+        <Typography>Current State: {state}</Typography>
+
+        {(state === "unknown" || state === "uninstall") && (
+          <Typography>
+            Infomation: Please make sure the Clash Verge Service is installed
+            and enabled
+          </Typography>
+        )}
+
+        <Stack
+          direction="row"
+          spacing={1}
+          sx={{ mt: 4, justifyContent: "flex-end" }}
+        >
+          {state === "uninstall" && (
+            <Button variant="contained" onClick={onInstall}>
+              Install
+            </Button>
+          )}
+
+          {(state === "active" || state === "installed") && (
+            <Button variant="outlined" onClick={onUninstall}>
+              Uninstall
+            </Button>
+          )}
+        </Stack>
+      </DialogContent>
+    </Dialog>
+  );
+};
+
+export default ServiceMode;
diff --git a/src/components/setting/setting-system.tsx b/src/components/setting/setting-system.tsx
index be49d64..bd3b993 100644
--- a/src/components/setting/setting-system.tsx
+++ b/src/components/setting/setting-system.tsx
@@ -1,24 +1,49 @@
 import useSWR, { useSWRConfig } from "swr";
+import { useState } from "react";
 import { useTranslation } from "react-i18next";
-import { Box, ListItemText, Switch, TextField } from "@mui/material";
-import { getVergeConfig, patchVergeConfig } from "../../services/cmds";
+import {
+  Box,
+  IconButton,
+  ListItemText,
+  Switch,
+  TextField,
+} from "@mui/material";
+import { ArrowForward, PrivacyTipRounded } from "@mui/icons-material";
+import {
+  checkService,
+  getVergeConfig,
+  patchVergeConfig,
+} from "../../services/cmds";
 import { SettingList, SettingItem } from "./setting";
 import { CmdType } from "../../services/types";
 import GuardState from "./guard-state";
+import ServiceMode from "./service-mode";
 import SysproxyTooltip from "./sysproxy-tooltip";
+import getSystem from "../../utils/get-system";
 
 interface Props {
   onError?: (err: Error) => void;
 }
 
+const isWIN = getSystem() === "windows";
+
 const SettingSystem = ({ onError }: Props) => {
   const { t } = useTranslation();
   const { mutate } = useSWRConfig();
   const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig);
 
+  // service mode
+  const [serviceOpen, setServiceOpen] = useState(false);
+  const { data: serviceStatus } = useSWR(
+    isWIN ? "checkService" : null,
+    checkService,
+    { revalidateIfStale: true, shouldRetryOnError: false }
+  );
+
   const {
     enable_tun_mode,
     enable_auto_launch,
+    enable_service_mode,
     enable_silent_start,
     enable_system_proxy,
     system_proxy_bypass,
@@ -46,6 +71,56 @@ const SettingSystem = ({ onError }: Props) => {
         </GuardState>
       </SettingItem>
 
+      {isWIN && (
+        <SettingItem>
+          <ListItemText
+            primary={
+              <Box sx={{ display: "flex", alignItems: "center" }}>
+                <span style={{ marginRight: 4 }}>{t("Service Mode")}</span>
+
+                {(serviceStatus === "active" ||
+                  serviceStatus === "installed") && (
+                  <PrivacyTipRounded
+                    fontSize="small"
+                    onClick={() => setServiceOpen(true)}
+                  />
+                )}
+              </Box>
+            }
+          />
+
+          {serviceStatus === "active" || serviceStatus === "installed" ? (
+            <GuardState
+              value={enable_service_mode ?? false}
+              valueProps="checked"
+              onCatch={onError}
+              onFormat={onSwitchFormat}
+              onChange={(e) => onChangeData({ enable_service_mode: e })}
+              onGuard={(e) => patchVergeConfig({ enable_service_mode: e })}
+            >
+              <Switch edge="end" />
+            </GuardState>
+          ) : (
+            <IconButton
+              color="inherit"
+              size="small"
+              onClick={() => setServiceOpen(true)}
+            >
+              <ArrowForward />
+            </IconButton>
+          )}
+
+          {serviceOpen && (
+            <ServiceMode
+              open={serviceOpen}
+              enable={!!enable_service_mode}
+              onError={onError}
+              onClose={() => setServiceOpen(false)}
+            />
+          )}
+        </SettingItem>
+      )}
+
       <SettingItem>
         <ListItemText primary={t("Auto Launch")} />
         <GuardState
@@ -78,7 +153,7 @@ const SettingSystem = ({ onError }: Props) => {
         <ListItemText
           primary={
             <Box sx={{ display: "flex", alignItems: "center" }}>
-              {t("System Proxy")}
+              <span style={{ marginRight: 4 }}>{t("System Proxy")}</span>
               <SysproxyTooltip />
             </Box>
           }
diff --git a/src/locales/en.json b/src/locales/en.json
index 7aff3f9..7d1b06b 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -41,6 +41,7 @@
   "Mixed Port": "Mixed Port",
   "Clash core": "Clash core",
   "Tun Mode": "Tun Mode",
+  "Service Mode": "Service Mode",
   "Auto Launch": "Auto Launch",
   "Silent Start": "Silent Start",
   "System Proxy": "System Proxy",
diff --git a/src/locales/zh.json b/src/locales/zh.json
index d691a8f..8894cfb 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -41,6 +41,7 @@
   "Mixed Port": "端口设置",
   "Clash core": "Clash 内核",
   "Tun Mode": "Tun 模式",
+  "Service Mode": "服务模式",
   "Auto Launch": "开机自启",
   "Silent Start": "静默启动",
   "System Proxy": "系统代理",
diff --git a/src/services/cmds.ts b/src/services/cmds.ts
index 28619a6..6ef0645 100644
--- a/src/services/cmds.ts
+++ b/src/services/cmds.ts
@@ -112,8 +112,19 @@ export async function startService() {
   return invoke<void>("start_service");
 }
 
+export async function stopService() {
+  return invoke<void>("stop_service");
+}
+
 export async function checkService() {
-  return invoke<any>("check_service");
+  try {
+    const result = await invoke<any>("check_service");
+    if (result?.code === 0) return "active";
+    if (result?.code === 400) return "installed";
+    return "unknown";
+  } catch (err: any) {
+    return "uninstall";
+  }
 }
 
 export async function installService() {
-- 
GitLab