Compare commits

...

16 Commits

31 changed files with 572 additions and 111 deletions

View File

@@ -13,15 +13,15 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
Click on the corresponding link below to download the installation package. 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.5.2/Clash.Verge_1.5.2_x64-setup.exe)]
[[Windows arm64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.2/Clash.Verge_1.5.2_arm64-setup.exe)]
[[Windows x64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.4/Clash.Verge_1.5.4_x64-setup.exe)]
[[Windows arm64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.4/Clash.Verge_1.5.4_arm64-setup.exe)]
[[macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.2/Clash.Verge_1.5.2_x64.dmg)]
[[macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.2/Clash.Verge_1.5.2_aarch64.dmg)]
[[macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.4/Clash.Verge_1.5.4_x64.dmg)]
[[macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.4/Clash.Verge_1.5.4_aarch64.dmg)]
[[Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.2/clash-verge_1.5.2_amd64.AppImage)]
[[Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.2/clash-verge_1.5.2_amd64.deb)]
[[Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.2/clash-verge_1.5.2_arm64.deb)]
[[Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.4/clash-verge_1.5.4_amd64.AppImage)]
[[Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.4/clash-verge_1.5.4_amd64.deb)]
[[Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.5.4/clash-verge_1.5.4_arm64.deb)]
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+

View File

@@ -1,3 +1,28 @@
## v1.5.4
### Features
- 支持自定义托盘图标
- 支持禁用代理组图标
- 代理组显示当前代理
- 修改 `打开面板` 快捷键为`打开/关闭面板`
---
## v1.5.3
### Features
- Tun 设置添加重置按钮
### Bugs Fixes
- Tun 设置项显示错误的问题
- 修改一些默认值
- 启动时不更改启动项设置
---
## v1.5.2
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "1.5.2",
"version": "1.5.4",
"license": "GPL-3.0-only",
"scripts": {
"dev": "tauri dev",

2
src-tauri/Cargo.lock generated
View File

@@ -598,7 +598,7 @@ dependencies = [
[[package]]
name = "clash-verge"
version = "1.5.2"
version = "1.5.4"
dependencies = [
"anyhow",
"auto-launch",

View File

@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "1.5.2"
version = "1.5.4"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -39,7 +39,7 @@ serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
auto-launch = { git="https://github.com/zzzgydi/auto-launch", branch = "main" }
tauri = { version = "1.5", features = [ "dialog-open", "notification-all", "icon-png", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
tauri = { version = "1.5", features = [ "path-all", "protocol-asset", "dialog-open", "notification-all", "icon-png", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
[target.'cfg(windows)'.dependencies]
runas = "=1.0.0" # 高版本会返回错误 Status

View File

@@ -267,6 +267,33 @@ pub async fn test_delay(url: String) -> CmdResult<u32> {
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
}
#[tauri::command]
pub fn get_app_dir() -> CmdResult<String> {
let app_home_dir = wrap_err!(dirs::app_home_dir())?
.to_string_lossy()
.to_string();
Ok(app_home_dir)
}
#[tauri::command]
pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> {
let file_path = std::path::Path::new(&path);
let icon_dir = wrap_err!(dirs::app_home_dir())?.join("icons");
if !icon_dir.exists() {
let _ = std::fs::create_dir_all(&icon_dir);
}
let dest_path = icon_dir.join(name);
if file_path.exists() {
match std::fs::copy(file_path, &dest_path) {
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
Err(err) => Err(err.to_string()),
}
} else {
return Err("file not found".to_string());
}
}
#[tauri::command]
pub fn exit_app(app_handle: tauri::AppHandle) {
let _ = resolve::save_window_size_position(&app_handle, true);

View File

@@ -32,12 +32,12 @@ impl IClashTemp {
pub fn template() -> Self {
let mut map = Mapping::new();
let mut tun = Mapping::new();
tun.insert("stack".into(), "gVisor".into());
tun.insert("stack".into(), "gvisor".into());
tun.insert("device".into(), "Meta".into());
tun.insert("auto-route".into(), true.into());
tun.insert("strict-route".into(), true.into());
tun.insert("strict-route".into(), false.into());
tun.insert("auto-detect-interface".into(), true.into());
tun.insert("dns-hijack".into(), vec!["any:53", "tcp://any:53"].into());
tun.insert("dns-hijack".into(), vec!["any:53"].into());
tun.insert("mtu".into(), 9000.into());
map.insert("mixed-port".into(), 7897.into());

View File

@@ -36,6 +36,18 @@ pub struct IVerge {
/// show memory info (only for Clash Meta)
pub enable_memory_usage: Option<bool>,
/// enable group icon
pub enable_group_icon: Option<bool>,
/// common tray icon
pub common_tray_icon: Option<bool>,
/// sysproxy tray icon
pub sysproxy_tray_icon: Option<bool>,
/// tun tray icon
pub tun_tray_icon: Option<bool>,
/// clash tun mode
pub enable_tun_mode: Option<bool>,
@@ -163,6 +175,10 @@ impl IVerge {
start_page: Some("/".into()),
traffic_graph: Some(true),
enable_memory_usage: Some(true),
enable_group_icon: Some(true),
common_tray_icon: Some(false),
sysproxy_tray_icon: Some(false),
tun_tray_icon: Some(false),
enable_auto_launch: Some(false),
enable_silent_start: Some(false),
enable_system_proxy: Some(false),
@@ -204,6 +220,10 @@ impl IVerge {
patch!(startup_script);
patch!(traffic_graph);
patch!(enable_memory_usage);
patch!(enable_group_icon);
patch!(common_tray_icon);
patch!(sysproxy_tray_icon);
patch!(tun_tray_icon);
patch!(enable_tun_mode);
patch!(enable_service_mode);

View File

@@ -144,10 +144,9 @@ impl CoreManager {
let config_path = dirs::path_to_str(&config_path)?;
// fix #212
let args = match clash_core.as_str() {
"clash-meta" => vec!["-m", "-d", app_dir, "-f", config_path],
"clash-meta-alpha" => vec!["-m", "-d", app_dir, "-f", config_path],
"clash-meta" => vec!["-d", app_dir, "-f", config_path],
"clash-meta-alpha" => vec!["-d", app_dir, "-f", config_path],
_ => vec!["-d", app_dir, "-f", config_path],
};

View File

@@ -65,7 +65,7 @@ impl Hotkey {
}
let f = match func.trim() {
"open_dashboard" => feat::open_dashboard,
"open_or_close_dashboard" => feat::open_or_close_dashboard,
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
"clash_mode_global" => || feat::change_clash_mode("global".into()),
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),

View File

@@ -148,9 +148,6 @@ impl Sysopt {
/// init the auto launch
pub fn init_launch(&self) -> Result<()> {
let enable = { Config::verge().latest().enable_auto_launch };
let enable = enable.unwrap_or(false);
let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?;
let app_name = app_exe
@@ -204,28 +201,6 @@ impl Sysopt {
.set_app_path(&app_path)
.build()?;
// 避免在开发时将自启动关了
#[cfg(feature = "verge-dev")]
if !enable {
return Ok(());
}
#[cfg(target_os = "macos")]
{
if enable && !auto.is_enabled().unwrap_or(false) {
// 避免重复设置登录项
let _ = auto.disable();
auto.enable()?;
} else if !enable {
let _ = auto.disable();
}
}
#[cfg(not(target_os = "macos"))]
if enable {
auto.enable()?;
}
*self.auto_launch.lock() = Some(auto);
Ok(())

View File

@@ -1,4 +1,9 @@
use crate::{cmds, config::Config, feat, utils::resolve};
use crate::{
cmds,
config::Config,
feat,
utils::{dirs, resolve},
};
use anyhow::Result;
use tauri::{
api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
@@ -129,26 +134,47 @@ impl Tray {
let verge = verge.latest();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let common_tray_icon = verge.common_tray_icon.as_ref().unwrap_or(&false);
let sysproxy_tray_icon = verge.sysproxy_tray_icon.as_ref().unwrap_or(&false);
let tun_tray_icon = verge.tun_tray_icon.as_ref().unwrap_or(&false);
let mut indication_icon = if *system_proxy {
#[cfg(not(target_os = "macos"))]
let icon = include_bytes!("../../icons/tray-icon-sys.png").to_vec();
let mut icon = include_bytes!("../../icons/tray-icon-sys.png").to_vec();
#[cfg(target_os = "macos")]
let icon = include_bytes!("../../icons/mac-tray-icon-sys.png").to_vec();
let mut icon = include_bytes!("../../icons/mac-tray-icon-sys.png").to_vec();
if *sysproxy_tray_icon {
let path = dirs::app_home_dir()?.join("icons").join("sysproxy.png");
if path.exists() {
icon = std::fs::read(path).unwrap();
}
}
icon
} else {
#[cfg(not(target_os = "macos"))]
let icon = include_bytes!("../../icons/tray-icon.png").to_vec();
let mut icon = include_bytes!("../../icons/tray-icon.png").to_vec();
#[cfg(target_os = "macos")]
let icon = include_bytes!("../../icons/mac-tray-icon.png").to_vec();
let mut icon = include_bytes!("../../icons/mac-tray-icon.png").to_vec();
if *common_tray_icon {
let path = dirs::app_home_dir()?.join("icons").join("common.png");
if path.exists() {
icon = std::fs::read(path).unwrap();
}
}
icon
};
if *tun_mode {
#[cfg(not(target_os = "macos"))]
let icon = include_bytes!("../../icons/tray-icon-tun.png").to_vec();
let mut icon = include_bytes!("../../icons/tray-icon-tun.png").to_vec();
#[cfg(target_os = "macos")]
let icon = include_bytes!("../../icons/mac-tray-icon-tun.png").to_vec();
let mut icon = include_bytes!("../../icons/mac-tray-icon-tun.png").to_vec();
if *tun_tray_icon {
let path = dirs::app_home_dir()?.join("icons").join("tun.png");
if path.exists() {
icon = std::fs::read(path).unwrap();
}
}
indication_icon = icon
}

View File

@@ -10,13 +10,19 @@ use crate::log_err;
use crate::utils::resolve;
use anyhow::{bail, Result};
use serde_yaml::{Mapping, Value};
use tauri::{AppHandle, ClipboardManager};
use tauri::{AppHandle, ClipboardManager, Manager};
// 打开面板
pub fn open_dashboard() {
pub fn open_or_close_dashboard() {
let handle = handle::Handle::global();
let app_handle = handle.app_handle.lock();
if let Some(app_handle) = app_handle.as_ref() {
if let Some(window) = app_handle.get_window("main") {
if let Ok(true) = window.is_focused() {
let _ = window.close();
return;
}
}
resolve::create_window(app_handle);
}
}
@@ -230,6 +236,9 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let proxy_bypass = patch.system_proxy_bypass;
let language = patch.language;
let port = patch.verge_mixed_port;
let common_tray_icon = patch.common_tray_icon;
let sysproxy_tray_icon = patch.sysproxy_tray_icon;
let tun_tray_icon = patch.tun_tray_icon;
match {
#[cfg(target_os = "windows")]
@@ -269,7 +278,12 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
if language.is_some() {
handle::Handle::update_systray()?;
} else if system_proxy.or(tun_mode).is_some() {
} else if system_proxy.is_some()
|| tun_mode.is_some()
|| common_tray_icon.is_some()
|| sysproxy_tray_icon.is_some()
|| tun_tray_icon.is_some()
{
handle::Handle::update_systray_part()?;
}

View File

@@ -55,6 +55,8 @@ fn main() -> std::io::Result<()> {
cmds::get_verge_config,
cmds::patch_verge_config,
cmds::test_delay,
cmds::get_app_dir,
cmds::copy_icon_file,
cmds::exit_app,
// cmds::update_hotkeys,
// profile

View File

@@ -1,7 +1,7 @@
{
"package": {
"productName": "Clash Verge",
"version": "1.5.2"
"version": "1.5.4"
},
"build": {
"distDir": "../dist",
@@ -58,11 +58,18 @@
"dialog": {
"all": false,
"open": true
},
"protocol": {
"asset": true,
"assetScope": ["**"]
},
"path": {
"all": true
}
},
"windows": [],
"security": {
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src http: https: data: 'self';"
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src asset: http: https: data: 'self';"
}
}
}

View File

@@ -55,6 +55,7 @@ export const ProviderButton = () => {
<Typography variant="h6">{t("Proxy Provider")}</Typography>
<Button
variant="contained"
size="small"
onClick={async () => {
Object.entries(data || {}).forEach(async ([key, item]) => {
await proxyProviderUpdate(key);

View File

@@ -95,7 +95,32 @@ export const ProxyItemMini = (props: Props) => {
</Typography>
{showType && (
<Box sx={{ display: "flex", flexWrap: "nowrap", flex: "none" }}>
<Box
sx={{
display: "flex",
flexWrap: "nowrap",
flex: "none",
marginTop: "4px",
}}
>
{proxy.now && (
<Typography
variant="body2"
component="div"
color="text.secondary"
sx={{
display: "block",
textOverflow: "ellipsis",
wordBreak: "break-all",
overflow: "hidden",
whiteSpace: "nowrap",
fontSize: "0.75rem",
marginRight: "8px",
}}
>
{proxy.now}
</Typography>
)}
{!!proxy.provider && (
<TypeBox component="span">{proxy.provider}</TypeBox>
)}
@@ -181,6 +206,16 @@ const TypeBox = styled(Box)(({ theme: { palette, typography } }) => ({
fontSize: 10,
fontFamily: typography.fontFamily,
marginRight: "4px",
marginTop: "auto",
padding: "0 2px",
lineHeight: 1.25,
}));
const TypeTypo = styled(Box)(({ theme: { palette, typography } }) => ({
display: "inline-block",
fontSize: 10,
fontFamily: typography.fontFamily,
marginRight: "4px",
padding: "0 2px",
lineHeight: 1.25,
}));

View File

@@ -100,7 +100,9 @@ export const ProxyItem = (props: Props) => {
secondary={
<>
<span style={{ marginRight: 4 }}>{proxy.name}</span>
{showType && proxy.now && (
<TypeBox component="span">{proxy.now}</TypeBox>
)}
{showType && !!proxy.provider && (
<TypeBox component="span">{proxy.provider}</TypeBox>
)}

View File

@@ -16,6 +16,7 @@ import { ProxyHead } from "./proxy-head";
import { ProxyItem } from "./proxy-item";
import { ProxyItemMini } from "./proxy-item-mini";
import type { IRenderItem } from "./use-render-list";
import { useVerge } from "@/hooks/use-verge";
interface RenderProps {
item: IRenderItem;
@@ -30,6 +31,8 @@ export const ProxyRender = (props: RenderProps) => {
const { indent, item, onLocation, onCheckAll, onHeadState, onChangeProxy } =
props;
const { type, group, headState, proxy, proxyCol } = item;
const { verge } = useVerge();
const enable_group_icon = verge?.enable_group_icon ?? true;
if (type === 0 && !group.hidden) {
return (
@@ -37,18 +40,32 @@ export const ProxyRender = (props: RenderProps) => {
dense
onClick={() => onHeadState(group.name, { open: !headState?.open })}
>
{group.icon && group.icon.trim().startsWith("http") && (
<img src={group.icon} height="40px" style={{ marginRight: "8px" }} />
)}
{group.icon && group.icon.trim().startsWith("data") && (
<img src={group.icon} height="40px" style={{ marginRight: "8px" }} />
)}
{group.icon && group.icon.trim().startsWith("<svg") && (
<img
src={`data:image/svg+xml;base64,${btoa(group.icon)}`}
height="40px"
/>
)}
{enable_group_icon &&
group.icon &&
group.icon.trim().startsWith("http") && (
<img
src={group.icon}
height="40px"
style={{ marginRight: "8px" }}
/>
)}
{enable_group_icon &&
group.icon &&
group.icon.trim().startsWith("data") && (
<img
src={group.icon}
height="40px"
style={{ marginRight: "8px" }}
/>
)}
{enable_group_icon &&
group.icon &&
group.icon.trim().startsWith("<svg") && (
<img
src={`data:image/svg+xml;base64,${btoa(group.icon)}`}
height="40px"
/>
)}
<ListItemText
primary={group.name}
secondary={

View File

@@ -53,6 +53,7 @@ export const ProviderButton = () => {
<Typography variant="h6">{t("Rule Provider")}</Typography>
<Button
variant="contained"
size="small"
onClick={async () => {
Object.entries(data || {}).forEach(async ([key, item]) => {
await ruleProviderUpdate(key);

View File

@@ -14,7 +14,7 @@ const ItemWrapper = styled("div")`
`;
const HOTKEY_FUNC = [
"open_dashboard",
"open_or_close_dashboard",
"clash_mode_rule",
"clash_mode_global",
"clash_mode_direct",

View File

@@ -1,16 +1,38 @@
import { forwardRef, useImperativeHandle, useState } from "react";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import { List, Switch } from "@mui/material";
import { List, Switch, Button } from "@mui/material";
import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { SettingItem } from "./setting-comp";
import { GuardState } from "./guard-state";
import { open as openDialog } from "@tauri-apps/api/dialog";
import { convertFileSrc } from "@tauri-apps/api/tauri";
import { copyIconFile, getAppDir } from "@/services/cmds";
import { join } from "@tauri-apps/api/path";
export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const { verge, patchVerge, mutateVerge } = useVerge();
const [open, setOpen] = useState(false);
const [commonIcon, setCommonIcon] = useState("");
const [sysproxyIcon, setSysproxyIcon] = useState("");
const [tunIcon, setTunIcon] = useState("");
useEffect(() => {
initIconPath();
}, []);
async function initIconPath() {
const appDir = await getAppDir();
const icon_dir = await join(appDir, "icons");
const common_icon = await join(icon_dir, "common.png");
const sysproxy_icon = await join(icon_dir, "sysproxy.png");
const tun_icon = await join(icon_dir, "tun.png");
setCommonIcon(common_icon);
setSysproxyIcon(sysproxy_icon);
setTunIcon(tun_icon);
}
useImperativeHandle(ref, () => ({
open: () => setOpen(true),
@@ -61,6 +83,149 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
<Switch edge="end" />
</GuardState>
</SettingItem>
<SettingItem label={t("Proxy Group Icon")}>
<GuardState
value={verge?.enable_group_icon ?? true}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_group_icon: e })}
onGuard={(e) => patchVerge({ enable_group_icon: e })}
>
<Switch edge="end" />
</GuardState>
</SettingItem>
<SettingItem label={t("Common Tray Icon")}>
<GuardState
value={verge?.common_tray_icon}
onCatch={onError}
onChange={(e) => onChangeData({ common_tray_icon: e })}
onGuard={(e) => patchVerge({ common_tray_icon: e })}
>
<Button
variant="outlined"
size="small"
startIcon={
verge?.common_tray_icon &&
commonIcon && (
<img height="20px" src={convertFileSrc(commonIcon)} />
)
}
onClick={async () => {
if (verge?.common_tray_icon) {
onChangeData({ common_tray_icon: false });
patchVerge({ common_tray_icon: false });
} else {
const path = await openDialog({
directory: false,
multiple: false,
filters: [
{
name: "Tray Icon Image",
extensions: ["png"],
},
],
});
if (path?.length) {
await copyIconFile(`${path}`, "common.png");
onChangeData({ common_tray_icon: true });
patchVerge({ common_tray_icon: true });
}
}
}}
>
{verge?.common_tray_icon ? t("Clear") : t("Browse")}
</Button>
</GuardState>
</SettingItem>
<SettingItem label={t("System Proxy Tray Icon")}>
<GuardState
value={verge?.sysproxy_tray_icon}
onCatch={onError}
onChange={(e) => onChangeData({ sysproxy_tray_icon: e })}
onGuard={(e) => patchVerge({ sysproxy_tray_icon: e })}
>
<Button
variant="outlined"
size="small"
startIcon={
verge?.sysproxy_tray_icon &&
sysproxyIcon && (
<img height="20px" src={convertFileSrc(sysproxyIcon)} />
)
}
onClick={async () => {
if (verge?.sysproxy_tray_icon) {
onChangeData({ sysproxy_tray_icon: false });
patchVerge({ sysproxy_tray_icon: false });
} else {
const path = await openDialog({
directory: false,
multiple: false,
filters: [
{
name: "Tray Icon Image",
extensions: ["png"],
},
],
});
if (path?.length) {
await copyIconFile(`${path}`, "sysproxy.png");
onChangeData({ sysproxy_tray_icon: true });
patchVerge({ sysproxy_tray_icon: true });
}
}
}}
>
{verge?.sysproxy_tray_icon ? t("Clear") : t("Browse")}
</Button>
</GuardState>
</SettingItem>
<SettingItem label={t("Tun Tray Icon")}>
<GuardState
value={verge?.tun_tray_icon}
onCatch={onError}
onChange={(e) => onChangeData({ tun_tray_icon: e })}
onGuard={(e) => patchVerge({ tun_tray_icon: e })}
>
<Button
variant="outlined"
size="small"
startIcon={
verge?.tun_tray_icon &&
tunIcon && <img height="20px" src={convertFileSrc(tunIcon)} />
}
onClick={async () => {
if (verge?.tun_tray_icon) {
onChangeData({ tun_tray_icon: false });
patchVerge({ tun_tray_icon: false });
} else {
const path = await openDialog({
directory: false,
multiple: false,
filters: [
{
name: "Tray Icon Image",
extensions: ["png"],
},
],
});
if (path?.length) {
await copyIconFile(`${path}`, "tun.png");
onChangeData({ tun_tray_icon: true });
patchVerge({ tun_tray_icon: true });
}
}
}}
>
{verge?.tun_tray_icon ? t("Clear") : t("Browse")}
</Button>
</GuardState>
</SettingItem>
</List>
</BaseDialog>
);

View File

@@ -194,7 +194,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
spellCheck="false"
sx={{ width: 250 }}
value={values.defaultLatencyTimeout}
placeholder="http://1.1.1.1"
placeholder="10000"
onChange={(e) =>
setValues((v) => ({
...v,

View File

@@ -0,0 +1,71 @@
import { useTranslation } from "react-i18next";
import { Button, ButtonGroup, Tooltip } from "@mui/material";
import { checkService } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge";
import getSystem from "@/utils/get-system";
import useSWR from "swr";
const isWIN = getSystem() === "windows";
interface Props {
value?: string;
onChange?: (value: string) => void;
}
export const StackModeSwitch = (props: Props) => {
const { value, onChange } = props;
const { verge } = useVerge();
const { enable_service_mode } = verge ?? {};
// service mode
const { data: serviceStatus } = useSWR(
isWIN ? "checkService" : null,
checkService,
{
revalidateIfStale: false,
shouldRetryOnError: false,
}
);
const { t } = useTranslation();
return (
<Tooltip
title={
isWIN && (serviceStatus !== "active" || !enable_service_mode)
? t("System and Mixed Can Only be Used in Service Mode")
: ""
}
>
<ButtonGroup size="small" sx={{ my: "4px" }}>
<Button
variant={value?.toLowerCase() === "system" ? "contained" : "outlined"}
onClick={() => onChange?.("system")}
disabled={
isWIN && (serviceStatus !== "active" || !enable_service_mode)
}
sx={{ textTransform: "capitalize" }}
>
System
</Button>
<Button
variant={value?.toLowerCase() === "gvisor" ? "contained" : "outlined"}
onClick={() => onChange?.("gvisor")}
sx={{ textTransform: "capitalize" }}
>
gVisor
</Button>
<Button
variant={value?.toLowerCase() === "mixed" ? "contained" : "outlined"}
onClick={() => onChange?.("mixed")}
disabled={
isWIN && (serviceStatus !== "active" || !enable_service_mode)
}
sx={{ textTransform: "capitalize" }}
>
Mixed
</Button>
</ButtonGroup>
</Tooltip>
);
};

View File

@@ -5,13 +5,15 @@ import {
List,
ListItem,
ListItemText,
MenuItem,
Select,
Box,
Typography,
Button,
Switch,
TextField,
} from "@mui/material";
import { useClash } from "@/hooks/use-clash";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { StackModeSwitch } from "./stack-mode-switch";
export const TunViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
@@ -20,12 +22,12 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
const [open, setOpen] = useState(false);
const [values, setValues] = useState({
stack: "gVisor",
device: "Mihomo",
stack: "gvisor",
device: "Meta",
autoRoute: true,
autoDetectInterface: true,
dnsHijack: ["any:53", "tcp://any:53"],
strictRoute: true,
dnsHijack: ["any:53"],
strictRoute: false,
mtu: 9000,
});
@@ -33,12 +35,12 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
open: () => {
setOpen(true);
setValues({
stack: clash?.tun.stack ?? "gVisor",
device: clash?.tun.device ?? "Mihomo",
stack: clash?.tun.stack ?? "gvisor",
device: clash?.tun.device ?? "Meta",
autoRoute: clash?.tun["auto-route"] ?? true,
autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true,
dnsHijack: clash?.tun["dns-hijack"] ?? ["any:53", "tcp://any:53"],
strictRoute: clash?.tun["strict-route"] ?? true,
dnsHijack: clash?.tun["dns-hijack"] ?? ["any:53"],
strictRoute: clash?.tun["strict-route"] ?? false,
mtu: clash?.tun.mtu ?? 9000,
});
},
@@ -73,7 +75,45 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
return (
<BaseDialog
open={open}
title={t("Tun Mode")}
title={
<Box display="flex" justifyContent="space-between" gap={1}>
<Typography variant="h6">{t("Tun Mode")}</Typography>
<Button
variant="outlined"
size="small"
onClick={async () => {
let tun = {
stack: "gvisor",
device: "Meta",
"auto-route": true,
"auto-detect-interface": true,
"dns-hijack": ["any:53"],
"strict-route": false,
mtu: 9000,
};
setValues({
stack: "gvisor",
device: "Meta",
autoRoute: true,
autoDetectInterface: true,
dnsHijack: ["any:53"],
strictRoute: false,
mtu: 9000,
});
await patchClash({ tun });
await mutateClash(
(old) => ({
...(old! || {}),
tun,
}),
false
);
}}
>
{t("Reset to Default")}
</Button>
</Box>
}
contentSx={{ width: 450 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
@@ -84,23 +124,15 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
<List>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Stack")} />
<Select
size="small"
sx={{ width: 100, "> div": { py: "7.5px" } }}
<StackModeSwitch
value={values.stack}
onChange={(e) => {
onChange={(value) => {
setValues((v) => ({
...v,
stack: e.target.value as string,
stack: value,
}));
}}
>
{["System", "gVisor", "Mixed"].map((i) => (
<MenuItem value={i} key={i}>
{i}
</MenuItem>
))}
</Select>
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
@@ -113,7 +145,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
spellCheck="false"
sx={{ width: 250 }}
value={values.device}
placeholder="Mihomo"
placeholder="Meta"
onChange={(e) =>
setValues((v) => ({ ...v, device: e.target.value }))
}

View File

@@ -1,17 +1,16 @@
import useSWR, { mutate } from "swr";
import { useLockFn } from "ahooks";
import { getAxios, getVersion, updateConfigs } from "@/services/api";
import {
getAxios,
getClashConfig,
getVersion,
updateConfigs,
} from "@/services/api";
import { getClashInfo, patchClashConfig } from "@/services/cmds";
getClashInfo,
patchClashConfig,
getRuntimeConfig,
} from "@/services/cmds";
export const useClash = () => {
const { data: clash, mutate: mutateClash } = useSWR(
"getClashConfig",
getClashConfig
"getRuntimeConfig",
getRuntimeConfig
);
const { data: versionData, mutate: mutateVersion } = useSWR(

View File

@@ -110,6 +110,10 @@
"Hotkey Setting": "Hotkey Setting",
"Traffic Graph": "Traffic Graph",
"Memory Usage": "Memory Usage",
"Proxy Group Icon": "Proxy Group Icon",
"Common Tray Icon": "Common Tray Icon",
"System Proxy Tray Icon": "System Proxy Tray Icon",
"Tun Tray Icon": "Tun Tray Icon",
"Language": "Language",
"Open App Dir": "Open App Dir",
"Open Core Dir": "Open Core Dir",
@@ -134,7 +138,7 @@
"Download Speed": "Download Speed",
"Upload Speed": "Upload Speed",
"open_dashboard": "Open Dashboard",
"open_or_close_dashboard": "Open/Close Dashboard",
"clash_mode_rule": "Rule Mode",
"clash_mode_global": "Global Mode",
"clash_mode_direct": "Direct Mode",
@@ -160,7 +164,16 @@
"Retain 30 Days": "Retain 30 Days",
"Retain 90 Days": "Retain 90 Days",
"Stack": "Tun Stack",
"Device": "Device Name",
"Auto Route": "Auto Route",
"Strict Route": "Strict Route",
"Auto Detect Interface": "Auto Detect Interface",
"DNS Hijack": "DNS Hijack",
"MTU": "Max Transmission Unit",
"Portable Updater Error": "The portable version does not support in-app updates. Please manually download and replace it",
"Tun Mode Info Windows": "The Tun mode requires granting core-related permissions. Please enable service mode before using it",
"Tun Mode Info Unix": "The Tun mode requires granting core-related permissions. Before using it, please authorize the core in the core settings"
"Tun Mode Info Unix": "The Tun mode requires granting core-related permissions. Before using it, please authorize the core in the core settings",
"System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode"
}

View File

@@ -118,7 +118,7 @@
"Cancel": "Отмена",
"Exit": "Выход",
"open_dashboard": "Open Dashboard",
"open_or_close_dashboard": "Open/Close Dashboard",
"clash_mode_rule": "Режим правил",
"clash_mode_global": "Глобальный режим",
"clash_mode_direct": "Прямой режим",

View File

@@ -110,6 +110,10 @@
"Hotkey Setting": "热键设置",
"Traffic Graph": "流量图显",
"Memory Usage": "内存使用",
"Proxy Group Icon": "代理组图标",
"Common Tray Icon": "常规托盘图标",
"System Proxy Tray Icon": "系统代理托盘图标",
"Tun Tray Icon": "Tun模式托盘图标",
"Language": "语言设置",
"Open App Dir": "应用目录",
"Open Core Dir": "内核目录",
@@ -134,7 +138,7 @@
"Download Speed": "下载速度",
"Upload Speed": "上传速度",
"open_dashboard": "打开面板",
"open_or_close_dashboard": "打开/关闭面板",
"clash_mode_rule": "规则模式",
"clash_mode_global": "全局模式",
"clash_mode_direct": "直连模式",
@@ -160,7 +164,17 @@
"Retain 30 Days": "保留30天",
"Retain 90 Days": "保留90天",
"Stack": "Tun 模式堆栈",
"Device": "Tun 网卡名称",
"Auto Route": "自动设置全局路由",
"Strict Route": "严格路由",
"Auto Detect Interface": "自动选择流量出口接口",
"DNS Hijack": "DNS 劫持",
"MTU": "最大传输单元",
"Reset to Default": "重置为默认值",
"Portable Updater Error": "便携版不支持应用内更新,请手动下载替换",
"Tun Mode Info Windows": "Tun模式需要授予内核相关权限使用前请先开启服务模式",
"Tun Mode Info Unix": "Tun模式需要授予内核相关权限使用前请先在内核设置中给内核授权"
"Tun Mode Info Unix": "Tun模式需要授予内核相关权限使用前请先在内核设置中给内核授权",
"System and Mixed Can Only be Used in Service Mode": "System和Mixed只能在服务模式下使用"
}

View File

@@ -90,8 +90,9 @@ export async function getClashInfo() {
return invoke<IClashInfo | null>("get_clash_info");
}
// Get runtime config which controlled by verge
export async function getRuntimeConfig() {
return invoke<any | null>("get_runtime_config");
return invoke<IConfigData | null>("get_runtime_config");
}
export async function getRuntimeYaml() {
@@ -138,6 +139,10 @@ export async function grantPermission(core: string) {
return invoke<void>("grant_permission", { core });
}
export async function getAppDir() {
return invoke<string>("get_app_dir");
}
export async function openAppDir() {
return invoke<void>("open_app_dir").catch((err) =>
Notice.error(err?.message || err.toString(), 1500)
@@ -211,3 +216,10 @@ export async function getPortableFlag() {
export async function exitApp() {
return invoke("exit_app");
}
export async function copyIconFile(
path: string,
name: "common.png" | "sysproxy.png" | "tun.png"
) {
return invoke<void>("copy_icon_file", { path, name });
}

View File

@@ -200,6 +200,10 @@ interface IVergeConfig {
theme_mode?: "light" | "dark" | "system";
traffic_graph?: boolean;
enable_memory_usage?: boolean;
enable_group_icon?: boolean;
common_tray_icon?: boolean;
sysproxy_tray_icon?: boolean;
tun_tray_icon?: boolean;
enable_tun_mode?: boolean;
enable_auto_launch?: boolean;
enable_service_mode?: boolean;