diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss index 20707e6fc9ac38aa87cbee237ebd5933dec812e8..bda856d06662d250f068b11630538f381fe30ef0 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 509aaba8fa22031754334b096b6745655d308757..03ff3eeb0ca9cb3f4095783c819e0b00379fa915 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 0000000000000000000000000000000000000000..645d3080ee3c1f48adef3eff45ed7e366649f4b7 --- /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 0000000000000000000000000000000000000000..10e16ac6612a277c4dead855a16bc48ff5d4350d --- /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 0000000000000000000000000000000000000000..8a83824493c98c0c1c49f8194e2571f883896587 --- /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 0000000000000000000000000000000000000000..4b5e470aa99987d95a1e277742812a95a9e1e944 --- /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 b847093c27c047d2fee0e28c6067a7054890d32a..16e58395dd09ce5d6a15940f32c29c5b8e470b4c 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 0000000000000000000000000000000000000000..66939e46309b18a078dd13e811fd06c8a4ef372c --- /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 c848c08ce23ade5fbf005dae856162d9ef0d2b9e..98bb8d3133d7ba72c6551701ae2129fa796391cc 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 1a32e6a5890d60cb206bbc55fd1f6d2c11bd1d63..c7a4a7e24cd41b22a9b03d055b6e0738e1cbe77d 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 adeef37f8dfaababb3a487e3ce849297928ebd8e..57ef2d465162966c519c2f88edcd7946be1ef934 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 02e5b0f0d0f4ca0d393e574870c6c0b7670b9055..f491dd42ff962dc2978ec958d9e8c568ea5a2c1d 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 6b3dfaf0ed246d513112952d2268587120f514fd..98c02563b2e95c547cb37c5fb8026481fec169cc 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 9508d1a8e729ab7bd7a54f76e461666469d192f0..d309c19e69bf45fd4e00d41df2c6e3cc1839c11c 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"); }