From 8bce2ce040069df05ec65a586007e430dc7faa58 Mon Sep 17 00:00:00 2001
From: GyDi <segydi@foxmail.com>
Date: Sun, 17 Jul 2022 16:02:17 +0800
Subject: [PATCH] feat: theme mode support follows system

---
 src/components/layout/use-custom-theme.ts    | 24 ++++++++--
 src/components/profile/file-editor.tsx       | 14 ++----
 src/components/setting/palette-switch.tsx    |  1 +
 src/components/setting/setting-verge.tsx     | 48 +++++++++-----------
 src/components/setting/theme-mode-switch.tsx | 34 ++++++++++++++
 src/locales/en.json                          |  5 +-
 src/locales/zh.json                          |  7 ++-
 src/services/states.ts                       |  5 ++
 src/services/types.ts                        |  2 +-
 9 files changed, 97 insertions(+), 43 deletions(-)
 create mode 100644 src/components/setting/theme-mode-switch.tsx

diff --git a/src/components/layout/use-custom-theme.ts b/src/components/layout/use-custom-theme.ts
index 7ddd7d3..2aa07a6 100644
--- a/src/components/layout/use-custom-theme.ts
+++ b/src/components/layout/use-custom-theme.ts
@@ -1,7 +1,10 @@
 import useSWR from "swr";
-import { useMemo } from "react";
+import { useEffect, useMemo } from "react";
+import { useRecoilState } from "recoil";
 import { createTheme } from "@mui/material";
+import { appWindow } from "@tauri-apps/api/window";
 import { getVergeConfig } from "../../services/cmds";
+import { atomThemeMode } from "../../services/states";
 import { defaultTheme, defaultDarkTheme } from "../../pages/_theme";
 
 /**
@@ -10,10 +13,23 @@ import { defaultTheme, defaultDarkTheme } from "../../pages/_theme";
 export default function useCustomTheme() {
   const { data } = useSWR("getVergeConfig", getVergeConfig);
   const { theme_mode, theme_setting } = data ?? {};
+  const [mode, setMode] = useRecoilState(atomThemeMode);
 
-  const theme = useMemo(() => {
-    const mode = theme_mode ?? "light";
+  useEffect(() => {
+    if (theme_mode !== "system") {
+      setMode(theme_mode ?? "light");
+      return;
+    }
+
+    appWindow.theme().then((m) => m && setMode(m));
+    const unlisten = appWindow.onThemeChanged((e) => setMode(e.payload));
 
+    return () => {
+      unlisten.then((fn) => fn());
+    };
+  }, [theme_mode]);
+
+  const theme = useMemo(() => {
     const setting = theme_setting || {};
     const dt = mode === "light" ? defaultTheme : defaultDarkTheme;
 
@@ -78,7 +94,7 @@ export default function useCustomTheme() {
     }, 0);
 
     return theme;
-  }, [theme_mode, theme_setting]);
+  }, [mode, theme_setting]);
 
   return { theme };
 }
diff --git a/src/components/profile/file-editor.tsx b/src/components/profile/file-editor.tsx
index 2b36b62..f7125ba 100644
--- a/src/components/profile/file-editor.tsx
+++ b/src/components/profile/file-editor.tsx
@@ -1,6 +1,6 @@
-import useSWR from "swr";
 import { useEffect, useRef } from "react";
 import { useLockFn } from "ahooks";
+import { useRecoilValue } from "recoil";
 import { useTranslation } from "react-i18next";
 import {
   Button,
@@ -9,11 +9,8 @@ import {
   DialogContent,
   DialogTitle,
 } from "@mui/material";
-import {
-  getVergeConfig,
-  readProfileFile,
-  saveProfileFile,
-} from "../../services/cmds";
+import { atomThemeMode } from "../../services/states";
+import { readProfileFile, saveProfileFile } from "../../services/cmds";
 import Notice from "../base/base-notice";
 
 import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js";
@@ -35,8 +32,7 @@ const FileEditor = (props: Props) => {
   const { t } = useTranslation();
   const editorRef = useRef<any>();
   const instanceRef = useRef<editor.IStandaloneCodeEditor | null>(null);
-  const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig);
-  const { theme_mode } = vergeConfig ?? {};
+  const themeMode = useRecoilValue(atomThemeMode);
 
   useEffect(() => {
     if (!open) return;
@@ -50,7 +46,7 @@ const FileEditor = (props: Props) => {
       instanceRef.current = editor.create(editorRef.current, {
         value: data,
         language: mode,
-        theme: theme_mode === "light" ? "vs" : "vs-dark",
+        theme: themeMode === "light" ? "vs" : "vs-dark",
         minimap: { enabled: false },
       });
     });
diff --git a/src/components/setting/palette-switch.tsx b/src/components/setting/palette-switch.tsx
index 49b37f7..9f9c6b4 100644
--- a/src/components/setting/palette-switch.tsx
+++ b/src/components/setting/palette-switch.tsx
@@ -1,5 +1,6 @@
 import { styled, Switch } from "@mui/material";
 
+// todo: deprecated
 // From: https://mui.com/components/switches/
 const PaletteSwitch = styled(Switch)(({ theme }) => ({
   width: 62,
diff --git a/src/components/setting/setting-verge.tsx b/src/components/setting/setting-verge.tsx
index 2241b96..f5f12cd 100644
--- a/src/components/setting/setting-verge.tsx
+++ b/src/components/setting/setting-verge.tsx
@@ -19,7 +19,7 @@ import { ArrowForward } from "@mui/icons-material";
 import { SettingList, SettingItem } from "./setting";
 import { CmdType } from "../../services/types";
 import { version } from "../../../package.json";
-import PaletteSwitch from "./palette-switch";
+import ThemeModeSwitch from "./theme-mode-switch";
 import GuardState from "./guard-state";
 import SettingTheme from "./setting-theme";
 
@@ -43,19 +43,31 @@ const SettingVerge = ({ onError }: Props) => {
 
   return (
     <SettingList title={t("Verge Setting")}>
+      <SettingItem>
+        <ListItemText primary={t("Language")} />
+        <GuardState
+          value={language ?? "en"}
+          onCatch={onError}
+          onFormat={(e: any) => e.target.value}
+          onChange={(e) => onChangeData({ language: e })}
+          onGuard={(e) => patchVergeConfig({ language: e })}
+        >
+          <Select size="small" sx={{ width: 100 }}>
+            <MenuItem value="zh">中文</MenuItem>
+            <MenuItem value="en">English</MenuItem>
+          </Select>
+        </GuardState>
+      </SettingItem>
+
       <SettingItem>
         <ListItemText primary={t("Theme Mode")} />
         <GuardState
-          value={theme_mode === "dark"}
-          valueProps="checked"
+          value={theme_mode}
           onCatch={onError}
-          onFormat={onSwitchFormat}
-          onChange={(e) => onChangeData({ theme_mode: e ? "dark" : "light" })}
-          onGuard={(e) =>
-            patchVergeConfig({ theme_mode: e ? "dark" : "light" })
-          }
+          onChange={(e) => onChangeData({ theme_mode: e })}
+          onGuard={(e) => patchVergeConfig({ theme_mode: e })}
         >
-          <PaletteSwitch edge="end" />
+          <ThemeModeSwitch />
         </GuardState>
       </SettingItem>
 
@@ -87,22 +99,6 @@ const SettingVerge = ({ onError }: Props) => {
         </GuardState>
       </SettingItem>
 
-      <SettingItem>
-        <ListItemText primary={t("Language")} />
-        <GuardState
-          value={language ?? "en"}
-          onCatch={onError}
-          onFormat={(e: any) => e.target.value}
-          onChange={(e) => onChangeData({ language: e })}
-          onGuard={(e) => patchVergeConfig({ language: e })}
-        >
-          <Select size="small" sx={{ width: 100 }}>
-            <MenuItem value="zh">中文</MenuItem>
-            <MenuItem value="en">English</MenuItem>
-          </Select>
-        </GuardState>
-      </SettingItem>
-
       <SettingItem>
         <ListItemText primary={t("Theme Setting")} />
         <IconButton
@@ -129,7 +125,7 @@ const SettingVerge = ({ onError }: Props) => {
       </SettingItem>
 
       <SettingItem>
-        <ListItemText primary={t("Version")} />
+        <ListItemText primary={t("Verge Version")} />
         <Typography sx={{ py: "6px" }}>v{version}</Typography>
       </SettingItem>
 
diff --git a/src/components/setting/theme-mode-switch.tsx b/src/components/setting/theme-mode-switch.tsx
new file mode 100644
index 0000000..994cdd1
--- /dev/null
+++ b/src/components/setting/theme-mode-switch.tsx
@@ -0,0 +1,34 @@
+import { useTranslation } from "react-i18next";
+import { Button, ButtonGroup } from "@mui/material";
+import { CmdType } from "../../services/types";
+
+type ThemeValue = CmdType.VergeConfig["theme_mode"];
+
+interface Props {
+  value?: ThemeValue;
+  onChange?: (value: ThemeValue) => void;
+}
+
+const ThemeModeSwitch = (props: Props) => {
+  const { value, onChange } = props;
+  const { t } = useTranslation();
+
+  const modes = ["light", "dark", "system"] as const;
+
+  return (
+    <ButtonGroup size="small">
+      {modes.map((mode) => (
+        <Button
+          key={mode}
+          variant={mode === value ? "contained" : "outlined"}
+          onClick={() => onChange?.(mode)}
+          sx={{ textTransform: "capitalize" }}
+        >
+          {t(`theme.${mode}`)}
+        </Button>
+      ))}
+    </ButtonGroup>
+  );
+};
+
+export default ThemeModeSwitch;
diff --git a/src/locales/en.json b/src/locales/en.json
index 87ac308..7b75b85 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -55,7 +55,10 @@
   "Language": "Language",
   "Open App Dir": "Open App Dir",
   "Open Logs Dir": "Open Logs Dir",
-  "Version": "Version",
+  "Verge Version": "Verge Version",
+  "theme.light": "Light",
+  "theme.dark": "Dark",
+  "theme.system": "System",
 
   "Save": "Save",
   "Cancel": "Cancel"
diff --git a/src/locales/zh.json b/src/locales/zh.json
index bd6f7b2..ea560ae 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -48,14 +48,17 @@
   "System Proxy": "系统代理",
   "Proxy Guard": "系统代理守卫",
   "Proxy Bypass": "Proxy Bypass",
-  "Theme Mode": "暗夜模式",
+  "Theme Mode": "主题模式",
   "Theme Blur": "背景模糊",
   "Theme Setting": "主题设置",
   "Traffic Graph": "流量图显",
   "Language": "语言设置",
   "Open App Dir": "应用目录",
   "Open Logs Dir": "日志目录",
-  "Version": "版本",
+  "Verge Version": "应用版本",
+  "theme.light": "浅色",
+  "theme.dark": "深色",
+  "theme.system": "系统",
 
   "Save": "保存",
   "Cancel": "取消"
diff --git a/src/services/states.ts b/src/services/states.ts
index 015f484..6d7ebea 100644
--- a/src/services/states.ts
+++ b/src/services/states.ts
@@ -1,6 +1,11 @@
 import { atom } from "recoil";
 import { ApiType } from "./types";
 
+export const atomThemeMode = atom<"light" | "dark">({
+  key: "atomThemeMode",
+  default: "light",
+});
+
 export const atomClashPort = atom<number>({
   key: "atomClashPort",
   default: 0,
diff --git a/src/services/types.ts b/src/services/types.ts
index 44b8e7b..2edea25 100644
--- a/src/services/types.ts
+++ b/src/services/types.ts
@@ -126,7 +126,7 @@ export namespace CmdType {
   export interface VergeConfig {
     language?: string;
     clash_core?: string;
-    theme_mode?: "light" | "dark";
+    theme_mode?: "light" | "dark" | "system";
     theme_blur?: boolean;
     traffic_graph?: boolean;
     enable_tun_mode?: boolean;
-- 
GitLab