From d6c3bc57c06c7aa5020b8f39ed2798ac74de6b14 Mon Sep 17 00:00:00 2001
From: GyDi <segydi@foxmail.com>
Date: Sun, 16 Jan 2022 03:11:07 +0800
Subject: [PATCH] feat: refactor and adjust ui

---
 src/assets/styles/index.scss      |   1 +
 src/assets/styles/layout.scss     |  27 +++----
 src/assets/styles/page.scss       |  33 ++++++++
 src/components/base-page.tsx      |  32 ++++++++
 src/components/update-button.tsx  |  40 ++++++++++
 src/components/window-control.tsx |  39 ++++++++++
 src/pages/_layout.tsx             | 122 +++++++-----------------------
 src/pages/_routers.tsx            |  33 ++++++++
 src/pages/connections.tsx         |  22 ++----
 src/pages/log.tsx                 |  51 ++++++-------
 src/pages/profile.tsx             |  17 ++---
 src/pages/proxy.tsx               |  15 ++--
 src/pages/setting.tsx             |  11 +--
 src/services/cmds.ts              |  12 ---
 14 files changed, 264 insertions(+), 191 deletions(-)
 create mode 100644 src/assets/styles/page.scss
 create mode 100644 src/components/base-page.tsx
 create mode 100644 src/components/update-button.tsx
 create mode 100644 src/components/window-control.tsx
 create mode 100644 src/pages/_routers.tsx

diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss
index 20707e6..bda856d 100644
--- a/src/assets/styles/index.scss
+++ b/src/assets/styles/index.scss
@@ -28,3 +28,4 @@ body {
 }
 
 @import "./layout.scss";
+@import "./page.scss";
diff --git a/src/assets/styles/layout.scss b/src/assets/styles/layout.scss
index 509aaba..03ff3ee 100644
--- a/src/assets/styles/layout.scss
+++ b/src/assets/styles/layout.scss
@@ -27,6 +27,10 @@
       text-align: center;
       box-sizing: border-box;
 
+      img {
+        width: 100%;
+      }
+
       .the-newbtn {
         position: absolute;
         right: 20px;
@@ -54,27 +58,24 @@
     position: relative;
     flex: 1 1 75%;
     height: 100%;
-    display: flex;
-    flex-direction: column;
-    padding: 2px 0;
-    box-sizing: border-box;
 
     .the-bar {
-      flex: 0 0 30px;
-      width: 100%;
-      height: 30px;
-      padding: 0 16px;
+      position: absolute;
+      top: 2px;
+      right: 8px;
+      height: 36px;
       display: flex;
       align-items: center;
-      justify-content: flex-end;
       box-sizing: border-box;
+      z-index: 2;
     }
 
     .the-content {
-      flex: 1 1 100%;
-      overflow: auto;
-      box-sizing: border-box;
-      scrollbar-gutter: stable;
+      position: absolute;
+      left: 0;
+      right: 0;
+      top: 30px;
+      bottom: 10px;
     }
   }
 }
diff --git a/src/assets/styles/page.scss b/src/assets/styles/page.scss
new file mode 100644
index 0000000..645d308
--- /dev/null
+++ b/src/assets/styles/page.scss
@@ -0,0 +1,33 @@
+.base-page {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+
+  > header {
+    flex: 0 0 58px;
+    width: 90%;
+    max-width: 850px;
+    margin: 0 auto;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+
+  > section {
+    position: relative;
+    flex: 1 1 100%;
+    width: 100%;
+    height: 100%;
+    overflow: auto;
+    padding: 8px 0;
+    box-sizing: border-box;
+    scrollbar-gutter: stable;
+
+    .base-content {
+      width: 90%;
+      max-width: 850px;
+      margin: 0 auto;
+    }
+  }
+}
diff --git a/src/components/base-page.tsx b/src/components/base-page.tsx
new file mode 100644
index 0000000..10e16ac
--- /dev/null
+++ b/src/components/base-page.tsx
@@ -0,0 +1,32 @@
+import { Typography } from "@mui/material";
+import React from "react";
+
+interface Props {
+  title?: React.ReactNode; // the page title
+  header?: React.ReactNode; // something behind title
+  contentStyle?: React.CSSProperties;
+}
+
+const BasePage: React.FC<Props> = (props) => {
+  const { title, header, contentStyle, children } = props;
+
+  return (
+    <div className="base-page" data-windrag>
+      <header data-windrag>
+        <Typography variant="h4" component="h1">
+          {title}
+        </Typography>
+
+        {header}
+      </header>
+
+      <section data-windrag>
+        <div className="base-content" style={contentStyle} data-windrag>
+          {children}
+        </div>
+      </section>
+    </div>
+  );
+};
+
+export default BasePage;
diff --git a/src/components/update-button.tsx b/src/components/update-button.tsx
new file mode 100644
index 0000000..8a83824
--- /dev/null
+++ b/src/components/update-button.tsx
@@ -0,0 +1,40 @@
+import useSWR from "swr";
+import { useState } from "react";
+import { Button } from "@mui/material";
+import { checkUpdate } from "@tauri-apps/api/updater";
+import UpdateDialog from "./update-dialog";
+
+interface Props {
+  className?: string;
+}
+
+const UpdateButton = (props: Props) => {
+  const { className } = props;
+
+  const [dialogOpen, setDialogOpen] = useState(false);
+  const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
+    errorRetryCount: 2,
+    revalidateIfStale: false,
+    focusThrottleInterval: 36e5, // 1 hour
+  });
+
+  if (!updateInfo?.shouldUpdate) return null;
+
+  return (
+    <>
+      <Button
+        color="error"
+        variant="contained"
+        size="small"
+        className={className}
+        onClick={() => setDialogOpen(true)}
+      >
+        New
+      </Button>
+
+      <UpdateDialog open={dialogOpen} onClose={() => setDialogOpen(false)} />
+    </>
+  );
+};
+
+export default UpdateButton;
diff --git a/src/components/window-control.tsx b/src/components/window-control.tsx
new file mode 100644
index 0000000..4b5e470
--- /dev/null
+++ b/src/components/window-control.tsx
@@ -0,0 +1,39 @@
+import { Button } from "@mui/material";
+import { appWindow } from "@tauri-apps/api/window";
+import {
+  CloseRounded,
+  CropLandscapeOutlined,
+  HorizontalRuleRounded,
+} from "@mui/icons-material";
+
+const WindowControl = () => {
+  return (
+    <>
+      <Button
+        size="small"
+        sx={{ minWidth: 48 }}
+        onClick={() => appWindow.minimize()}
+      >
+        <HorizontalRuleRounded />
+      </Button>
+
+      <Button
+        size="small"
+        sx={{ minWidth: 48 }}
+        onClick={() => appWindow.toggleMaximize()}
+      >
+        <CropLandscapeOutlined />
+      </Button>
+
+      <Button
+        size="small"
+        sx={{ minWidth: 48 }}
+        onClick={() => appWindow.hide()}
+      >
+        <CloseRounded />
+      </Button>
+    </>
+  );
+};
+
+export default WindowControl;
diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx
index b847093..16e5839 100644
--- a/src/pages/_layout.tsx
+++ b/src/pages/_layout.tsx
@@ -1,72 +1,26 @@
-import { useEffect, useMemo, useState } from "react";
 import useSWR, { SWRConfig } from "swr";
+import { useEffect, useMemo } from "react";
 import { Route, Routes } from "react-router-dom";
 import { useRecoilState } from "recoil";
-import {
-  alpha,
-  Button,
-  createTheme,
-  IconButton,
-  List,
-  Paper,
-  ThemeProvider,
-} from "@mui/material";
-import { HorizontalRuleRounded, CloseRounded } from "@mui/icons-material";
-import { checkUpdate } from "@tauri-apps/api/updater";
+import { alpha, createTheme, List, Paper, ThemeProvider } from "@mui/material";
+import { appWindow } from "@tauri-apps/api/window";
 import { atomPaletteMode, atomThemeBlur } from "../states/setting";
-import { getVergeConfig, windowDrag, windowHide } from "../services/cmds";
+import { getVergeConfig } from "../services/cmds";
+import { routers } from "./_routers";
 import LogoSvg from "../assets/image/logo.svg";
-import LogPage from "./log";
-import ProfilePage from "./profile";
-import ProxyPage from "./proxy";
-import SettingPage from "./setting";
-import ConnectionsPage from "./connections";
-import LayoutItem from "../components/layout-item";
 import Traffic from "../components/traffic";
-import UpdateDialog from "../components/update-dialog";
-
-const routers = [
-  {
-    label: "Proxy",
-    link: "/",
-    ele: ProxyPage,
-  },
-  {
-    label: "Profile",
-    link: "/profile",
-    ele: ProfilePage,
-  },
-  {
-    label: "Connections",
-    link: "/connections",
-    ele: ConnectionsPage,
-  },
-  {
-    label: "Log",
-    link: "/log",
-    ele: LogPage,
-  },
-  {
-    label: "Setting",
-    link: "/setting",
-    ele: SettingPage,
-  },
-];
+import LayoutItem from "../components/layout-item";
+import UpdateButton from "../components/update-button";
+import WindowControl from "../components/window-control";
 
 const Layout = () => {
   const [mode, setMode] = useRecoilState(atomPaletteMode);
   const [blur, setBlur] = useRecoilState(atomThemeBlur);
   const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig);
-  const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
-    errorRetryCount: 2,
-    revalidateIfStale: false,
-    focusThrottleInterval: 36e5, // 1 hour
-  });
-  const [dialogOpen, setDialogOpen] = useState(false);
 
   useEffect(() => {
     window.addEventListener("keydown", (e) => {
-      if (e.key === "Escape") windowHide();
+      if (e.key === "Escape") appWindow.hide();
     });
   }, []);
 
@@ -96,6 +50,12 @@ const Layout = () => {
     });
   }, [mode]);
 
+  const onDragging = (e: any) => {
+    if (e?.target?.dataset?.windrag) {
+      appWindow.startDragging();
+    }
+  };
+
   return (
     <SWRConfig value={{}}>
       <ThemeProvider theme={theme}>
@@ -103,38 +63,21 @@ const Layout = () => {
           square
           elevation={0}
           className="layout"
+          onPointerDown={onDragging}
           sx={[
             (theme) => ({
               bgcolor: alpha(theme.palette.background.paper, blur ? 0.85 : 1),
             }),
           ]}
         >
-          <div className="layout__left">
-            <div className="the-logo">
-              <img
-                src={LogoSvg}
-                width="100%"
-                alt=""
-                onPointerDown={(e) => {
-                  windowDrag();
-                  e.preventDefault();
-                }}
-              />
+          <div className="layout__left" data-windrag>
+            <div className="the-logo" data-windrag>
+              <img src={LogoSvg} alt="" data-windrag />
 
-              {updateInfo?.shouldUpdate && (
-                <Button
-                  color="error"
-                  variant="contained"
-                  size="small"
-                  className="the-newbtn"
-                  onClick={() => setDialogOpen(true)}
-                >
-                  New
-                </Button>
-              )}
+              <UpdateButton className="the-newbtn" />
             </div>
 
-            <List className="the-menu">
+            <List className="the-menu" data-windrag>
               {routers.map((router) => (
                 <LayoutItem key={router.label} to={router.link}>
                   {router.label}
@@ -142,29 +85,17 @@ const Layout = () => {
               ))}
             </List>
 
-            <div className="the-traffic">
+            <div className="the-traffic" data-windrag>
               <Traffic />
             </div>
           </div>
 
-          <div className="layout__right">
-            <div
-              className="the-bar"
-              onPointerDown={(e) =>
-                e.target === e.currentTarget && windowDrag()
-              }
-            >
-              {/* todo: onClick = windowMini */}
-              <IconButton size="small" sx={{ mx: 1 }} onClick={windowHide}>
-                <HorizontalRuleRounded fontSize="inherit" />
-              </IconButton>
-
-              <IconButton size="small" onClick={windowHide}>
-                <CloseRounded fontSize="inherit" />
-              </IconButton>
+          <div className="layout__right" data-windrag>
+            <div className="the-bar">
+              <WindowControl />
             </div>
 
-            <div className="the-content">
+            <div className="the-content" data-windrag>
               <Routes>
                 {routers.map(({ label, link, ele: Ele }) => (
                   <Route key={label} path={link} element={<Ele />} />
@@ -173,7 +104,6 @@ const Layout = () => {
             </div>
           </div>
         </Paper>
-        <UpdateDialog open={dialogOpen} onClose={() => setDialogOpen(false)} />
       </ThemeProvider>
     </SWRConfig>
   );
diff --git a/src/pages/_routers.tsx b/src/pages/_routers.tsx
new file mode 100644
index 0000000..66939e4
--- /dev/null
+++ b/src/pages/_routers.tsx
@@ -0,0 +1,33 @@
+import LogPage from "./log";
+import ProxyPage from "./proxy";
+import ProfilePage from "./profile";
+import SettingPage from "./setting";
+import ConnectionsPage from "./connections";
+
+export const routers = [
+  {
+    label: "Proxy",
+    link: "/",
+    ele: ProxyPage,
+  },
+  {
+    label: "Profile",
+    link: "/profile",
+    ele: ProfilePage,
+  },
+  {
+    label: "Connections",
+    link: "/connections",
+    ele: ConnectionsPage,
+  },
+  {
+    label: "Log",
+    link: "/log",
+    ele: LogPage,
+  },
+  {
+    label: "Setting",
+    link: "/setting",
+    ele: SettingPage,
+  },
+];
diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx
index c848c08..98bb8d3 100644
--- a/src/pages/connections.tsx
+++ b/src/pages/connections.tsx
@@ -1,8 +1,9 @@
 import { useEffect, useState } from "react";
-import { Box, Paper, Typography } from "@mui/material";
+import { Paper } from "@mui/material";
 import { Virtuoso } from "react-virtuoso";
-import { getInfomation } from "../services/api";
 import { ApiType } from "../services/types";
+import { getInfomation } from "../services/api";
+import BasePage from "../components/base-page";
 import ConnectionItem from "../components/connection-item";
 
 const ConnectionsPage = () => {
@@ -26,25 +27,14 @@ const ConnectionsPage = () => {
   }, []);
 
   return (
-    <Box
-      sx={{
-        width: 0.9,
-        maxWidth: "850px",
-        height: "100%",
-        mx: "auto",
-      }}
-    >
-      <Typography variant="h4" component="h1" sx={{ py: 2 }}>
-        Connections
-      </Typography>
-
-      <Paper sx={{ boxShadow: 2, height: "calc(100% - 100px)" }}>
+    <BasePage title="Connections" contentStyle={{ height: "100%" }}>
+      <Paper sx={{ boxShadow: 2, height: "100%" }}>
         <Virtuoso
           data={conn.connections}
           itemContent={(index, item) => <ConnectionItem value={item} />}
         />
       </Paper>
-    </Box>
+    </BasePage>
   );
 };
 
diff --git a/src/pages/log.tsx b/src/pages/log.tsx
index 1a32e6a..c7a4a7e 100644
--- a/src/pages/log.tsx
+++ b/src/pages/log.tsx
@@ -1,9 +1,10 @@
 import dayjs from "dayjs";
-import { useEffect, useRef, useState } from "react";
-import { Box, Button, Paper, Typography } from "@mui/material";
+import { useEffect, useState } from "react";
+import { Button, Paper } from "@mui/material";
 import { Virtuoso } from "react-virtuoso";
 import { ApiType } from "../services/types";
 import { getInfomation } from "../services/api";
+import BasePage from "../components/base-page";
 import LogItem from "../components/log-item";
 
 let logCache: ApiType.LogItem[] = [];
@@ -28,33 +29,27 @@ const LogPage = () => {
     return () => ws?.close();
   }, []);
 
+  const onClear = () => {
+    setLogData([]);
+    logCache = [];
+  };
+
   return (
-    <Box
-      sx={{
-        position: "relative",
-        width: 0.9,
-        maxWidth: "850px",
-        height: "100%",
-        mx: "auto",
-      }}
+    <BasePage
+      title="Logs"
+      contentStyle={{ height: "100%" }}
+      header={
+        <Button
+          size="small"
+          sx={{ mt: 1 }}
+          variant="contained"
+          onClick={onClear}
+        >
+          Clear
+        </Button>
+      }
     >
-      <Typography variant="h4" component="h1" sx={{ py: 2 }}>
-        Logs
-      </Typography>
-
-      <Button
-        size="small"
-        variant="contained"
-        sx={{ position: "absolute", top: 22, right: 0 }}
-        onClick={() => {
-          setLogData([]);
-          logCache = [];
-        }}
-      >
-        Clear
-      </Button>
-
-      <Paper sx={{ boxShadow: 2, height: "calc(100% - 100px)" }}>
+      <Paper sx={{ boxShadow: 2, height: "100%" }}>
         <Virtuoso
           initialTopMostItemIndex={999}
           data={logData}
@@ -62,7 +57,7 @@ const LogPage = () => {
           followOutput={"smooth"}
         />
       </Paper>
-    </Box>
+    </BasePage>
   );
 };
 
diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx
index adeef37..57ef2d4 100644
--- a/src/pages/profile.tsx
+++ b/src/pages/profile.tsx
@@ -1,6 +1,6 @@
-import { useEffect, useRef, useState } from "react";
 import useSWR, { useSWRConfig } from "swr";
-import { Box, Button, Grid, TextField, Typography } from "@mui/material";
+import { useEffect, useRef, useState } from "react";
+import { Box, Button, Grid, TextField } from "@mui/material";
 import {
   getProfiles,
   selectProfile,
@@ -8,9 +8,10 @@ import {
   importProfile,
 } from "../services/cmds";
 import { getProxies, updateProxy } from "../services/api";
-import ProfileItemComp from "../components/profile-item";
-import useNotice from "../utils/use-notice";
 import noop from "../utils/noop";
+import useNotice from "../utils/use-notice";
+import BasePage from "../components/base-page";
+import ProfileItemComp from "../components/profile-item";
 
 const ProfilePage = () => {
   const [url, setUrl] = useState("");
@@ -97,11 +98,7 @@ const ProfilePage = () => {
   };
 
   return (
-    <Box sx={{ width: 0.9, maxWidth: "850px", mx: "auto", mb: 2 }}>
-      <Typography variant="h4" component="h1" sx={{ py: 2, mb: 1 }}>
-        Profiles
-      </Typography>
-
+    <BasePage title="Profiles">
       <Box sx={{ display: "flex", mb: 3 }}>
         <TextField
           id="profile_url"
@@ -136,7 +133,7 @@ const ProfilePage = () => {
       </Grid>
 
       {noticeElement}
-    </Box>
+    </BasePage>
   );
 };
 
diff --git a/src/pages/proxy.tsx b/src/pages/proxy.tsx
index 02e5b0f..f491dd4 100644
--- a/src/pages/proxy.tsx
+++ b/src/pages/proxy.tsx
@@ -1,9 +1,10 @@
 import useSWR, { useSWRConfig } from "swr";
 import { useEffect } from "react";
-import { Box, List, Paper, Typography } from "@mui/material";
+import { List, Paper } from "@mui/material";
 import { getProxies } from "../services/api";
-import ProxyGroup from "../components/proxy-group";
+import BasePage from "../components/base-page";
 import ProxyItem from "../components/proxy-item";
+import ProxyGroup from "../components/proxy-group";
 
 const ProxyPage = () => {
   const { mutate } = useSWRConfig();
@@ -19,12 +20,8 @@ const ProxyPage = () => {
   }, []);
 
   return (
-    <Box sx={{ width: 0.9, maxWidth: "850px", mx: "auto", mb: 2 }}>
-      <Typography variant="h4" component="h1" sx={{ py: 2 }}>
-        {groups.length ? "Proxy Groups" : "Proxies"}
-      </Typography>
-
-      <Paper sx={{ borderRadius: 1, boxShadow: 2 }}>
+    <BasePage title={groups.length ? "Proxy Groups" : "Proxies"}>
+      <Paper sx={{ borderRadius: 1, boxShadow: 2, mb: 1 }}>
         {groups.length > 0 && (
           <List>
             {groups.map((group) => (
@@ -46,7 +43,7 @@ const ProxyPage = () => {
           </List>
         )}
       </Paper>
-    </Box>
+    </BasePage>
   );
 };
 
diff --git a/src/pages/setting.tsx b/src/pages/setting.tsx
index 6b3dfaf..98c0256 100644
--- a/src/pages/setting.tsx
+++ b/src/pages/setting.tsx
@@ -1,14 +1,11 @@
-import { Box, Paper, Typography } from "@mui/material";
+import { Paper } from "@mui/material";
+import BasePage from "../components/base-page";
 import SettingVerge from "../components/setting-verge";
 import SettingClash from "../components/setting-clash";
 
 const SettingPage = () => {
   return (
-    <Box sx={{ width: 0.9, maxWidth: 850, mx: "auto", mb: 2 }}>
-      <Typography variant="h4" component="h1" sx={{ py: 2 }}>
-        Setting
-      </Typography>
-
+    <BasePage title="Settings">
       <Paper sx={{ borderRadius: 1, boxShadow: 2 }}>
         <SettingVerge />
       </Paper>
@@ -16,7 +13,7 @@ const SettingPage = () => {
       <Paper sx={{ borderRadius: 1, boxShadow: 2, mt: 3 }}>
         <SettingClash />
       </Paper>
-    </Box>
+    </BasePage>
   );
 };
 
diff --git a/src/services/cmds.ts b/src/services/cmds.ts
index 9508d1a..d309c19 100644
--- a/src/services/cmds.ts
+++ b/src/services/cmds.ts
@@ -36,18 +36,6 @@ export async function restartSidecar() {
   return invoke<void>("restart_sidecar");
 }
 
-export async function windowDrag() {
-  return invoke<void>("win_drag");
-}
-
-export async function windowHide() {
-  return invoke<void>("win_hide");
-}
-
-export async function windowMini() {
-  return invoke<void>("win_mini");
-}
-
 export async function getClashInfo() {
   return invoke<CmdType.ClashInfo | null>("get_clash_info");
 }
-- 
GitLab