diff --git a/src/components/layout/layout-control.tsx b/src/components/layout/layout-control.tsx index 379527bbfdef0083518e04870a8f7f8ba9e03983..6ec12b970a695f17570bf289570765fbb648d827 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 545b36e0d1e9f633233db0c15dfe32fdb56260ae..5a33da22a38e91e505444a88b4c20c610a959240 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 01e55e59ce140b1b702289846688d12045d99d0e..4e65a87059e68473b0d3d80c6cf6879d53938960 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 5bccb4c616b2a395b27684b7a5b81ef2cd2ed849..b71ea07b65f581e85713dd50302a4d33b48ecd61 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 d7aae5552ec0520a40b239e3cae1f35d65a9e288..e02949413133848dd5aa76369d39df1b3b333a09 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 392e0865870e8d629d7830d049dca361fda9e512..8b4e68a279a6279ab8819e1160ad54103d9a58a3 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 9805e5e6b8bd653c4360cf2e8a52051eac6777b1..d54094d419b006b04930e47bc9273817f4a25302 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 3a14bc5caa66251922db047638ba4f5b582ce015..1ac4a0e4f3943b0e535ac78c9d1f45eff173d1a1 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 65a1340e78fcaa77ad04ef2fdb2ea8371d2294e1..26964be0f20213dd3e64fc6b0b2979a89288cb30 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 f0b1b963f9ad9431512af147405822bedefd438e..27ef9b005a90eaf4364d1e7634f0ce7dc549560b 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";