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 "./layout.scss";
@import "./page.scss";

View File

@ -27,6 +27,10 @@
text-align: center; text-align: center;
box-sizing: border-box; box-sizing: border-box;
img {
width: 100%;
}
.the-newbtn { .the-newbtn {
position: absolute; position: absolute;
right: 20px; right: 20px;
@ -54,27 +58,24 @@
position: relative; position: relative;
flex: 1 1 75%; flex: 1 1 75%;
height: 100%; height: 100%;
display: flex;
flex-direction: column;
padding: 2px 0;
box-sizing: border-box;
.the-bar { .the-bar {
flex: 0 0 30px; position: absolute;
width: 100%; top: 2px;
height: 30px; right: 8px;
padding: 0 16px; height: 36px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end;
box-sizing: border-box; box-sizing: border-box;
z-index: 2;
} }
.the-content { .the-content {
flex: 1 1 100%; position: absolute;
overflow: auto; left: 0;
box-sizing: border-box; right: 0;
scrollbar-gutter: stable; 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 useSWR, { SWRConfig } from "swr";
import { useEffect, useMemo } from "react";
import { Route, Routes } from "react-router-dom"; import { Route, Routes } from "react-router-dom";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { import { alpha, createTheme, List, Paper, ThemeProvider } from "@mui/material";
alpha, import { appWindow } from "@tauri-apps/api/window";
Button,
createTheme,
IconButton,
List,
Paper,
ThemeProvider,
} from "@mui/material";
import { HorizontalRuleRounded, CloseRounded } from "@mui/icons-material";
import { checkUpdate } from "@tauri-apps/api/updater";
import { atomPaletteMode, atomThemeBlur } from "../states/setting"; 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 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 Traffic from "../components/traffic";
import UpdateDialog from "../components/update-dialog"; import LayoutItem from "../components/layout-item";
import UpdateButton from "../components/update-button";
const routers = [ import WindowControl from "../components/window-control";
{
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,
},
];
const Layout = () => { const Layout = () => {
const [mode, setMode] = useRecoilState(atomPaletteMode); const [mode, setMode] = useRecoilState(atomPaletteMode);
const [blur, setBlur] = useRecoilState(atomThemeBlur); const [blur, setBlur] = useRecoilState(atomThemeBlur);
const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig); 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(() => { useEffect(() => {
window.addEventListener("keydown", (e) => { window.addEventListener("keydown", (e) => {
if (e.key === "Escape") windowHide(); if (e.key === "Escape") appWindow.hide();
}); });
}, []); }, []);
@ -96,6 +50,12 @@ const Layout = () => {
}); });
}, [mode]); }, [mode]);
const onDragging = (e: any) => {
if (e?.target?.dataset?.windrag) {
appWindow.startDragging();
}
};
return ( return (
<SWRConfig value={{}}> <SWRConfig value={{}}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
@ -103,38 +63,21 @@ const Layout = () => {
square square
elevation={0} elevation={0}
className="layout" className="layout"
onPointerDown={onDragging}
sx={[ sx={[
(theme) => ({ (theme) => ({
bgcolor: alpha(theme.palette.background.paper, blur ? 0.85 : 1), bgcolor: alpha(theme.palette.background.paper, blur ? 0.85 : 1),
}), }),
]} ]}
> >
<div className="layout__left"> <div className="layout__left" data-windrag>
<div className="the-logo"> <div className="the-logo" data-windrag>
<img <img src={LogoSvg} alt="" data-windrag />
src={LogoSvg}
width="100%"
alt=""
onPointerDown={(e) => {
windowDrag();
e.preventDefault();
}}
/>
{updateInfo?.shouldUpdate && ( <UpdateButton className="the-newbtn" />
<Button
color="error"
variant="contained"
size="small"
className="the-newbtn"
onClick={() => setDialogOpen(true)}
>
New
</Button>
)}
</div> </div>
<List className="the-menu"> <List className="the-menu" data-windrag>
{routers.map((router) => ( {routers.map((router) => (
<LayoutItem key={router.label} to={router.link}> <LayoutItem key={router.label} to={router.link}>
{router.label} {router.label}
@ -142,29 +85,17 @@ const Layout = () => {
))} ))}
</List> </List>
<div className="the-traffic"> <div className="the-traffic" data-windrag>
<Traffic /> <Traffic />
</div> </div>
</div> </div>
<div className="layout__right"> <div className="layout__right" data-windrag>
<div <div className="the-bar">
className="the-bar" <WindowControl />
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> </div>
<div className="the-content"> <div className="the-content" data-windrag>
<Routes> <Routes>
{routers.map(({ label, link, ele: Ele }) => ( {routers.map(({ label, link, ele: Ele }) => (
<Route key={label} path={link} element={<Ele />} /> <Route key={label} path={link} element={<Ele />} />
@ -173,7 +104,6 @@ const Layout = () => {
</div> </div>
</div> </div>
</Paper> </Paper>
<UpdateDialog open={dialogOpen} onClose={() => setDialogOpen(false)} />
</ThemeProvider> </ThemeProvider>
</SWRConfig> </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 { useEffect, useState } from "react";
import { Box, Paper, Typography } from "@mui/material"; import { Paper } from "@mui/material";
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
import { getInfomation } from "../services/api";
import { ApiType } from "../services/types"; import { ApiType } from "../services/types";
import { getInfomation } from "../services/api";
import BasePage from "../components/base-page";
import ConnectionItem from "../components/connection-item"; import ConnectionItem from "../components/connection-item";
const ConnectionsPage = () => { const ConnectionsPage = () => {
@ -26,25 +27,14 @@ const ConnectionsPage = () => {
}, []); }, []);
return ( return (
<Box <BasePage title="Connections" contentStyle={{ height: "100%" }}>
sx={{ <Paper sx={{ boxShadow: 2, height: "100%" }}>
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)" }}>
<Virtuoso <Virtuoso
data={conn.connections} data={conn.connections}
itemContent={(index, item) => <ConnectionItem value={item} />} itemContent={(index, item) => <ConnectionItem value={item} />}
/> />
</Paper> </Paper>
</Box> </BasePage>
); );
}; };

View File

@ -1,9 +1,10 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import { Box, Button, Paper, Typography } from "@mui/material"; import { Button, Paper } from "@mui/material";
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
import { ApiType } from "../services/types"; import { ApiType } from "../services/types";
import { getInfomation } from "../services/api"; import { getInfomation } from "../services/api";
import BasePage from "../components/base-page";
import LogItem from "../components/log-item"; import LogItem from "../components/log-item";
let logCache: ApiType.LogItem[] = []; let logCache: ApiType.LogItem[] = [];
@ -28,33 +29,27 @@ const LogPage = () => {
return () => ws?.close(); return () => ws?.close();
}, []); }, []);
const onClear = () => {
setLogData([]);
logCache = [];
};
return ( return (
<Box <BasePage
sx={{ title="Logs"
position: "relative", contentStyle={{ height: "100%" }}
width: 0.9, header={
maxWidth: "850px", <Button
height: "100%", size="small"
mx: "auto", sx={{ mt: 1 }}
}} variant="contained"
onClick={onClear}
>
Clear
</Button>
}
> >
<Typography variant="h4" component="h1" sx={{ py: 2 }}> <Paper sx={{ boxShadow: 2, height: "100%" }}>
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)" }}>
<Virtuoso <Virtuoso
initialTopMostItemIndex={999} initialTopMostItemIndex={999}
data={logData} data={logData}
@ -62,7 +57,7 @@ const LogPage = () => {
followOutput={"smooth"} followOutput={"smooth"}
/> />
</Paper> </Paper>
</Box> </BasePage>
); );
}; };

View File

@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from "react";
import useSWR, { useSWRConfig } from "swr"; 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 { import {
getProfiles, getProfiles,
selectProfile, selectProfile,
@ -8,9 +8,10 @@ import {
importProfile, importProfile,
} from "../services/cmds"; } from "../services/cmds";
import { getProxies, updateProxy } from "../services/api"; import { getProxies, updateProxy } from "../services/api";
import ProfileItemComp from "../components/profile-item";
import useNotice from "../utils/use-notice";
import noop from "../utils/noop"; 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 ProfilePage = () => {
const [url, setUrl] = useState(""); const [url, setUrl] = useState("");
@ -97,11 +98,7 @@ const ProfilePage = () => {
}; };
return ( return (
<Box sx={{ width: 0.9, maxWidth: "850px", mx: "auto", mb: 2 }}> <BasePage title="Profiles">
<Typography variant="h4" component="h1" sx={{ py: 2, mb: 1 }}>
Profiles
</Typography>
<Box sx={{ display: "flex", mb: 3 }}> <Box sx={{ display: "flex", mb: 3 }}>
<TextField <TextField
id="profile_url" id="profile_url"
@ -136,7 +133,7 @@ const ProfilePage = () => {
</Grid> </Grid>
{noticeElement} {noticeElement}
</Box> </BasePage>
); );
}; };

View File

@ -1,9 +1,10 @@
import useSWR, { useSWRConfig } from "swr"; import useSWR, { useSWRConfig } from "swr";
import { useEffect } from "react"; import { useEffect } from "react";
import { Box, List, Paper, Typography } from "@mui/material"; import { List, Paper } from "@mui/material";
import { getProxies } from "../services/api"; import { getProxies } from "../services/api";
import ProxyGroup from "../components/proxy-group"; import BasePage from "../components/base-page";
import ProxyItem from "../components/proxy-item"; import ProxyItem from "../components/proxy-item";
import ProxyGroup from "../components/proxy-group";
const ProxyPage = () => { const ProxyPage = () => {
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
@ -19,12 +20,8 @@ const ProxyPage = () => {
}, []); }, []);
return ( return (
<Box sx={{ width: 0.9, maxWidth: "850px", mx: "auto", mb: 2 }}> <BasePage title={groups.length ? "Proxy Groups" : "Proxies"}>
<Typography variant="h4" component="h1" sx={{ py: 2 }}> <Paper sx={{ borderRadius: 1, boxShadow: 2, mb: 1 }}>
{groups.length ? "Proxy Groups" : "Proxies"}
</Typography>
<Paper sx={{ borderRadius: 1, boxShadow: 2 }}>
{groups.length > 0 && ( {groups.length > 0 && (
<List> <List>
{groups.map((group) => ( {groups.map((group) => (
@ -46,7 +43,7 @@ const ProxyPage = () => {
</List> </List>
)} )}
</Paper> </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 SettingVerge from "../components/setting-verge";
import SettingClash from "../components/setting-clash"; import SettingClash from "../components/setting-clash";
const SettingPage = () => { const SettingPage = () => {
return ( return (
<Box sx={{ width: 0.9, maxWidth: 850, mx: "auto", mb: 2 }}> <BasePage title="Settings">
<Typography variant="h4" component="h1" sx={{ py: 2 }}>
Setting
</Typography>
<Paper sx={{ borderRadius: 1, boxShadow: 2 }}> <Paper sx={{ borderRadius: 1, boxShadow: 2 }}>
<SettingVerge /> <SettingVerge />
</Paper> </Paper>
@ -16,7 +13,7 @@ const SettingPage = () => {
<Paper sx={{ borderRadius: 1, boxShadow: 2, mt: 3 }}> <Paper sx={{ borderRadius: 1, boxShadow: 2, mt: 3 }}>
<SettingClash /> <SettingClash />
</Paper> </Paper>
</Box> </BasePage>
); );
}; };

View File

@ -36,18 +36,6 @@ export async function restartSidecar() {
return invoke<void>("restart_sidecar"); 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() { export async function getClashInfo() {
return invoke<CmdType.ClashInfo | null>("get_clash_info"); return invoke<CmdType.ClashInfo | null>("get_clash_info");
} }