feat: refactor and adjust ui

This commit is contained in:
GyDi 2022-01-16 03:11:07 +08:00
parent 59c09f90f9
commit d6c3bc57c0
No known key found for this signature in database
GPG Key ID: 1C95E0D3467B3084
14 changed files with 264 additions and 191 deletions

View File

@ -28,3 +28,4 @@ body {
}
@import "./layout.scss";
@import "./page.scss";

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>
);

33
src/pages/_routers.tsx Normal file
View File

@ -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,
},
];

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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");
}