Compare commits

...

13 Commits

26 changed files with 1648 additions and 1089 deletions

67
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,67 @@
# CONTRIBUTING
Thank you for your interest in contributing to Clash Verge Rev! This document provides guidelines and instructions to help you set up your development environment and start contributing.
## Development Setup
Before you start contributing to the project, you need to set up your development environment. Here are the steps you need to follow:
### Prerequisites
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/v1/guides/getting-started/prerequisites) to install them on your system.
### Setup for Windows Users
If you're a Windows user, you may need to perform some additional steps:
- Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary.
- The gnu `patch` tool should be installed
### Install Node.js Packages
After installing Rust and Node.js, install the necessary Node.js packages:
```shell
pnpm i
```
### Download the Clash Binary
You have two options for downloading the clash binary:
- Automatically download it via the provided script:
```shell
pnpm run check
# Use '--force' to force update to the latest version
# pnpm run check --force
```
- Manually download it from the [Clash Meta release](https://github.com/MetaCubeX/Clash.Meta/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
### Run the Development Server
To run the development server, use the following command:
```shell
pnpm dev
# If an app instance already exists, use a different command
pnpm dev:diff
```
### Build the Project
If you want to build the project, use:
```shell
pnpm build
```
## Contributing Your Changes
Once you have made your changes:
1. Fork the repository.
2. Create a new branch for your feature or bug fix.
3. Commit your changes with clear and concise commit messages.
4. Push your branch to your fork and submit a pull request to our repository.
We appreciate your contributions and look forward to your active participation in our project!

View File

@@ -41,18 +41,18 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
Download from [release](https://github.com/clash-verge-rev/clash-verge-rev/releases). Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
- [Windows x64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/Clash.Verge_1.4.9_x64-setup.exe)
- [Windows x86](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/Clash.Verge_1.4.9_x86-setup.exe)
- [Windows arm64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/Clash.Verge_1.4.9_arm64-setup.exe)
- [Windows x64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/Clash.Verge_1.4.10_x64-setup.exe)
- [Windows x86](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/Clash.Verge_1.4.10_x86-setup.exe)
- [Windows arm64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/Clash.Verge_1.4.10_arm64-setup.exe)
- [macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/Clash.Verge_1.4.9_x64.dmg)
- [macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/Clash.Verge_1.4.9_aarch64.dmg)
- [macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/Clash.Verge_1.4.10_x64.dmg)
- [macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/Clash.Verge_1.4.10_aarch64.dmg)
- [Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/clash-verge_1.4.9_amd64.AppImage)
- [Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/clash-verge_1.4.9_amd64.deb)
- [Linux x86 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/clash-verge_1.4.9_i386.AppImage)
- [Linux x86 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/clash-verge_1.4.9_i386.deb)
- [Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.9/clash-verge_1.4.9_arm64.deb)
- [Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/clash-verge_1.4.10_amd64.AppImage)
- [Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/clash-verge_1.4.10_amd64.deb)
- [Linux x86 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/clash-verge_1.4.10_i386.AppImage)
- [Linux x86 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/clash-verge_1.4.10_i386.deb)
- [Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.10/clash-verge_1.4.10_arm64.deb)
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
@@ -66,34 +66,14 @@ open the terminal and run `sudo xattr -r -d com.apple.quarantine /Applications/C
## Development
You should install Rust and Nodejs, see [here](https://tauri.app/v1/guides/getting-started/prerequisites) for more details. Then install Nodejs packages.
See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
To run the development server, execute the following commands after all prerequisites for **Tauri** are installed:
```shell
pnpm i
```
Then download the clash binary... Or you can download it from [clash meta release](https://github.com/MetaCubeX/Clash.Meta/releases) and rename it according to [tauri config](https://tauri.app/v1/api/config#bundleconfig.externalbin).
```shell
# force update to latest version
# pnpm run check --force
pnpm run check
```
Then run
```shell
pnpm dev
# run it in another way if app instance exists
pnpm dev:diff
```
Or you can build it
```shell
pnpm build
```
## Todos

View File

@@ -1,3 +1,19 @@
## v1.4.10
### Features
- 设置中添加退出按钮
- 支持自定义软件启动页
- 在 Proxy Provider 页面展示订阅信息
- 优化 Provider 支持
### Bugs fixed:
- 更改端口时立即重设系统代理
- 网站测试超时错误
---
## v1.4.9
### Features
@@ -11,6 +27,8 @@
- 连接页面时间排序错误
- 连接页面表格宽度优化
---
## v1.4.8
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "1.4.9",
"version": "1.4.10",
"license": "GPL-3.0",
"scripts": {
"dev": "tauri dev",
@@ -19,26 +19,27 @@
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.11.1",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^5.14.19",
"@mui/icons-material": "^5.15.5",
"@mui/lab": "5.0.0-alpha.149",
"@mui/material": "^5.14.19",
"@mui/x-data-grid": "^6.18.2",
"@tauri-apps/api": "^1.5.1",
"@mui/material": "^5.15.5",
"@mui/x-data-grid": "^6.18.7",
"@tauri-apps/api": "^1.5.3",
"ahooks": "^3.7.8",
"axios": "^1.6.2",
"axios": "^1.6.5",
"dayjs": "1.11.5",
"i18next": "^23.7.7",
"i18next": "^23.7.16",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.34.1",
"nanoid": "^5.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4",
"react-hook-form": "^7.48.2",
"react-hook-form": "^7.49.3",
"react-i18next": "^13.5.0",
"react-router-dom": "^6.20.0",
"react-router-dom": "^6.21.2",
"react-transition-group": "^4.4.5",
"react-virtuoso": "^4.6.2",
"recoil": "^0.7.7",
@@ -48,15 +49,15 @@
},
"devDependencies": {
"@actions/github": "^5.1.1",
"@tauri-apps/cli": "^1.5.6",
"@tauri-apps/cli": "^1.5.9",
"@types/fs-extra": "^9.0.13",
"@types/js-cookie": "^3.0.6",
"@types/lodash": "^4.14.202",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.2.39",
"@types/react-dom": "^18.2.17",
"@types/react-transition-group": "^4.4.9",
"@vitejs/plugin-react": "^4.2.0",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@types/react-transition-group": "^4.4.10",
"@vitejs/plugin-react": "^4.2.1",
"adm-zip": "^0.5.10",
"cross-env": "^7.0.3",
"fs-extra": "^11.2.0",
@@ -64,10 +65,10 @@
"husky": "^7.0.4",
"node-fetch": "^3.3.2",
"prettier": "^2.8.8",
"pretty-quick": "^3.1.3",
"sass": "^1.69.5",
"typescript": "^5.3.2",
"vite": "^4.5.0",
"pretty-quick": "^3.3.1",
"sass": "^1.70.0",
"typescript": "^5.3.3",
"vite": "^5.0.11",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.2.0"
},

1239
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@ const PLATFORM_MAP = {
"i686-unknown-linux-gnu": "linux",
"aarch64-unknown-linux-gnu": "linux",
"armv7-unknown-linux-gnueabihf": "linux",
"loongarch64-unknown-linux-gnu": "linux",
};
const ARCH_MAP = {
"x86_64-pc-windows-msvc": "x64",
@@ -32,6 +33,7 @@ const ARCH_MAP = {
"i686-unknown-linux-gnu": "ia32",
"aarch64-unknown-linux-gnu": "arm64",
"armv7-unknown-linux-gnueabihf": "arm",
"loongarch64-unknown-linux-gnu": "loong64",
};
const arg1 = process.argv.slice(2)[0];
@@ -63,6 +65,7 @@ const META_ALPHA_MAP = {
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
"linux-loong64": "mihomo-linux-loong64",
};
// Fetch the latest alpha release version from the version.txt file
@@ -108,6 +111,7 @@ const META_MAP = {
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
"linux-loong64": "mihomo-linux-loong64",
};
// Fetch the latest release version from the version.txt file

790
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "1.4.9"
version = "1.4.10"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0"

View File

@@ -261,7 +261,9 @@ impl PrfItem {
},
}
}
None => None,
None => Some(
crate::utils::help::get_last_part_and_decode(url).unwrap_or("Remote File".into()),
),
};
let option = match update_interval {
Some(val) => Some(PrfOption {

View File

@@ -25,6 +25,8 @@ pub struct IVerge {
/// copy env type
pub env_type: Option<String>,
/// start page
pub start_page: Option<String>,
/// startup script path
pub startup_script: Option<String>,
@@ -153,6 +155,7 @@ impl IVerge {
env_type: Some("bash".into()),
#[cfg(target_os = "windows")]
env_type: Some("powershell".into()),
start_page: Some("/".into()),
traffic_graph: Some(true),
enable_memory_usage: Some(true),
enable_auto_launch: Some(false),
@@ -192,6 +195,7 @@ impl IVerge {
patch!(theme_mode);
patch!(tray_event);
patch!(env_type);
patch!(start_page);
patch!(startup_script);
patch!(traffic_graph);
patch!(enable_memory_usage);

View File

@@ -123,6 +123,12 @@ impl Sysopt {
sysproxy.enable = enable;
sysproxy.bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
sysproxy.port = port;
if registry_mode {
#[cfg(windows)]
sysproxy.set_system_proxy_with_registry()?;

View File

@@ -225,6 +225,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let system_proxy = patch.enable_system_proxy;
let proxy_bypass = patch.system_proxy_bypass;
let language = patch.language;
let port = patch.verge_mixed_port;
match {
#[cfg(target_os = "windows")]
@@ -249,7 +250,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
if auto_launch.is_some() {
sysopt::Sysopt::global().update_launch()?;
}
if system_proxy.is_some() || proxy_bypass.is_some() {
if system_proxy.is_some() || proxy_bypass.is_some() || port.is_some() {
sysopt::Sysopt::global().update_sysproxy()?;
sysopt::Sysopt::global().guard_proxy();
}
@@ -377,23 +378,26 @@ pub async fn test_delay(url: String) -> Result<u32> {
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
let proxy_scheme = format!("http://127.0.0.1:{port}");
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
if !tun_mode {
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
}
let request = builder
.timeout(Duration::from_millis(10000))
.build()?
.get(url);
.get(url).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
let start = Instant::now();
let response = request.send().await?;

View File

@@ -80,6 +80,19 @@ pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
})
}
/// get the last part of the url, if not found, return empty string
pub fn get_last_part_and_decode(url: &str) -> Option<String> {
let path = url.split('?').next().unwrap_or(""); // Splits URL and takes the path part
let segments: Vec<&str> = path.split('/').collect();
let last_segment = segments.last()?;
Some(
percent_encoding::percent_decode_str(last_segment)
.decode_utf8_lossy()
.to_string(),
)
}
/// open file
/// use vscode by default
pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {

View File

@@ -1,7 +1,7 @@
{
"package": {
"productName": "Clash Verge",
"version": "1.4.9"
"version": "1.4.10"
},
"build": {
"distDir": "../dist",

View File

@@ -7,25 +7,32 @@ import {
List,
ListItem,
ListItemText,
styled,
Box,
alpha,
Typography,
Divider,
LinearProgress,
} from "@mui/material";
import { RefreshRounded } from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import { getProviders, providerUpdate } from "@/services/api";
import { getProxyProviders, proxyProviderUpdate } from "@/services/api";
import { BaseDialog } from "../base";
import parseTraffic from "@/utils/parse-traffic";
export const ProviderButton = () => {
const { t } = useTranslation();
const { data } = useSWR("getProviders", getProviders);
const { data } = useSWR("getProxyProviders", getProxyProviders);
const [open, setOpen] = useState(false);
const hasProvider = Object.keys(data || {}).length > 0;
const handleUpdate = useLockFn(async (key: string) => {
await providerUpdate(key);
await proxyProviderUpdate(key);
await mutate("getProxies");
await mutate("getProviders");
await mutate("getProxyProviders");
});
if (!hasProvider) return null;
@@ -43,7 +50,23 @@ export const ProviderButton = () => {
<BaseDialog
open={open}
title={t("Proxy Provider")}
title={
<Box display="flex" justifyContent="space-between" gap={1}>
<Typography variant="h6">{t("Proxy Provider")}</Typography>
<Button
variant="contained"
onClick={async () => {
Object.entries(data || {}).forEach(async ([key, item]) => {
await proxyProviderUpdate(key);
await mutate("getProxies");
await mutate("getProxyProviders");
});
}}
>
{t("Update All")}
</Button>
</Box>
}
contentSx={{ width: 400 }}
disableOk
cancelBtn={t("Cancel")}
@@ -53,30 +76,81 @@ export const ProviderButton = () => {
<List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(data || {}).map(([key, item]) => {
const time = dayjs(item.updatedAt);
const sub = item.subscriptionInfo;
const hasSubInfo = !!sub;
const upload = sub?.Upload || 0;
const download = sub?.Download || 0;
const total = sub?.Total || 0;
const expire = sub?.Expire || 0;
const progress = Math.round(
((download + upload) * 100) / (total + 0.1)
);
return (
<ListItem sx={{ p: 0 }} key={key}>
<ListItemText
primary={key}
secondary={
<>
<span style={{ marginRight: "4em" }}>
Type: {item.vehicleType}
</span>
<span title={time.format("YYYY-MM-DD HH:mm:ss")}>
Updated: {time.fromNow()}
</span>
</>
}
/>
<IconButton
size="small"
color="inherit"
title="Update Provider"
onClick={() => handleUpdate(key)}
<>
<ListItem
sx={(theme) => ({
p: 0,
borderRadius: "10px",
boxShadow: theme.shadows[2],
mb: 1,
})}
key={key}
>
<RefreshRounded />
</IconButton>
</ListItem>
<ListItemText
sx={{ px: 1 }}
primary={
<>
<Typography
variant="h6"
component="span"
noWrap
title={key}
>
{key}
</Typography>
</>
}
secondary={
<>
<StyledTypeBox component="span">
{item.vehicleType}
</StyledTypeBox>
<StyledTypeBox component="span">
{t("Update At")} {time.fromNow()}
</StyledTypeBox>
{hasSubInfo && (
<>
<Box sx={{ ...boxStyle, fontSize: 14 }}>
<span title="Used / Total">
{parseTraffic(upload + download)} /{" "}
{parseTraffic(total)}
</span>
<span title="Expire Time">
{parseExpire(expire)}
</span>
</Box>
<LinearProgress
variant="determinate"
value={progress}
color="inherit"
/>
</>
)}
</>
}
/>
<Divider orientation="vertical" flexItem />
<IconButton
size="small"
color="inherit"
title="Update Provider"
onClick={() => handleUpdate(key)}
>
<RefreshRounded />
</IconButton>
</ListItem>
</>
);
})}
</List>
@@ -84,3 +158,27 @@ export const ProviderButton = () => {
</>
);
};
const StyledTypeBox = styled(Box)(({ theme }) => ({
display: "inline-block",
border: "1px solid #ccc",
borderColor: alpha(theme.palette.primary.main, 0.5),
color: alpha(theme.palette.primary.main, 0.8),
borderRadius: 4,
fontSize: 10,
marginRight: "4px",
padding: "0 2px",
lineHeight: 1.25,
}));
const boxStyle = {
height: 26,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
};
function parseExpire(expire?: number) {
if (!expire) return "-";
return dayjs(expire * 1000).format("YYYY-MM-DD");
}

View File

@@ -0,0 +1,159 @@
import dayjs from "dayjs";
import useSWR, { mutate } from "swr";
import { useState } from "react";
import {
Button,
IconButton,
List,
ListItem,
ListItemText,
Typography,
styled,
Box,
alpha,
Divider,
} from "@mui/material";
import { RefreshRounded } from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import { getRuleProviders, ruleProviderUpdate } from "@/services/api";
import { BaseDialog } from "../base";
export const ProviderButton = () => {
const { t } = useTranslation();
const { data } = useSWR("getRuleProviders", getRuleProviders);
const [open, setOpen] = useState(false);
const hasProvider = Object.keys(data || {}).length > 0;
const handleUpdate = useLockFn(async (key: string) => {
await ruleProviderUpdate(key);
await mutate("getRules");
await mutate("getRuleProviders");
});
if (!hasProvider) return null;
return (
<>
<Button
size="small"
variant="outlined"
sx={{ textTransform: "capitalize" }}
onClick={() => setOpen(true)}
>
{t("Provider")}
</Button>
<BaseDialog
open={open}
title={
<Box display="flex" justifyContent="space-between" gap={1}>
<Typography variant="h6">{t("Rule Provider")}</Typography>
<Button
variant="contained"
onClick={async () => {
Object.entries(data || {}).forEach(async ([key, item]) => {
await ruleProviderUpdate(key);
await mutate("getRules");
await mutate("getRuleProviders");
});
}}
>
{t("Update All")}
</Button>
</Box>
}
contentSx={{ width: 400 }}
disableOk
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
<List sx={{ py: 0, minHeight: 250 }}>
{Object.entries(data || {}).map(([key, item]) => {
const time = dayjs(item.updatedAt);
return (
<>
<ListItem
sx={(theme) => ({
p: 0,
borderRadius: "10px",
boxShadow: theme.shadows[2],
mb: 1,
})}
key={key}
>
<ListItemText
sx={{ px: 1 }}
primary={
<>
<Typography
variant="h6"
component="span"
noWrap
title={key}
>
{key}
</Typography>
<TypeBox component="span" sx={{ marginLeft: "8px" }}>
{item.ruleCount}
</TypeBox>
</>
}
secondary={
<>
<StyledTypeBox component="span">
{item.vehicleType}
</StyledTypeBox>
<StyledTypeBox component="span">
{item.behavior}
</StyledTypeBox>
<StyledTypeBox component="span">
{t("Update At")} {time.fromNow()}
</StyledTypeBox>
</>
}
/>
<Divider orientation="vertical" flexItem />
<IconButton
size="small"
color="inherit"
title="Update Provider"
onClick={() => handleUpdate(key)}
>
<RefreshRounded />
</IconButton>
</ListItem>
</>
);
})}
</List>
</BaseDialog>
</>
);
};
const TypeBox = styled(Box)(({ theme }) => ({
display: "inline-block",
border: "1px solid #ccc",
borderColor: alpha(theme.palette.secondary.main, 0.5),
color: alpha(theme.palette.secondary.main, 0.8),
borderRadius: 4,
fontSize: 10,
marginRight: "4px",
padding: "0 2px",
lineHeight: 1.25,
}));
const StyledTypeBox = styled(Box)(({ theme }) => ({
display: "inline-block",
border: "1px solid #ccc",
borderColor: alpha(theme.palette.primary.main, 0.5),
color: alpha(theme.palette.primary.main, 0.8),
borderRadius: 4,
fontSize: 10,
marginRight: "4px",
padding: "0 2px",
lineHeight: 1.25,
}));

View File

@@ -13,6 +13,7 @@ import {
import { openAppDir, openCoreDir, openLogsDir } from "@/services/cmds";
import { ArrowForward } from "@mui/icons-material";
import { checkUpdate } from "@tauri-apps/api/updater";
import { exit } from "@tauri-apps/api/process";
import { useVerge } from "@/hooks/use-verge";
import { version } from "@root/package.json";
import { DialogRef, Notice } from "@/components/base";
@@ -26,8 +27,7 @@ import { GuardState } from "./mods/guard-state";
import { LayoutViewer } from "./mods/layout-viewer";
import { UpdateViewer } from "./mods/update-viewer";
import getSystem from "@/utils/get-system";
import { portableFlag } from "@/pages/_layout";
import { routers } from "@/pages/_routers";
interface Props {
onError?: (err: Error) => void;
}
@@ -38,8 +38,14 @@ const SettingVerge = ({ onError }: Props) => {
const { t } = useTranslation();
const { verge, patchVerge, mutateVerge } = useVerge();
const { theme_mode, language, tray_event, env_type, startup_script } =
verge ?? {};
const {
theme_mode,
language,
tray_event,
env_type,
startup_script,
start_page,
} = verge ?? {};
const configRef = useRef<DialogRef>(null);
const hotkeyRef = useRef<DialogRef>(null);
const miscRef = useRef<DialogRef>(null);
@@ -134,6 +140,23 @@ const SettingVerge = ({ onError }: Props) => {
</Select>
</GuardState>
</SettingItem>
<SettingItem label={t("Start Page")}>
<GuardState
value={start_page ?? "/"}
onCatch={onError}
onFormat={(e: any) => e.target.value}
onChange={(e) => onChangeData({ start_page: e })}
onGuard={(e) => patchVerge({ start_page: e })}
>
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
{routers.map((page: { label: string; link: string }) => {
return <MenuItem value={page.link}>{t(page.label)}</MenuItem>;
})}
</Select>
</GuardState>
</SettingItem>
<SettingItem label={t("Startup Script")}>
<GuardState
value={startup_script ?? ""}
@@ -270,18 +293,29 @@ const SettingVerge = ({ onError }: Props) => {
</IconButton>
</SettingItem>
{!portableFlag && (
<SettingItem label={t("Check for Updates")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={onCheckUpdate}
>
<ArrowForward />
</IconButton>
</SettingItem>
)}
<SettingItem label={t("Check for Updates")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={onCheckUpdate}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem label={t("Exit")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => {
exit(0);
}}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem label={t("Verge Version")}>
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>

View File

@@ -5,6 +5,7 @@ import { useForm, Controller } from "react-hook-form";
import { TextField } from "@mui/material";
import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, Notice } from "@/components/base";
import { nanoid } from "nanoid";
interface Props {
onChange: (uid: string, patch?: Partial<IVergeTestItem>) => void;
@@ -67,7 +68,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
let uid;
if (openType === "new") {
uid = crypto.randomUUID();
uid = nanoid();
const item = { ...form, uid };
newList = [...testList, item];
await patchVerge({ test_list: newList });

View File

@@ -57,6 +57,8 @@
"Filter conditions": "Filter conditions",
"Refresh profiles": "Refresh profiles",
"Rules": "Rules",
"Update All": "Update All",
"Update At": "Update At",
"Type": "Type",
"Name": "Name",
@@ -99,6 +101,7 @@
"Theme Mode": "Theme Mode",
"Tray Click Event": "Tray Click Event",
"Copy Env Type": "Copy Env Type",
"Start Page": "Start Page",
"Startup Script": "Startup Script",
"Browse": "Browse",
"Show Main Window": "Show Main Window",
@@ -126,6 +129,7 @@
"Back": "Back",
"Save": "Save",
"Cancel": "Cancel",
"Exit": "Exit",
"Default": "Default",
"Download Speed": "Download Speed",

View File

@@ -56,6 +56,9 @@
"Filter": "Фильтр",
"Filter conditions": "Условия фильтрации",
"Refresh profiles": "Обновить профили",
"Rules": "Правила",
"Update All": "Обновить все",
"Update At": "Обновлено в",
"Type": "Тип",
"Name": "Название",
@@ -89,6 +92,7 @@
"Current System Proxy": "Текущий системный прокси",
"Theme Mode": "Режим темы",
"Tray Click Event": "Событие щелчка в лотке",
"Start Page": "Главная страница",
"Copy Env Type": "Скопировать тип Env",
"Startup Script": "Скрипт запуска",
"Browse": "Просмотреть",
@@ -113,6 +117,7 @@
"Back": "Назад",
"Save": "Сохранить",
"Cancel": "Отмена",
"Exit": "Выход",
"open_dashboard": "Open Dashboard",
"clash_mode_rule": "Режим правил",

View File

@@ -57,6 +57,8 @@
"Filter conditions": "过滤条件",
"Refresh profiles": "刷新订阅",
"Rules": "规则",
"Update All": "更新全部",
"Update At": "更新于",
"Type": "类型",
"Name": "名称",
@@ -99,6 +101,7 @@
"Theme Mode": "主题模式",
"Tray Click Event": "托盘点击事件",
"Copy Env Type": "复制环境变量类型",
"Start Page": "启动页面",
"Startup Script": "启动脚本",
"Browse": "浏览",
"Show Main Window": "显示主窗口",
@@ -126,6 +129,7 @@
"Back": "返回",
"Save": "保存",
"Cancel": "取消",
"Exit": "退出",
"Default": "默认",
"Download Speed": "下载速度",

View File

@@ -23,7 +23,7 @@ import getSystem from "@/utils/get-system";
import "dayjs/locale/ru";
import "dayjs/locale/zh-cn";
import { getPortableFlag } from "@/services/cmds";
import { useNavigate } from "react-router-dom";
export let portableFlag = false;
dayjs.extend(relativeTime);
@@ -36,8 +36,8 @@ const Layout = () => {
const { theme } = useCustomTheme();
const { verge } = useVerge();
const { language } = verge || {};
const { language, start_page } = verge || {};
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
@@ -54,7 +54,7 @@ const Layout = () => {
mutate("getProxies");
mutate("getVersion");
mutate("getClashConfig");
mutate("getProviders");
mutate("getProxyProviders");
});
// update the verge config
@@ -88,7 +88,10 @@ const Layout = () => {
dayjs.locale(language === "zh" ? "zh-cn" : language);
i18next.changeLanguage(language);
}
}, [language]);
if (start_page) {
navigate(start_page);
}
}, [language, start_page]);
return (
<SWRConfig value={{ errorRetryCount: 3 }}>

View File

@@ -2,10 +2,11 @@ import useSWR from "swr";
import { useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Virtuoso } from "react-virtuoso";
import { Box, Paper, TextField } from "@mui/material";
import { Box, TextField } from "@mui/material";
import { getRules } from "@/services/api";
import { BaseEmpty, BasePage } from "@/components/base";
import RuleItem from "@/components/rule/rule-item";
import { ProviderButton } from "@/components/rule/provider-button";
const RulesPage = () => {
const { t } = useTranslation();
@@ -18,7 +19,16 @@ const RulesPage = () => {
}, [data, filterText]);
return (
<BasePage full title={t("Rules")} contentStyle={{ height: "100%" }}>
<BasePage
full
title={t("Rules")}
contentStyle={{ height: "100%" }}
header={
<Box display="flex" alignItems="center" gap={1}>
<ProviderButton />
</Box>
}
>
<Box
sx={{
pt: 1,

View File

@@ -20,6 +20,7 @@ import { BasePage } from "@/components/base";
import { TestViewer, TestViewerRef } from "@/components/test/test-viewer";
import { TestItem } from "@/components/test/test-item";
import { emit } from "@tauri-apps/api/event";
import { nanoid } from "nanoid";
const TestPage = () => {
const { t } = useTranslation();
@@ -34,19 +35,19 @@ const TestPage = () => {
// test list
const testList = verge?.test_list ?? [
{
uid: crypto.randomUUID(),
uid: nanoid(),
name: "Apple",
url: "https://www.apple.com",
icon: "https://www.apple.com/favicon.ico",
},
{
uid: crypto.randomUUID(),
uid: nanoid(),
name: "GitHub",
url: "https://www.github.com",
icon: `<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#000000"/></svg>`,
},
{
uid: crypto.randomUUID(),
uid: nanoid(),
name: "Google",
url: "https://www.google.com",
icon: `<svg enable-background="new 0 0 48 48" height="48" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg"><path d="m43.611 20.083h-1.611v-.083h-18v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657c-3.572-3.329-8.35-5.382-13.618-5.382-11.045 0-20 8.955-20 20s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z" fill="#ffc107"/><path d="m6.306 14.691 6.571 4.819c1.778-4.402 6.084-7.51 11.123-7.51 3.059 0 5.842 1.154 7.961 3.039l5.657-5.657c-3.572-3.329-8.35-5.382-13.618-5.382-7.682 0-14.344 4.337-17.694 10.691z" fill="#ff3d00"/><path d="m24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238c-2.008 1.521-4.504 2.43-7.219 2.43-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025c3.31 6.477 10.032 10.921 17.805 10.921z" fill="#4caf50"/><path d="m43.611 20.083h-1.611v-.083h-18v8h11.303c-.792 2.237-2.231 4.166-4.087 5.571.001-.001.002-.001.003-.002l6.19 5.238c-.438.398 6.591-4.807 6.591-14.807 0-1.341-.138-2.65-.389-3.917z" fill="#1976d2"/></svg>`,

View File

@@ -105,7 +105,7 @@ export const getProxiesInner = async () => {
export const getProxies = async () => {
const [proxyRecord, providerRecord] = await Promise.all([
getProxiesInner(),
getProviders(),
getProxyProviders(),
]);
// provider name map
@@ -166,11 +166,31 @@ export const getProxies = async () => {
};
// get proxy providers
export const getProviders = async () => {
export const getProxyProviders = async () => {
const instance = await getAxios();
const response = await instance.get<any, any>("/providers/proxies");
const providers = (response.providers || {}) as Record<string, IProviderItem>;
const providers = (response.providers || {}) as Record<
string,
IProxyProviderItem
>;
return Object.fromEntries(
Object.entries(providers).filter(([key, item]) => {
const type = item.vehicleType.toLowerCase();
return type === "http" || type === "file";
})
);
};
export const getRuleProviders = async () => {
const instance = await getAxios();
const response = await instance.get<any, any>("/providers/rules");
const providers = (response.providers || {}) as Record<
string,
IRuleProviderItem
>;
return Object.fromEntries(
Object.entries(providers).filter(([key, item]) => {
@@ -188,11 +208,16 @@ export const providerHealthCheck = async (name: string) => {
);
};
export const providerUpdate = async (name: string) => {
export const proxyProviderUpdate = async (name: string) => {
const instance = await getAxios();
return instance.put(`/providers/proxies/${encodeURIComponent(name)}`);
};
export const ruleProviderUpdate = async (name: string) => {
const instance = await getAxios();
return instance.put(`/providers/rules/${encodeURIComponent(name)}`);
};
export const getConnections = async () => {
const instance = await getAxios();
const result = await instance.get("/connections");

View File

@@ -61,12 +61,28 @@ type IProxyGroupItem = Omit<IProxyItem, "all"> & {
all: IProxyItem[];
};
interface IProviderItem {
interface IProxyProviderItem {
name: string;
type: string;
proxies: IProxyItem[];
updatedAt: string;
vehicleType: string;
subscriptionInfo?: {
Upload: number;
Download: number;
Total: number;
Expire: number;
};
}
interface IRuleProviderItem {
name: string;
behavior: string;
format: string;
ruleCount: number;
type: string;
updatedAt: string;
vehicleType: string;
}
interface ITrafficItem {
@@ -168,6 +184,7 @@ interface IVergeConfig {
tray_event?: "main_window" | "system_proxy" | "tun_mode" | string;
env_type?: "bash" | "cmd" | "powershell" | string;
startup_script?: string;
start_page?: string;
clash_core?: string;
theme_mode?: "light" | "dark" | "system";
traffic_graph?: boolean;