Merge branch 'dev' into refactor/mihomo-api

This commit is contained in:
oomeow
2025-10-01 09:33:56 +08:00
Unverified
17 changed files with 564 additions and 380 deletions

View File

@@ -10,28 +10,67 @@ on:
jobs:
rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check Rust changes
id: check_rust
uses: dorny/paths-filter@v3
with:
filters: |
rust:
- 'src-tauri/**'
- '**/*.rs'
- name: Skip if no Rust changes
if: steps.check_rust.outputs.rust != 'true'
run: echo "No Rust changes, skipping rustfmt."
- name: install Rust stable and rustfmt
if: steps.check_rust.outputs.rust == 'true'
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: run cargo fmt
if: steps.check_rust.outputs.rust == 'true'
run: cargo fmt --manifest-path ./src-tauri/Cargo.toml --all -- --check
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check Web changes
id: check_web
uses: dorny/paths-filter@v3
with:
filters: |
web:
- 'src/**'
- '**/*.js'
- '**/*.ts'
- '**/*.tsx'
- '**/*.css'
- '**/*.scss'
- '**/*.json'
- '**/*.md'
- '**/*.json'
- name: Skip if no Web changes
if: steps.check_web.outputs.web != 'true'
run: echo "No web changes, skipping prettier."
- uses: actions/setup-node@v4
if: steps.check_web.outputs.web == 'true'
with:
node-version: "lts/*"
- run: corepack enable
if: steps.check_web.outputs.web == 'true'
- run: pnpm install --frozen-lockfile
if: steps.check_web.outputs.web == 'true'
- run: pnpm format:check
if: steps.check_web.outputs.web == 'true'
# taplo:
# name: taplo (.toml files)

View File

@@ -19,6 +19,22 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Check src-tauri changes
id: check_changes
uses: dorny/paths-filter@v3
with:
filters: |
rust:
- 'src-tauri/**'
- name: Skip if src-tauri not changed
if: steps.check_changes.outputs.rust != 'true'
run: echo "No src-tauri changes, skipping clippy lint."
- name: Continue if src-tauri changed
if: steps.check_changes.outputs.rust == 'true'
run: echo "src-tauri changed, running clippy lint."
- name: Checkout Repository
uses: actions/checkout@v4

View File

@@ -16,6 +16,7 @@
- 改进 macos 下系统代理设置的方法
- 优化 TUN 模式可用性的判断
- 移除流媒体检测的系统级提示(使用软件内通知)
- 优化后端 i18n 资源占用
### 🐞 修复问题

View File

@@ -39,7 +39,7 @@
"@mui/icons-material": "^7.3.2",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.2",
"@mui/x-data-grid": "^8.11.3",
"@mui/x-data-grid": "^8.12.1",
"@tauri-apps/api": "2.8.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.4.0",
@@ -64,11 +64,11 @@
"react-dom": "19.1.1",
"react-error-boundary": "6.0.0",
"react-hook-form": "^7.63.0",
"react-i18next": "15.7.3",
"react-i18next": "16.0.0",
"react-markdown": "10.1.0",
"react-monaco-editor": "0.59.0",
"react-router-dom": "7.9.1",
"react-virtuoso": "^4.14.0",
"react-router-dom": "7.9.3",
"react-virtuoso": "^4.14.1",
"swr": "^2.3.6",
"types-pac": "^1.0.3",
"zustand": "^5.0.8",
@@ -76,15 +76,15 @@
},
"devDependencies": {
"@actions/github": "^6.0.1",
"@eslint-react/eslint-plugin": "^1.53.1",
"@eslint-react/eslint-plugin": "^2.0.1",
"@eslint/js": "^9.36.0",
"@tauri-apps/cli": "2.8.4",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/react": "19.1.13",
"@types/react": "19.1.15",
"@types/react-dom": "19.1.9",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-react": "5.0.3",
"@vitejs/plugin-react": "5.0.4",
"adm-zip": "^0.5.16",
"cli-color": "^2.0.4",
"commander": "^14.0.1",
@@ -95,7 +95,7 @@
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.21",
"eslint-plugin-react-refresh": "^0.4.22",
"eslint-plugin-unused-imports": "^4.2.0",
"glob": "^11.0.3",
"globals": "^16.4.0",
@@ -104,8 +104,8 @@
"meta-json-schema": "^1.19.13",
"node-fetch": "^3.3.2",
"prettier": "^3.6.2",
"sass": "^1.93.1",
"tar": "^7.4.4",
"sass": "^1.93.2",
"tar": "^7.5.1",
"terser": "^5.44.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.44.1",

561
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

38
src-tauri/Cargo.lock generated
View File

@@ -2230,9 +2230,9 @@ dependencies = [
[[package]]
name = "flexi_logger"
version = "0.31.3"
version = "0.31.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce93582299e89591bcb298db76e75498a9372eaef42c9cbedfc57d670176a3cd"
checksum = "ff38b61724dd492b5171d5dbb0921dfc8e859022c5993b22f80f74e9afe6d573"
dependencies = [
"chrono",
"log",
@@ -3782,15 +3782,16 @@ dependencies = [
[[package]]
name = "kode-bridge"
version = "0.2.1"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e80a61472dcdfbd60624634f257c592d513b8df2991bd0946e4d54ccb24ea568"
checksum = "368479099245d8ecd5b74e6b2b6279a69b38556a442aefbbaadd3ecf8246ffc3"
dependencies = [
"bytes",
"futures",
"http 1.3.1",
"httparse",
"interprocess",
"libc",
"parking_lot 0.12.4",
"pin-project-lite",
"rand 0.9.2",
@@ -3802,6 +3803,7 @@ dependencies = [
"tokio-util",
"toml 0.9.7",
"tracing",
"widestring",
]
[[package]]
@@ -3854,9 +3856,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
[[package]]
name = "libc"
version = "0.2.175"
version = "0.2.176"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
[[package]]
name = "libloading"
@@ -5844,9 +5846,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.2"
version = "1.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
dependencies = [
"aho-corasick",
"memchr",
@@ -5856,9 +5858,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.10"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
dependencies = [
"aho-corasick",
"memchr",
@@ -6318,9 +6320,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.226"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
@@ -6352,18 +6354,18 @@ dependencies = [
[[package]]
name = "serde_core"
version = "1.0.226"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.226"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -6884,9 +6886,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.37.0"
version = "0.37.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07cec4dc2d2e357ca1e610cfb07de2fa7a10fc3e9fe89f72545f3d244ea87753"
checksum = "3bddd368fda2f82ead69c03d46d351987cfa0c2a57abfa37a017f3aa3e9bf69a"
dependencies = [
"libc",
"memchr",

View File

@@ -24,7 +24,7 @@ log = "0.4.28"
dunce = "1.0.5"
nanoid = "0.4"
chrono = "0.4.42"
sysinfo = { version = "0.37.0", features = ["network", "system"] }
sysinfo = { version = "0.37.1", features = ["network", "system"] }
boa_engine = "0.20.0"
serde_json = "1.0.145"
serde_yaml_ng = "0.10.0"
@@ -39,9 +39,9 @@ tokio = { version = "1.47.1", features = [
"time",
"sync",
] }
serde = { version = "1.0.226", features = ["derive"] }
serde = { version = "1.0.228", features = ["derive"] }
reqwest = { version = "0.12.23", features = ["json", "cookies"] }
regex = "1.11.2"
regex = "1.11.3"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
tauri = { version = "2.8.5", features = [
"protocol-asset",
@@ -65,13 +65,13 @@ base64 = "0.22.1"
getrandom = "0.3.3"
futures = "0.3.31"
sys-locale = "0.3.2"
libc = "0.2.175"
libc = "0.2.176"
gethostname = "1.0.2"
hmac = "0.12.1"
sha2 = "0.10.9"
hex = "0.4.3"
scopeguard = "1.2.0"
kode-bridge = "0.2.1"
kode-bridge = "0.3.0"
dashmap = "6.1.0"
tauri-plugin-notification = "2.3.1"
tokio-stream = "0.1.17"
@@ -81,7 +81,7 @@ isahc = { version = "1.7.2", default-features = false, features = [
] }
backoff = { version = "0.4.0", features = ["tokio"] }
tauri-plugin-http = "2.5.2"
flexi_logger = "0.31.3"
flexi_logger = "0.31.4"
cfg-if = "1.0.3"
nu-ansi-term = { version = "0.50.1", optional = true }
console-subscriber = { version = "0.4.1", optional = true }

View File

@@ -1,7 +1,7 @@
use crate::{config::Config, utils::dirs};
use once_cell::sync::Lazy;
use serde_json::Value;
use std::{collections::HashMap, fs, path::PathBuf};
use std::{fs, path::PathBuf, sync::RwLock};
use sys_locale;
const DEFAULT_LANGUAGE: &str = "zh";
@@ -33,22 +33,20 @@ pub fn get_supported_languages() -> Vec<String> {
languages
}
static TRANSLATIONS: Lazy<HashMap<String, Value>> = Lazy::new(|| {
let mut translations = HashMap::new();
if let Some(locales_dir) = get_locales_dir() {
for lang in get_supported_languages() {
let file_path = locales_dir.join(format!("{lang}.json"));
if let Ok(content) = fs::read_to_string(file_path)
&& let Ok(json) = serde_json::from_str(&content)
{
translations.insert(lang.to_string(), json);
}
}
}
translations
static TRANSLATIONS: Lazy<RwLock<(String, Value)>> = Lazy::new(|| {
let lang = get_system_language();
let json = load_lang_file(&lang).unwrap_or_else(|| Value::Object(Default::default()));
RwLock::new((lang, json))
});
fn load_lang_file(lang: &str) -> Option<Value> {
let locales_dir = get_locales_dir()?;
let file_path = locales_dir.join(format!("{lang}.json"));
fs::read_to_string(file_path)
.ok()
.and_then(|content| serde_json::from_str(&content).ok())
}
fn get_system_language() -> String {
sys_locale::get_locale()
.map(|locale| locale.to_lowercase())
@@ -58,8 +56,6 @@ fn get_system_language() -> String {
}
pub async fn t(key: &str) -> String {
let key = key.to_string(); // own the string
let current_lang = Config::verge()
.await
.latest_ref()
@@ -68,22 +64,35 @@ pub async fn t(key: &str) -> String {
.map(String::from)
.unwrap_or_else(get_system_language);
if let Some(text) = TRANSLATIONS
.get(&current_lang)
.and_then(|trans| trans.get(&key))
.and_then(|val| val.as_str())
{
return text.to_string();
if let Ok(cache) = TRANSLATIONS.read()
&& cache.0 == current_lang
&& let Some(text) = cache.1.get(key).and_then(|val| val.as_str())
{
return text.to_string();
}
}
if let Some(new_json) = load_lang_file(&current_lang)
&& let Ok(mut cache) = TRANSLATIONS.write()
{
*cache = (current_lang.clone(), new_json);
if let Some(text) = cache.1.get(key).and_then(|val| val.as_str()) {
return text.to_string();
}
}
if current_lang != DEFAULT_LANGUAGE
&& let Some(text) = TRANSLATIONS
.get(DEFAULT_LANGUAGE)
.and_then(|trans| trans.get(&key))
.and_then(|val| val.as_str())
&& let Some(default_json) = load_lang_file(DEFAULT_LANGUAGE)
&& let Ok(mut cache) = TRANSLATIONS.write()
{
return text.to_string();
*cache = (DEFAULT_LANGUAGE.to_string(), default_json);
if let Some(text) = cache.1.get(key).and_then(|val| val.as_str()) {
return text.to_string();
}
}
key
key.to_string()
}

View File

@@ -24,6 +24,8 @@ export const RuleItem = (props: Props) => {
const proxyPolicy = rule.match(/[^,]+$/)?.[0] ?? "";
const ruleContent = rule.slice(ruleType.length + 1, -proxyPolicy.length - 1);
const $sortable = useSortable({ id: ruleRaw });
const {
attributes,
listeners,
@@ -32,7 +34,7 @@ export const RuleItem = (props: Props) => {
transition,
isDragging,
} = sortable
? useSortable({ id: ruleRaw })
? $sortable
: {
attributes: {},
listeners: {},

View File

@@ -0,0 +1,94 @@
import { Box, Button, Tooltip } from "@mui/material";
import { useCallback, useMemo } from "react";
interface ProxyGroupNavigatorProps {
proxyGroupNames: string[];
onGroupLocation: (groupName: string) => void;
}
// 提取代理组名的第一个字符
const getGroupDisplayChar = (groupName: string): string => {
if (!groupName) return "?";
// 直接返回第一个字符,支持表情符号
const firstChar = Array.from(groupName)[0];
return firstChar || "?";
};
export const ProxyGroupNavigator = ({
proxyGroupNames,
onGroupLocation,
}: ProxyGroupNavigatorProps) => {
const handleGroupClick = useCallback(
(groupName: string) => {
onGroupLocation(groupName);
},
[onGroupLocation],
);
// 处理代理组数据,去重和排序
const processedGroups = useMemo(() => {
return proxyGroupNames
.filter((name) => name && name.trim())
.map((name) => ({
name,
displayChar: getGroupDisplayChar(name),
}));
}, [proxyGroupNames]);
if (processedGroups.length === 0) {
return null;
}
return (
<Box
sx={{
position: "absolute",
right: 2,
top: "50%",
transform: "translateY(-50%)",
zIndex: 10,
display: "flex",
flexDirection: "column",
gap: 0.25,
bgcolor: "transparent",
borderRadius: 0.5,
boxShadow: 0,
p: 0.25,
maxHeight: "70vh",
overflowY: "auto",
minWidth: "auto",
}}
>
{processedGroups.map(({ name, displayChar }) => (
<Tooltip key={name} title={name} placement="left" arrow>
<Button
size="small"
variant="text"
onClick={() => handleGroupClick(name)}
sx={{
minWidth: 28,
minHeight: 28,
width: 28,
height: 28,
fontSize: "12px",
fontWeight: 600,
padding: 0,
borderRadius: 0.25,
color: "text.secondary",
textAlign: "center",
justifyContent: "center",
textTransform: "none",
"&:hover": {
bgcolor: "primary.light",
color: "primary.contrastText",
},
}}
>
{displayChar}
</Button>
</Tooltip>
))}
</Box>
);
};

View File

@@ -25,6 +25,7 @@ import { ScrollTopButton } from "../layout/scroll-top-button";
import { ProxyChain } from "./proxy-chain";
import { ProxyRender } from "./proxy-render";
import { ProxyGroupNavigator } from "./proxy-group-navigator";
import { useRenderList } from "./use-render-list";
import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api";
@@ -318,6 +319,45 @@ export const ProxyGroups = (props: Props) => {
}
};
// 获取运行时配置
const { data: runtimeConfig } = useSWR("getRuntimeConfig", getRuntimeConfig, {
revalidateOnFocus: false,
revalidateIfStale: true,
});
// 获取所有代理组名称
const getProxyGroupNames = useCallback(() => {
const config = runtimeConfig as any;
if (!config?.["proxy-groups"]) return [];
return config["proxy-groups"]
.map((group: any) => group.name)
.filter((name: string) => name && name.trim() !== "");
}, [runtimeConfig]);
// 定位到指定的代理组
const handleGroupLocationByName = useCallback(
(groupName: string) => {
const index = renderList.findIndex(
(item) => item.type === 0 && item.group?.name === groupName,
);
if (index >= 0) {
virtuosoRef.current?.scrollToIndex?.({
index,
align: "start",
behavior: "smooth",
});
}
},
[renderList],
);
const proxyGroupNames = useMemo(
() => getProxyGroupNames(),
[getProxyGroupNames],
);
if (mode === "direct") {
return <BaseEmpty text={t("clash_mode_direct")} />;
}
@@ -514,6 +554,14 @@ export const ProxyGroups = (props: Props) => {
<div
style={{ position: "relative", height: "100%", willChange: "transform" }}
>
{/* 代理组导航栏 */}
{mode === "rule" && (
<ProxyGroupNavigator
proxyGroupNames={proxyGroupNames}
onGroupLocation={handleGroupLocationByName}
/>
)}
<Virtuoso
ref={virtuosoRef}
style={{ height: "calc(100% - 14px)" }}

View File

@@ -31,9 +31,15 @@ interface Props {
onHeadState: (val: Partial<HeadState>) => void;
}
export const ProxyHead = (props: Props) => {
const { sx = {}, url, groupName, headState, onHeadState } = props;
export const ProxyHead = ({
sx = {},
url,
groupName,
headState,
onHeadState,
onLocation,
onCheckDelay,
}: Props) => {
const { showType, sortType, filterText, textState, testUrl } = headState;
const { t } = useTranslation();
@@ -46,13 +52,11 @@ export const ProxyHead = (props: Props) => {
}, []);
const { verge } = useVerge();
const default_latency_test = verge!.default_latency_test!;
useEffect(() => {
delayManager.setUrl(
groupName,
testUrl || url || verge?.default_latency_test!,
);
}, [groupName, testUrl, verge?.default_latency_test]);
delayManager.setUrl(groupName, testUrl || url || default_latency_test);
}, [groupName, testUrl, default_latency_test, url]);
return (
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, ...sx }}>
@@ -60,7 +64,7 @@ export const ProxyHead = (props: Props) => {
size="small"
color="inherit"
title={t("locate")}
onClick={props.onLocation}
onClick={onLocation}
>
<MyLocationRounded />
</IconButton>
@@ -76,7 +80,7 @@ export const ProxyHead = (props: Props) => {
console.log(`[ProxyHead] 使用自定义测试URL: ${testUrl}`);
onHeadState({ textState: "url" });
}
props.onCheckDelay();
onCheckDelay();
}}
>
<NetworkCheckRounded />

View File

@@ -61,12 +61,12 @@ export const ProxyItem = (props: Props) => {
return () => {
delayManager.removeListener(proxy.name, group.name);
};
}, [proxy.name, group.name]);
}, [proxy.name, group.name, isPreset]);
useEffect(() => {
if (!proxy) return;
setDelay(delayManager.getDelayFix(proxy, group.name));
}, [proxy]);
}, [group.name, proxy]);
const onDelay = useLockFn(async () => {
setDelay(-2);

View File

@@ -11,7 +11,7 @@ export default function useFilterSort(
filterText: string,
sortType: ProxySortType,
) {
const [refresh, setRefresh] = useState({});
const [, setRefresh] = useState({});
useEffect(() => {
let last = 0;
@@ -34,7 +34,7 @@ export default function useFilterSort(
const fp = filterProxies(proxies, groupName, filterText);
const sp = sortProxies(fp, groupName, sortType);
return sp;
}, [proxies, groupName, filterText, sortType, refresh]);
}, [proxies, groupName, filterText, sortType]);
}
export function filterSort(

View File

@@ -90,13 +90,11 @@ export const HotkeyInput = (props: Props) => {
<div className="list">
{keys.map((key, index) => (
<Box display="flex">
<Box display="flex" key={key}>
<span className="delimiter" hidden={index === 0}>
+
</span>
<div key={key} className="item">
{key}
</div>
<div className="item">{key}</div>
</Box>
))}
</div>

View File

@@ -59,13 +59,13 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
const onExportDiagnosticInfo = useCallback(async () => {
await exportDiagnosticInfo();
showNotice("success", t("Copy Success"), 1000);
}, []);
}, [t]);
const copyVersion = useCallback(() => {
navigator.clipboard.writeText(`v${version}`).then(() => {
showNotice("success", t("Version copied to clipboard"), 1000);
});
}, [version, t]);
}, [t]);
return (
<SettingList title={t("Verge Advanced Setting")}>

View File

@@ -77,7 +77,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
const onCopyClashEnv = useCallback(async () => {
await copyClashEnv();
showNotice("success", t("Copy Success"), 1000);
}, []);
}, [t]);
return (
<SettingList title={t("Verge Basic Setting")}>