feat: refactor and adjust ui
This commit is contained in:
parent
59c09f90f9
commit
d6c3bc57c0
@ -28,3 +28,4 @@ body {
|
||||
}
|
||||
|
||||
@import "./layout.scss";
|
||||
@import "./page.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
src/assets/styles/page.scss
Normal file
33
src/assets/styles/page.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
32
src/components/base-page.tsx
Normal file
32
src/components/base-page.tsx
Normal 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;
|
40
src/components/update-button.tsx
Normal file
40
src/components/update-button.tsx
Normal 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;
|
39
src/components/window-control.tsx
Normal file
39
src/components/window-control.tsx
Normal 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;
|
@ -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
33
src/pages/_routers.tsx
Normal 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,
|
||||
},
|
||||
];
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: 0.9,
|
||||
maxWidth: "850px",
|
||||
height: "100%",
|
||||
mx: "auto",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4" component="h1" sx={{ py: 2 }}>
|
||||
Logs
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
sx={{ position: "absolute", top: 22, right: 0 }}
|
||||
onClick={() => {
|
||||
const onClear = () => {
|
||||
setLogData([]);
|
||||
logCache = [];
|
||||
}}
|
||||
};
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title="Logs"
|
||||
contentStyle={{ height: "100%" }}
|
||||
header={
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ mt: 1 }}
|
||||
variant="contained"
|
||||
onClick={onClear}
|
||||
>
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user