From f4c7b17a87ae853ad67fdcea727e93266babf779 Mon Sep 17 00:00:00 2001
From: GyDi <zzzgydi@gmail.com>
Date: Sun, 10 Sep 2023 14:30:31 +0800
Subject: [PATCH] feat: add check for updates button, close #766

---
 src/components/layout/layout-control.tsx      |  4 +-
 src/components/layout/layout-item.tsx         |  4 +-
 src/components/layout/layout-traffic.tsx      |  4 +-
 src/components/layout/update-button.tsx       | 20 ++---
 src/components/layout/use-custom-theme.ts     |  4 +-
 .../mods/update-viewer.tsx}                   | 89 ++++++++-----------
 src/components/setting/setting-verge.tsx      | 40 ++++++++-
 src/locales/en.json                           |  1 +
 src/locales/zh.json                           |  1 +
 src/pages/_layout.tsx                         | 10 +--
 10 files changed, 93 insertions(+), 84 deletions(-)
 rename src/components/{layout/update-dialog.tsx => setting/mods/update-viewer.tsx} (53%)

diff --git a/src/components/layout/layout-control.tsx b/src/components/layout/layout-control.tsx
index 379527b..6ec12b9 100644
--- a/src/components/layout/layout-control.tsx
+++ b/src/components/layout/layout-control.tsx
@@ -6,7 +6,7 @@ import {
   HorizontalRuleRounded,
 } from "@mui/icons-material";
 
-const LayoutControl = () => {
+export const LayoutControl = () => {
   const minWidth = 40;
 
   return (
@@ -37,5 +37,3 @@ const LayoutControl = () => {
     </>
   );
 };
-
-export default LayoutControl;
diff --git a/src/components/layout/layout-item.tsx b/src/components/layout/layout-item.tsx
index 545b36e..5a33da2 100644
--- a/src/components/layout/layout-item.tsx
+++ b/src/components/layout/layout-item.tsx
@@ -2,7 +2,7 @@ import { alpha, ListItem, ListItemButton, ListItemText } from "@mui/material";
 import { useMatch, useResolvedPath, useNavigate } from "react-router-dom";
 import type { LinkProps } from "react-router-dom";
 
-const LayoutItem = (props: LinkProps) => {
+export const LayoutItem = (props: LinkProps) => {
   const { to, children } = props;
 
   const resolved = useResolvedPath(to);
@@ -40,5 +40,3 @@ const LayoutItem = (props: LinkProps) => {
     </ListItem>
   );
 };
-
-export default LayoutItem;
diff --git a/src/components/layout/layout-traffic.tsx b/src/components/layout/layout-traffic.tsx
index 01e55e5..4e65a87 100644
--- a/src/components/layout/layout-traffic.tsx
+++ b/src/components/layout/layout-traffic.tsx
@@ -14,7 +14,7 @@ import { useWebsocket } from "@/hooks/use-websocket";
 import parseTraffic from "@/utils/parse-traffic";
 
 // setup the traffic
-const LayoutTraffic = () => {
+export const LayoutTraffic = () => {
   const { clashInfo } = useClashInfo();
   const { verge } = useVerge();
 
@@ -134,5 +134,3 @@ const LayoutTraffic = () => {
     </Box>
   );
 };
-
-export default LayoutTraffic;
diff --git a/src/components/layout/update-button.tsx b/src/components/layout/update-button.tsx
index 5bccb4c..b71ea07 100644
--- a/src/components/layout/update-button.tsx
+++ b/src/components/layout/update-button.tsx
@@ -1,17 +1,19 @@
 import useSWR from "swr";
-import { useState } from "react";
+import { useRef } from "react";
 import { Button } from "@mui/material";
 import { checkUpdate } from "@tauri-apps/api/updater";
-import UpdateDialog from "./update-dialog";
+import { UpdateViewer } from "../setting/mods/update-viewer";
+import { DialogRef } from "../base";
 
 interface Props {
   className?: string;
 }
 
-const UpdateButton = (props: Props) => {
+export const UpdateButton = (props: Props) => {
   const { className } = props;
 
-  const [dialogOpen, setDialogOpen] = useState(false);
+  const viewerRef = useRef<DialogRef>(null);
+
   const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
     errorRetryCount: 2,
     revalidateIfStale: false,
@@ -22,21 +24,17 @@ const UpdateButton = (props: Props) => {
 
   return (
     <>
+      <UpdateViewer ref={viewerRef} />
+
       <Button
         color="error"
         variant="contained"
         size="small"
         className={className}
-        onClick={() => setDialogOpen(true)}
+        onClick={() => viewerRef.current?.open()}
       >
         New
       </Button>
-
-      {dialogOpen && (
-        <UpdateDialog open={dialogOpen} onClose={() => setDialogOpen(false)} />
-      )}
     </>
   );
 };
-
-export default UpdateButton;
diff --git a/src/components/layout/use-custom-theme.ts b/src/components/layout/use-custom-theme.ts
index d7aae55..e029494 100644
--- a/src/components/layout/use-custom-theme.ts
+++ b/src/components/layout/use-custom-theme.ts
@@ -9,7 +9,7 @@ import { useVerge } from "@/hooks/use-verge";
 /**
  * custom theme
  */
-export default function useCustomTheme() {
+export const useCustomTheme = () => {
   const { verge } = useVerge();
   const { theme_mode, theme_setting } = verge ?? {};
   const [mode, setMode] = useRecoilState(atomThemeMode);
@@ -121,4 +121,4 @@ export default function useCustomTheme() {
   }, [mode, theme_setting]);
 
   return { theme };
-}
+};
diff --git a/src/components/layout/update-dialog.tsx b/src/components/setting/mods/update-viewer.tsx
similarity index 53%
rename from src/components/layout/update-dialog.tsx
rename to src/components/setting/mods/update-viewer.tsx
index 392e086..8b4e68a 100644
--- a/src/components/layout/update-dialog.tsx
+++ b/src/components/setting/mods/update-viewer.tsx
@@ -1,43 +1,45 @@
 import useSWR from "swr";
 import snarkdown from "snarkdown";
-import { useMemo } from "react";
+import { forwardRef, useImperativeHandle, useState, useMemo } from "react";
+import { useLockFn } from "ahooks";
+import { Box, styled } from "@mui/material";
 import { useRecoilState } from "recoil";
 import { useTranslation } from "react-i18next";
-import {
-  Box,
-  Button,
-  Dialog,
-  DialogActions,
-  DialogContent,
-  DialogTitle,
-  styled,
-} from "@mui/material";
 import { relaunch } from "@tauri-apps/api/process";
 import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
+import { BaseDialog, DialogRef, Notice } from "@/components/base";
 import { atomUpdateState } from "@/services/states";
-import { Notice } from "@/components/base";
-
-interface Props {
-  open: boolean;
-  onClose: () => void;
-}
 
 const UpdateLog = styled(Box)(() => ({
   "h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" },
 }));
 
-const UpdateDialog = (props: Props) => {
-  const { open, onClose } = props;
+export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
   const { t } = useTranslation();
+
+  const [open, setOpen] = useState(false);
+  const [updateState, setUpdateState] = useRecoilState(atomUpdateState);
+
   const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
     errorRetryCount: 2,
     revalidateIfStale: false,
     focusThrottleInterval: 36e5, // 1 hour
   });
 
-  const [updateState, setUpdateState] = useRecoilState(atomUpdateState);
+  useImperativeHandle(ref, () => ({
+    open: () => setOpen(true),
+    close: () => setOpen(false),
+  }));
+
+  // markdown parser
+  const parseContent = useMemo(() => {
+    if (!updateInfo?.manifest?.body) {
+      return "New Version is available";
+    }
+    return snarkdown(updateInfo?.manifest?.body);
+  }, [updateInfo]);
 
-  const onUpdate = async () => {
+  const onUpdate = useLockFn(async () => {
     if (updateState) return;
     setUpdateState(true);
 
@@ -49,39 +51,20 @@ const UpdateDialog = (props: Props) => {
     } finally {
       setUpdateState(false);
     }
-  };
-
-  // markdown parser
-  const parseContent = useMemo(() => {
-    if (!updateInfo?.manifest?.body) {
-      return "New Version is available";
-    }
-    return snarkdown(updateInfo?.manifest?.body);
-  }, [updateInfo]);
+  });
 
   return (
-    <Dialog open={open} onClose={onClose}>
-      <DialogTitle>New Version v{updateInfo?.manifest?.version}</DialogTitle>
-
-      <DialogContent sx={{ minWidth: 360, maxWidth: 400, maxHeight: "50vh" }}>
-        <UpdateLog dangerouslySetInnerHTML={{ __html: parseContent }} />
-      </DialogContent>
-
-      <DialogActions>
-        <Button variant="outlined" onClick={onClose}>
-          {t("Cancel")}
-        </Button>
-        <Button
-          autoFocus
-          variant="contained"
-          disabled={updateState}
-          onClick={onUpdate}
-        >
-          {t("Update")}
-        </Button>
-      </DialogActions>
-    </Dialog>
+    <BaseDialog
+      open={open}
+      title={`New Version v${updateInfo?.manifest?.version}`}
+      contentSx={{ minWidth: 360, maxWidth: 400, maxHeight: "50vh" }}
+      okBtn={t("Update")}
+      cancelBtn={t("Cancel")}
+      onClose={() => setOpen(false)}
+      onCancel={() => setOpen(false)}
+      onOk={onUpdate}
+    >
+      <UpdateLog dangerouslySetInnerHTML={{ __html: parseContent }} />
+    </BaseDialog>
   );
-};
-
-export default UpdateDialog;
+});
diff --git a/src/components/setting/setting-verge.tsx b/src/components/setting/setting-verge.tsx
index 9805e5e..d54094d 100644
--- a/src/components/setting/setting-verge.tsx
+++ b/src/components/setting/setting-verge.tsx
@@ -1,11 +1,13 @@
 import { useRef } from "react";
+import { useLockFn } from "ahooks";
 import { useTranslation } from "react-i18next";
 import { IconButton, MenuItem, Select, Typography } from "@mui/material";
 import { openAppDir, openCoreDir, openLogsDir } from "@/services/cmds";
 import { ArrowForward } from "@mui/icons-material";
+import { checkUpdate } from "@tauri-apps/api/updater";
 import { useVerge } from "@/hooks/use-verge";
 import { version } from "@root/package.json";
-import { DialogRef } from "@/components/base";
+import { DialogRef, Notice } from "@/components/base";
 import { SettingList, SettingItem } from "./mods/setting-comp";
 import { ThemeModeSwitch } from "./mods/theme-mode-switch";
 import { ConfigViewer } from "./mods/config-viewer";
@@ -14,29 +16,45 @@ import { MiscViewer } from "./mods/misc-viewer";
 import { ThemeViewer } from "./mods/theme-viewer";
 import { GuardState } from "./mods/guard-state";
 import { LayoutViewer } from "./mods/layout-viewer";
+import { UpdateViewer } from "./mods/update-viewer";
+import getSystem from "@/utils/get-system";
 
 interface Props {
   onError?: (err: Error) => void;
 }
 
+const OS = getSystem();
+
 const SettingVerge = ({ onError }: Props) => {
   const { t } = useTranslation();
 
   const { verge, patchVerge, mutateVerge } = useVerge();
-
-  const { theme_mode, theme_blur, traffic_graph, language } = verge ?? {};
+  const { theme_mode, language } = verge ?? {};
 
   const configRef = useRef<DialogRef>(null);
   const hotkeyRef = useRef<DialogRef>(null);
   const miscRef = useRef<DialogRef>(null);
   const themeRef = useRef<DialogRef>(null);
   const layoutRef = useRef<DialogRef>(null);
+  const updateRef = useRef<DialogRef>(null);
 
-  const onSwitchFormat = (_e: any, value: boolean) => value;
   const onChangeData = (patch: Partial<IVergeConfig>) => {
     mutateVerge({ ...verge, ...patch }, false);
   };
 
+  const onCheckUpdate = useLockFn(async () => {
+    try {
+      const info = await checkUpdate();
+      if (!info?.shouldUpdate) {
+        Notice.success("No Updates Available");
+      } else {
+        updateRef.current?.open();
+      }
+    } catch (err: any) {
+      Notice.error(err.message || err.toString());
+    }
+  });
+
   return (
     <SettingList title={t("Verge Setting")}>
       <ThemeViewer ref={themeRef} />
@@ -44,6 +62,7 @@ const SettingVerge = ({ onError }: Props) => {
       <HotkeyViewer ref={hotkeyRef} />
       <MiscViewer ref={miscRef} />
       <LayoutViewer ref={layoutRef} />
+      <UpdateViewer ref={updateRef} />
 
       <SettingItem label={t("Language")}>
         <GuardState
@@ -160,6 +179,19 @@ const SettingVerge = ({ onError }: Props) => {
         </IconButton>
       </SettingItem>
 
+      {!(OS === "windows" && WIN_PORTABLE) && (
+        <SettingItem label={t("Check for Updates")}>
+          <IconButton
+            color="inherit"
+            size="small"
+            sx={{ my: "2px" }}
+            onClick={onCheckUpdate}
+          >
+            <ArrowForward />
+          </IconButton>
+        </SettingItem>
+      )}
+
       <SettingItem label={t("Verge Version")}>
         <Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
       </SettingItem>
diff --git a/src/locales/en.json b/src/locales/en.json
index 3a14bc5..1ac4a0e 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -90,6 +90,7 @@
   "Open App Dir": "Open App Dir",
   "Open Core Dir": "Open Core Dir",
   "Open Logs Dir": "Open Logs Dir",
+  "Check for Updates": "Check for Updates",
   "Verge Version": "Verge Version",
   "theme.light": "Light",
   "theme.dark": "Dark",
diff --git a/src/locales/zh.json b/src/locales/zh.json
index 65a1340..26964be 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -90,6 +90,7 @@
   "Open App Dir": "应用目录",
   "Open Core Dir": "内核目录",
   "Open Logs Dir": "日志目录",
+  "Check for Updates": "检查更新",
   "Verge Version": "应用版本",
   "theme.light": "浅色",
   "theme.dark": "深色",
diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx
index f0b1b96..27ef9b0 100644
--- a/src/pages/_layout.tsx
+++ b/src/pages/_layout.tsx
@@ -13,11 +13,11 @@ import { getAxios } from "@/services/api";
 import { useVerge } from "@/hooks/use-verge";
 import { ReactComponent as LogoSvg } from "@/assets/image/logo.svg";
 import { BaseErrorBoundary, Notice } from "@/components/base";
-import LayoutItem from "@/components/layout/layout-item";
-import LayoutControl from "@/components/layout/layout-control";
-import LayoutTraffic from "@/components/layout/layout-traffic";
-import UpdateButton from "@/components/layout/update-button";
-import useCustomTheme from "@/components/layout/use-custom-theme";
+import { LayoutItem } from "@/components/layout/layout-item";
+import { LayoutControl } from "@/components/layout/layout-control";
+import { LayoutTraffic } from "@/components/layout/layout-traffic";
+import { UpdateButton } from "@/components/layout/update-button";
+import { useCustomTheme } from "@/components/layout/use-custom-theme";
 import getSystem from "@/utils/get-system";
 import "dayjs/locale/ru";
 import "dayjs/locale/zh-cn";
-- 
GitLab