6 Commits

Author SHA1 Message Date
GyDi
cceb4bb81f v0.0.13 2022-01-25 02:11:00 +08:00
GyDi
f0f45e007d feat: global proxies use virtual list 2022-01-25 02:08:10 +08:00
GyDi
6a8ffe1642 feat: enable change proxy mode 2022-01-25 01:51:44 +08:00
GyDi
3a73868c10 feat: update styles 2022-01-25 01:48:26 +08:00
GyDi
ab1b5897a6 feat: manage clash mode 2022-01-24 23:13:13 +08:00
GyDi
f94734a5c8 chore: update readme 2022-01-22 23:57:39 +08:00
10 changed files with 122 additions and 29 deletions

View File

@@ -58,6 +58,15 @@ This is a learning project for Rust practice.
PR welcome! PR welcome!
## Acknowledgement
Clash Verge was based on or inspired by these projects and so on:
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, and more secure desktop applications with a web frontend.
- [Dreamacro/clash](https://github.com/Dreamacro/clash): A rule-based tunnel in Go.
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): A Windows/macOS GUI based on Clash.
- [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast!
## License ## License
GPL-3.0 License GPL-3.0 License. See [License here](./LICENSE) for details.

View File

@@ -1,6 +1,6 @@
{ {
"name": "clash-verge", "name": "clash-verge",
"version": "0.0.12", "version": "0.0.13",
"license": "GPL-3.0", "license": "GPL-3.0",
"scripts": { "scripts": {
"dev": "cargo tauri dev", "dev": "cargo tauri dev",

View File

@@ -4,4 +4,5 @@ mixed-port: 7890
log-level: info log-level: info
allow-lan: false allow-lan: false
external-controller: 127.0.0.1:9090 external-controller: 127.0.0.1:9090
mode: rule
secret: "" secret: ""

View File

@@ -306,6 +306,7 @@ pub async fn activate_profile(
"allow-lan", "allow-lan",
"external-controller", "external-controller",
"secret", "secret",
"mode",
"ipv6", "ipv6",
]; ];
valid_keys.iter().for_each(|key| { valid_keys.iter().for_each(|key| {

View File

@@ -1,7 +1,7 @@
{ {
"package": { "package": {
"productName": "clash-verge", "productName": "clash-verge",
"version": "0.0.12" "version": "0.0.13"
}, },
"build": { "build": {
"distDir": "../dist", "distDir": "../dist",

View File

@@ -72,9 +72,9 @@
.the-content { .the-content {
position: absolute; position: absolute;
left: 0;
right: 0;
top: 30px; top: 30px;
left: 0;
right: 2px;
bottom: 10px; bottom: 10px;
} }
} }

View File

@@ -9,6 +9,8 @@
width: 90%; width: 90%;
max-width: 850px; max-width: 850px;
margin: 0 auto; margin: 0 auto;
padding-right: 4px;
box-sizing: border-box;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;

View File

@@ -20,7 +20,7 @@ const BasePage: React.FC<Props> = (props) => {
{header} {header}
</header> </header>
<section data-windrag> <section>
<div className="base-content" style={contentStyle} data-windrag> <div className="base-content" style={contentStyle} data-windrag>
{children} {children}
</div> </div>

View File

@@ -107,7 +107,7 @@ const Layout = () => {
<LayoutControl /> <LayoutControl />
</div> </div>
<div className="the-content" data-windrag> <div className="the-content">
<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 />} />

View File

@@ -1,6 +1,9 @@
import useSWR, { useSWRConfig } from "swr"; import useSWR, { useSWRConfig } from "swr";
import { useEffect } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { List, Paper } from "@mui/material"; import { Virtuoso } from "react-virtuoso";
import { Button, ButtonGroup, List, Paper } from "@mui/material";
import { getClashConfig, updateConfigs, updateProxy } from "../services/api";
import { patchClashConfig } from "../services/cmds";
import { getProxies } from "../services/api"; import { getProxies } from "../services/api";
import BasePage from "../components/base-page"; import BasePage from "../components/base-page";
import ProxyItem from "../components/proxy-item"; import ProxyItem from "../components/proxy-item";
@@ -9,38 +12,115 @@ import ProxyGroup from "../components/proxy-group";
const ProxyPage = () => { const ProxyPage = () => {
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const { data: proxiesData } = useSWR("getProxies", getProxies); const { data: proxiesData } = useSWR("getProxies", getProxies);
const { groups = [], proxies = [] } = proxiesData ?? {}; const { data: clashConfig } = useSWR("getClashConfig", getClashConfig);
const [curProxy, setCurProxy] = useState<string>("DIRECT");
const curMode = clashConfig?.mode.toLowerCase();
// proxy groups
const { groups = [] } = proxiesData ?? {};
// proxies and sorted
const filterProxies = useMemo(() => {
if (!proxiesData?.proxies) return [];
const list = Object.values(proxiesData.proxies);
const retList = list.filter(
(p) => !p.all?.length && p.name !== "DIRECT" && p.name !== "REJECT"
);
const direct = list.filter((p) => p.name === "DIRECT");
const reject = list.filter((p) => p.name === "REJECT");
return direct.concat(retList).concat(reject);
}, [proxiesData]);
const modeList = ["rule", "global", "direct"];
const asGroup = curMode === "rule" || !groups.length;
// make sure that fetch the proxies successfully
useEffect(() => { useEffect(() => {
// fix the empty proxies on the first sight if (
// this bud only show on the build version (curMode === "rule" && !groups.length) ||
// call twice to avoid something unknown or the delay of the clash startup (curMode === "global" && filterProxies.length < 4)
setTimeout(() => mutate("getProxies"), 250); ) {
setTimeout(() => mutate("getProxies"), 1000); setTimeout(() => mutate("getProxies"), 500);
}, []); }
}, [groups, filterProxies, curMode]);
// update the current proxy
useEffect(() => {
if (curMode === "direct") setCurProxy("DIRECT");
if (curMode === "global") {
const globalNow = proxiesData?.proxies?.GLOBAL?.now;
setCurProxy(globalNow || "DIRECT");
}
}, [curMode, proxiesData]);
const changeLockRef = useRef(false);
const onChangeMode = async (mode: string) => {
if (changeLockRef.current) return;
changeLockRef.current = true;
try {
// switch rapidly
await updateConfigs({ mode });
await patchClashConfig({ mode });
mutate("getClashConfig");
} finally {
changeLockRef.current = false;
}
};
const onChangeProxy = async (name: string) => {
if (curMode !== "global") return;
await updateProxy("GLOBAL", name);
setCurProxy(name);
};
// difference style
const pageStyle = asGroup ? {} : { height: "100%" };
const paperStyle: any = asGroup
? { mb: 0.5 }
: { py: 1, height: "100%", boxSizing: "border-box" };
return ( return (
<BasePage title={groups.length ? "Proxy Groups" : "Proxies"}> <BasePage
<Paper sx={{ borderRadius: 1, boxShadow: 2, mb: 1 }}> contentStyle={pageStyle}
{groups.length > 0 && ( title={asGroup ? "Proxy Groups" : "Proxies"}
header={
<ButtonGroup size="small">
{modeList.map((mode) => (
<Button
key={mode}
variant={mode === curMode ? "contained" : "outlined"}
onClick={() => onChangeMode(mode)}
sx={{ textTransform: "capitalize" }}
>
{mode}
</Button>
))}
</ButtonGroup>
}
>
<Paper sx={{ borderRadius: 1, boxShadow: 2, ...paperStyle }}>
{asGroup ? (
<List> <List>
{groups.map((group) => ( {groups.map((group) => (
<ProxyGroup key={group.name} group={group} /> <ProxyGroup key={group.name} group={group} />
))} ))}
</List> </List>
)} ) : (
// virtual list
{!groups.length && ( <Virtuoso
<List> style={{ height: "100%" }}
{Object.values(proxies).map((proxy) => ( totalCount={filterProxies.length}
itemContent={(index) => (
<ProxyItem <ProxyItem
key={proxy.name} proxy={filterProxies[index]}
proxy={proxy} selected={filterProxies[index].name === curProxy}
selected={false} onClick={onChangeProxy}
sx={{ py: 0, px: 2 }} sx={{ py: 0, px: 2 }}
/> />
))} )}
</List> />
)} )}
</Paper> </Paper>
</BasePage> </BasePage>