2 Commits

Author SHA1 Message Date
coolcoala
53de1bc8b0 v0.2.4 2025-07-30 09:12:05 +03:00
coolcoala
77eacd3ab3 fixed icons 2025-07-30 09:12:05 +03:00
127 changed files with 2098 additions and 4067 deletions

6
.github/FUNDING.yml vendored
View File

@@ -1,5 +1 @@
custom:
[
"https://t.me/tribute/app?startapp=dtfk",
"https://t.me/tribute/app?startapp=dtLE",
]
github: clash-verge-rev

View File

@@ -193,9 +193,6 @@ jobs:
- os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
env:
CARGO_NET_RETRY: "5"
CARGO_HTTP_CHECK_REVOKE: "false"
steps:
- name: Checkout Repository
uses: actions/checkout@v4

View File

@@ -17,9 +17,6 @@ jobs:
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
env:
CARGO_NET_RETRY: "5"
CARGO_HTTP_CHECK_REVOKE: "false"
steps:
- name: Checkout Repository
uses: actions/checkout@v4

View File

@@ -28,9 +28,6 @@ jobs:
bundle: dmg
runs-on: ${{ matrix.os }}
env:
CARGO_NET_RETRY: "5"
CARGO_HTTP_CHECK_REVOKE: "false"
steps:
- name: Checkout Repository
uses: actions/checkout@v4

View File

@@ -10,9 +10,6 @@ on:
jobs:
rustfmt:
runs-on: ubuntu-latest
env:
CARGO_NET_RETRY: "5"
CARGO_HTTP_CHECK_REVOKE: "false"
steps:
- uses: actions/checkout@v4

View File

@@ -90,18 +90,18 @@ jobs:
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_x64.dmg"><img src="https://img.shields.io/badge/DMG-default?style=flat&logo=apple&label=Intel"></a><br>
> :warning: **Warning**
If you get a notification that the application is corrupted when you run it on macOS, run this command:<br>
<code>sudo xattr -r -c /Applications/Koala\ Clash.app</code>
<code>sudo xattr -r -c /Applications/Clash\ Verge\ Rev\ Lite.app</code>
### Linux
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_amd64.deb"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=debian&label=DEB"> </a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash.x86_64.rpm"><img src="https://img.shields.io/badge/x64-default?style=flat&logo=fedora&label=RPM"> </a>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_arm64.deb"><img src="https://img.shields.io/badge/arm64-default?style=flat&logo=debian&label=DEB"> </a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash.aarch64.rpm"><img src="https://img.shields.io/badge/aarch64-default?style=flat&logo=fedora&label=RPM"> </a>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_armhf.deb"><img src="https://img.shields.io/badge/armhf-default?style=flat&logo=debian&label=DEB"> </a><br>
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash.armhfp.rpm"><img src="https://img.shields.io/badge/armhfp-default?style=flat&logo=fedora&label=RPM"> </a>
### Windows (Win7 is no longer supported)
#### Normal version (recommended)
<a href="https://github.com/coolcoala/clash-verge-rev-lite/releases/download/v${{ env.VERSION }}/Koala.Clash_x64-setup.exe"><img src="https://badgen.net/badge/icon/x64?icon=windows&label=exe"></a><br>
@@ -178,11 +178,6 @@ jobs:
pnpm i
pnpm run prebuild ${{ matrix.target }}
- name: Create .p8 file
run: |
mkdir -p ~/.appstoreconnect/private_keys
echo "${{ secrets.APPLE_API_KEY_CONTENT }}" > ~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8
- name: Tauri build
id: build
uses: tauri-apps/tauri-action@v0
@@ -191,12 +186,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_PATH: "~/.appstoreconnect/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8"
with:
tauriScript: pnpm
args: --target ${{ matrix.target }}
@@ -205,9 +194,10 @@ jobs:
if: runner.os == 'Windows'
shell: pwsh
run: |
$version = ${{steps.build.outputs.appVersion}}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) {
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_"
$newName = $file.Name -replace "_${version}_", "_"
Rename-Item $file.FullName $newName
}
@@ -216,11 +206,11 @@ jobs:
shell: bash
run: |
TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle"
if [ ! -d "$TARGET_DIR" ]; then
exit 1
fi
find "$TARGET_DIR" -type f \( -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" \) -print0 | while IFS= read -r -d '' old_path; do
dir_path=$(dirname "$old_path")
old_filename=$(basename "$old_path")
@@ -246,8 +236,6 @@ jobs:
src-tauri/target/${{ matrix.target }}/release/bundle/rpm/*.rpm
src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup*
src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz
src-tauri/target/${{ matrix.target }}/release/bundle/macos/*.tar.gz.sig
release-for-linux-arm:
name: Release Build for Linux ARM
@@ -364,11 +352,11 @@ jobs:
shell: bash
run: |
TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle"
if [ ! -d "$TARGET_DIR" ]; then
exit 1
fi
find "$TARGET_DIR" -type f \( -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" \) -print0 | while IFS= read -r -d '' old_path; do
dir_path=$(dirname "$old_path")
old_filename=$(basename "$old_path")
@@ -575,16 +563,15 @@ jobs:
UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
else
echo "Using found update logs"
UPDATE_LOGS=$(echo "$UPDATE_LOGS" | sed 's/^## \(v.*\)/\*\1\*/')
fi
cat > release.txt << EOF
Вышло обновление!
$UPDATE_LOGS
[Ссылка на релиз](https://github.com/coolcoala/clash-verge-rev-lite/releases/latest)
EOF
- name: notify to channel
@@ -593,7 +580,6 @@ jobs:
to: ${{ secrets.TELEGRAM_TO_CHANNEL }}
token: ${{ secrets.TELEGRAM_TOKEN }}
message_file: release.txt
format: markdown
- name: notify to group
uses: appleboy/telegram-action@master
@@ -602,3 +588,4 @@ jobs:
token: ${{ secrets.TELEGRAM_TOKEN }}
message_file: release.txt
format: markdown

1
.gitignore vendored
View File

@@ -10,4 +10,3 @@ scripts/_env.sh
.tool-versions
.idea
.old
bun.lock

View File

@@ -1,28 +1,3 @@
## v0.2.6
- fixed deep links
- removed AliDNS, replaced with Cloudflare and Google DNS servers
- improved proxy selector view
- added some animations
- fixed an issue with saving the profile when changing advanced settings
- fixed DNS leak, strict routing now default
- logs translated into English
- table on the connections page corrected
- fixed issue with deleting profiles with long names
- glass effect added to components
- icon background fixed
- fixed tun settings override
- added support for brotli, gzip, zstd
## v0.2.5
- new main page
- fixed issue with opening via shortcut
- fixed logo in sidebar
- fixed issue with changing tray settings
- name changed to koala clash
- added signing for installer on macOS
## v0.2.4
- added auto-scaling and scaling via key combination
@@ -43,6 +18,7 @@
- corrected side menu in compressed window
- added check at the main toggle switch, now it cannot be enabled if there are no profiles.
## v0.2.1
- added headers "announce-url", "update-always"

View File

@@ -1,21 +1,19 @@
import * as React from "react";
import * as React from "react"
const MOBILE_BREAKPOINT = 768;
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined,
);
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile;
return !!isMobile
}

View File

@@ -1,6 +1,6 @@
{
"name": "koala-clash",
"version": "0.2.6",
"name": "clash-verge",
"version": "0.2.4",
"license": "GPL-3.0-only",
"scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev",
@@ -53,7 +53,6 @@
"@tanstack/react-table": "^8.21.3",
"@tauri-apps/api": "2.5.0",
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
"@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-dialog": "^2.2.2",
"@tauri-apps/plugin-fs": "^2.3.0",
"@tauri-apps/plugin-global-shortcut": "^2.2.1",
@@ -74,7 +73,6 @@
"d3-shape": "^3.2.0",
"dayjs": "1.11.13",
"foxact": "^0.2.45",
"framer-motion": "^12.23.12",
"glob": "^11.0.2",
"i18next": "^25.2.1",
"js-base64": "^3.7.7",

49
pnpm-lock.yaml generated
View File

@@ -92,9 +92,6 @@ importers:
'@tauri-apps/plugin-clipboard-manager':
specifier: ^2.2.2
version: 2.3.0
'@tauri-apps/plugin-deep-link':
specifier: ~2
version: 2.4.1
'@tauri-apps/plugin-dialog':
specifier: ^2.2.2
version: 2.3.0
@@ -155,9 +152,6 @@ importers:
foxact:
specifier: ^0.2.45
version: 0.2.49(react@19.1.0)
framer-motion:
specifier: ^12.23.12
version: 12.23.12(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
glob:
specifier: ^11.0.2
version: 11.0.3
@@ -2205,9 +2199,6 @@ packages:
'@tauri-apps/plugin-clipboard-manager@2.3.0':
resolution: {integrity: sha512-81NOBA2P+OTY8RLkBwyl9ZR/0CeggLub4F6zxcxUIfFOAqtky7J61+K/MkH2SC1FMxNBxrX0swDuKvkjkHadlA==}
'@tauri-apps/plugin-deep-link@2.4.1':
resolution: {integrity: sha512-I8Bo+spcAKGhIIJ1qN/gapp/Ot3mosQL98znxr975Zn2ODAkUZ++BQ9FnTpR7PDwfIl5ANSGdIW/YU01zVTcJw==}
'@tauri-apps/plugin-dialog@2.3.0':
resolution: {integrity: sha512-ylSBvYYShpGlKKh732ZuaHyJ5Ie1JR71QCXewCtsRLqGdc8Is4xWdz6t43rzXyvkItM9syNPMvFVcvjgEy+/GA==}
@@ -2757,20 +2748,6 @@ packages:
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
framer-motion@12.23.12:
resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -3218,12 +3195,6 @@ packages:
peerDependencies:
monaco-editor: '>=0.36'
motion-dom@12.23.12:
resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==}
motion-utils@12.23.6:
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
@@ -5808,10 +5779,6 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.6.0
'@tauri-apps/plugin-deep-link@2.4.1':
dependencies:
'@tauri-apps/api': 2.6.0
'@tauri-apps/plugin-dialog@2.3.0':
dependencies:
'@tauri-apps/api': 2.6.0
@@ -6394,16 +6361,6 @@ snapshots:
fraction.js@4.3.7: {}
framer-motion@12.23.12(@emotion/is-prop-valid@1.3.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
motion-dom: 12.23.12
motion-utils: 12.23.6
tslib: 2.8.1
optionalDependencies:
'@emotion/is-prop-valid': 1.3.1
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
fsevents@2.3.3:
optional: true
@@ -6957,12 +6914,6 @@ snapshots:
vscode-uri: 3.1.0
yaml: 2.7.1
motion-dom@12.23.12:
dependencies:
motion-utils: 12.23.6
motion-utils@12.23.6: {}
mri@1.2.0: {}
ms@2.1.3: {}

View File

@@ -42,9 +42,9 @@ async function resolvePortable() {
const zip = new AdmZip();
zip.addLocalFile(path.join(releaseDir, "Koala Clash.exe"));
zip.addLocalFile(path.join(releaseDir, "koala-mihomo.exe"));
zip.addLocalFile(path.join(releaseDir, "koala-mihomo-alpha.exe"));
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe"));
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
zip.addLocalFolder(
path.join(

View File

@@ -35,9 +35,9 @@ async function resolvePortable() {
}
const zip = new AdmZip();
zip.addLocalFile(path.join(releaseDir, "koala-clash.exe"));
zip.addLocalFile(path.join(releaseDir, "koala-mihomo.exe"));
zip.addLocalFile(path.join(releaseDir, "koala-mihomo-alpha.exe"));
zip.addLocalFile(path.join(releaseDir, "clash-verge.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe"));
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
zip.addLocalFolder(configDir, ".config");

View File

@@ -175,8 +175,8 @@ function clashMetaAlpha() {
const zipFile = `${name}-${META_ALPHA_VERSION}.${urlExt}`;
return {
name: "koala-mihomo-alpha",
targetFile: `koala-mihomo-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
name: "verge-mihomo-alpha",
targetFile: `verge-mihomo-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
exeFile,
zipFile,
downloadURL,
@@ -192,8 +192,8 @@ function clashMeta() {
const zipFile = `${name}-${META_VERSION}.${urlExt}`;
return {
name: "koala-mihomo",
targetFile: `koala-mihomo-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
name: "verge-mihomo",
targetFile: `verge-mihomo-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
exeFile,
zipFile,
downloadURL,
@@ -381,7 +381,7 @@ const resolvePlugin = async () => {
// service chmod
const resolveServicePermission = async () => {
const serviceExecutables = [
"koala-clash-service*",
"clash-verge-service*",
"install-service*",
"uninstall-service*",
];
@@ -429,14 +429,14 @@ async function resolveLocales() {
/**
* main
*/
const SERVICE_URL = `https://github.com/coolcoala/koala-clash-service/releases/download/${SIDECAR_HOST}`;
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
const resolveService = () => {
let ext = platform === "win32" ? ".exe" : "";
let suffix = platform === "linux" ? "-" + SIDECAR_HOST : "";
resolveResource({
file: "koala-clash-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/koala-clash-service${ext}`,
file: "clash-verge-service" + suffix + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
});
};
@@ -489,13 +489,13 @@ const resolveWinSysproxy = () =>
const tasks = [
// { name: "clash", func: resolveClash, retry: 5 },
{
name: "koala-mihomo-alpha",
name: "verge-mihomo-alpha",
func: () =>
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())),
retry: 5,
},
{
name: "koala-mihomo",
name: "verge-mihomo",
func: () =>
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
retry: 5,

214
src-tauri/Cargo.lock generated
View File

@@ -256,22 +256,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "async-compression"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8"
dependencies = [
"brotli",
"flate2",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
"zstd",
"zstd-safe",
]
[[package]]
name = "async-executor"
version = "1.13.2"
@@ -1075,6 +1059,80 @@ dependencies = [
"inout",
]
[[package]]
name = "clash-verge"
version = "0.2.4"
dependencies = [
"ab_glyph",
"aes-gcm",
"anyhow",
"async-trait",
"base64 0.22.1",
"boa_engine",
"chrono",
"deelevate",
"delay_timer",
"dirs 6.0.0",
"dunce",
"futures",
"gethostname 1.0.2",
"getrandom 0.3.3",
"hex",
"hmac",
"image",
"imageproc",
"lazy_static",
"libc",
"log",
"log4rs",
"machine-uid",
"mihomo_api",
"nanoid",
"network-interface",
"once_cell",
"open",
"os_info",
"parking_lot",
"percent-encoding",
"port_scanner",
"regex",
"reqwest",
"reqwest_dav",
"runas",
"scopeguard",
"serde",
"serde_json",
"serde_yaml",
"sha2 0.10.9",
"sys-locale",
"sysinfo",
"sysproxy",
"tauri",
"tauri-build",
"tauri-plugin-autostart",
"tauri-plugin-clipboard-manager",
"tauri-plugin-deep-link",
"tauri-plugin-devtools",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-global-shortcut",
"tauri-plugin-notification",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"tempfile",
"tokio",
"tokio-tungstenite 0.27.0",
"tungstenite 0.27.0",
"url",
"users",
"warp",
"winapi",
"winreg 0.55.0",
"zip",
]
[[package]]
name = "clipboard-win"
version = "5.4.0"
@@ -3564,81 +3622,6 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "koala-clash"
version = "0.2.6"
dependencies = [
"ab_glyph",
"aes-gcm",
"anyhow",
"async-trait",
"base64 0.22.1",
"boa_engine",
"chrono",
"deelevate",
"delay_timer",
"dirs 6.0.0",
"dunce",
"futures",
"gethostname 1.0.2",
"getrandom 0.3.3",
"hex",
"hmac",
"image",
"imageproc",
"lazy_static",
"libc",
"log",
"log4rs",
"machine-uid",
"mihomo_api",
"nanoid",
"network-interface",
"once_cell",
"open",
"os_info",
"parking_lot",
"percent-encoding",
"port_scanner",
"regex",
"reqwest",
"reqwest_dav",
"runas",
"scopeguard",
"serde",
"serde_json",
"serde_yaml",
"sha2 0.10.9",
"sys-locale",
"sysinfo",
"sysproxy",
"tauri",
"tauri-build",
"tauri-plugin-autostart",
"tauri-plugin-clipboard-manager",
"tauri-plugin-deep-link",
"tauri-plugin-devtools",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-global-shortcut",
"tauri-plugin-notification",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"tempfile",
"tokio",
"tokio-tungstenite 0.27.0",
"tungstenite 0.27.0",
"url",
"users",
"warp",
"winapi",
"winreg 0.55.0",
"zip",
]
[[package]]
name = "kuchikiki"
version = "0.8.8-speedreader"
@@ -3904,12 +3887,11 @@ dependencies = [
[[package]]
name = "machine-uid"
version = "0.5.3"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4506fa0abb0a2ea93f5862f55973da0a662d2ad0e98f337a1c5aac657f0892"
checksum = "1f1595709b0a7386bcd56ba34d250d626e5503917d05d32cdccddcd68603e212"
dependencies = [
"libc",
"winreg 0.52.0",
"winreg 0.6.2",
]
[[package]]
@@ -5869,7 +5851,6 @@ version = "0.12.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
dependencies = [
"async-compression",
"base64 0.22.1",
"bytes",
"cookie",
@@ -6865,9 +6846,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.36.1"
version = "0.35.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d"
checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e"
dependencies = [
"libc",
"memchr",
@@ -7163,9 +7144,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-deep-link"
version = "2.4.1"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fec67f32d7a06d80bd3dc009fdb678c35a66116d9cb8cd2bb32e406c2b5bbd2"
checksum = "ab261eb006db10ab478e3fbb5a4e2692df3f7eb3e28300ee2b64428979167ed0"
dependencies = [
"dunce",
"rust-ini",
@@ -7313,22 +7294,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "tauri-plugin-single-instance"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50a0e5a4ce43cb3a733c3aef85e8478bc769dac743c615e26639cbf5d953faf7"
dependencies = [
"serde",
"serde_json",
"tauri",
"tauri-plugin-deep-link",
"thiserror 2.0.12",
"tracing",
"windows-sys 0.60.2",
"zbus",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.9.0"
@@ -9358,6 +9323,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9"
dependencies = [
"winapi",
]
[[package]]
name = "winreg"
version = "0.10.1"
@@ -9594,9 +9568,9 @@ dependencies = [
[[package]]
name = "zbus"
version = "5.9.0"
version = "5.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad"
checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68"
dependencies = [
"async-broadcast",
"async-executor",
@@ -9628,9 +9602,9 @@ dependencies = [
[[package]]
name = "zbus_macros"
version = "5.9.0"
version = "5.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659"
checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a"
dependencies = [
"proc-macro-crate 3.3.0",
"proc-macro2",

View File

@@ -1,16 +1,16 @@
[package]
name = "koala-clash"
version = "0.2.6"
description = "koala clash"
name = "clash-verge"
version = "0.2.4"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda", "coolcoala"]
license = "GPL-3.0-only"
repository = "https://github.com/coolcoala/clash-verge-rev-lite.git"
default-run = "koala-clash"
default-run = "clash-verge"
edition = "2021"
build = "build.rs"
[package.metadata.bundle]
identifier = "io.github.koala-clash"
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies]
tauri-build = { version = "2.3.0", features = [] }
@@ -18,7 +18,7 @@ tauri-build = { version = "2.3.0", features = [] }
[dependencies]
url = "2.5.4"
os_info = "3.0"
machine-uid = "0.5.3"
machine-uid = "0.2"
warp = "0.3.7"
anyhow = "1.0.98"
dirs = "6.0"
@@ -28,7 +28,7 @@ dunce = "1.0.5"
log4rs = "1.3.0"
nanoid = "0.4"
chrono = "0.4.41"
sysinfo = "0.36.1"
sysinfo = "0.35.2"
boa_engine = "0.20.0"
serde_json = "1.0.140"
serde_yaml = "0.9.34-deprecated"
@@ -45,7 +45,7 @@ tokio = { version = "1.45.1", features = [
"sync",
] }
serde = { version = "1.0.219", features = ["derive"] }
reqwest = { version = "0.12.20", features = ["json", "rustls-tls", "cookies", "brotli", "gzip", "zstd"] }
reqwest = { version = "0.12.20", features = ["json", "rustls-tls", "cookies"] }
regex = "1.11.1"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
image = "0.25.6"
@@ -63,6 +63,7 @@ tauri-plugin-dialog = "2.3.0"
tauri-plugin-fs = "2.4.0"
tauri-plugin-process = "2.3.0"
tauri-plugin-clipboard-manager = "2.3.0"
tauri-plugin-deep-link = "2.4.0"
tauri-plugin-devtools = "2.0.0"
tauri-plugin-window-state = "2.3.0"
zip = "4.2.0"
@@ -84,7 +85,6 @@ sha2 = "0.10.9"
hex = "0.4.3"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.3.0"
tauri-plugin-deep-link = "2"
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"
@@ -110,7 +110,6 @@ users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.5.0"
tauri-plugin-global-shortcut = "2.3.0"
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }
tauri-plugin-updater = "2.9.0"
[features]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,4 +1,4 @@
#!/bin/bash
chmod +x /usr/bin/install-service
chmod +x /usr/bin/uninstall-service
chmod +x /usr/bin/koala-clash-service
chmod +x /usr/bin/clash-verge-service

View File

@@ -6,7 +6,7 @@
<false/>
<key>com.apple.security.application-groups</key>
<array>
<string>io.github.koala-clash</string>
<string>io.github.clash-verge-rev.clash-verge-rev</string>
</array>
<key>com.apple.security.inherit</key>
<true/>

View File

@@ -427,52 +427,52 @@ Function .onInit
!endif
FunctionEnd
!macro CheckAllKoalaProcesses
; Check if koala-clash-service.exe is running
!macro CheckAllVergeProcesses
; Check if clash-verge-service.exe is running
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::FindProcessCurrentUser "koala-clash-service.exe"
nsis_tauri_utils::FindProcessCurrentUser "clash-verge-service.exe"
!else
nsis_tauri_utils::FindProcess "koala-clash-service.exe"
nsis_tauri_utils::FindProcess "clash-verge-service.exe"
!endif
Pop $R0
${If} $R0 = 0
DetailPrint "Kill koala-clash-service.exe..."
DetailPrint "Kill clash-verge-service.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "koala-clash-service.exe"
nsis_tauri_utils::KillProcessCurrentUser "clash-verge-service.exe"
!else
nsis_tauri_utils::KillProcess "koala-clash-service.exe"
nsis_tauri_utils::KillProcess "clash-verge-service.exe"
!endif
${EndIf}
; Check if koala-mihomo-alpha.exe is running
; Check if verge-mihomo-alpha.exe is running
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::FindProcessCurrentUser "koala-mihomo-alpha.exe"
nsis_tauri_utils::FindProcessCurrentUser "verge-mihomo-alpha.exe"
!else
nsis_tauri_utils::FindProcess "koala-mihomo-alpha.exe"
nsis_tauri_utils::FindProcess "verge-mihomo-alpha.exe"
!endif
Pop $R0
${If} $R0 = 0
DetailPrint "Kill koala-mihomo-alpha.exe..."
DetailPrint "Kill verge-mihomo-alpha.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "koala-mihomo-alpha.exe"
nsis_tauri_utils::KillProcessCurrentUser "verge-mihomo-alpha.exe"
!else
nsis_tauri_utils::KillProcess "koala-mihomo-alpha.exe"
nsis_tauri_utils::KillProcess "verge-mihomo-alpha.exe"
!endif
${EndIf}
; Check if koala-mihomo.exe is running
; Check if verge-mihomo.exe is running
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::FindProcessCurrentUser "koala-mihomo.exe"
nsis_tauri_utils::FindProcessCurrentUser "verge-mihomo.exe"
!else
nsis_tauri_utils::FindProcess "koala-mihomo.exe"
nsis_tauri_utils::FindProcess "verge-mihomo.exe"
!endif
Pop $R0
${If} $R0 = 0
DetailPrint "Kill koala-mihomo.exe..."
DetailPrint "Kill verge-mihomo.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "koala-mihomo.exe"
nsis_tauri_utils::KillProcessCurrentUser "verge-mihomo.exe"
!else
nsis_tauri_utils::KillProcess "koala-mihomo.exe"
nsis_tauri_utils::KillProcess "verge-mihomo.exe"
!endif
${EndIf}
@@ -509,22 +509,22 @@ FunctionEnd
${EndIf}
!macroend
!macro StartKoalaService
!macro StartVergeService
; Check if the service exists
SimpleSC::ExistsService "koala_clash_service"
SimpleSC::ExistsService "clash_verge_service"
Pop $0 ; 0service existsother: service not exists
; Service exists
${If} $0 == 0
Push $0
; Check if the service is running
SimpleSC::ServiceIsRunning "koala_clash_service"
SimpleSC::ServiceIsRunning "clash_verge_service"
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
${If} $0 == 0
Push $0
${If} $1 == 0
DetailPrint "Restart Koala Clash Service..."
SimpleSC::StartService "koala_clash_service" "" 30
DetailPrint "Restart Clash Verge Service..."
SimpleSC::StartService "clash_verge_service" "" 30
${EndIf}
${ElseIf} $0 != 0
Push $0
@@ -535,35 +535,35 @@ FunctionEnd
${EndIf}
!macroend
!macro RemoveKoalaService
!macro RemoveVergeService
; Check if the service exists
SimpleSC::ExistsService "koala_clash_service"
SimpleSC::ExistsService "clash_verge_service"
Pop $0 ; 0service existsother: service not exists
; Service exists
${If} $0 == 0
Push $0
; Check if the service is running
SimpleSC::ServiceIsRunning "koala_clash_service"
SimpleSC::ServiceIsRunning "clash_verge_service"
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
${If} $0 == 0
Push $0
${If} $1 == 1
DetailPrint "Stop Koala Clash Service..."
SimpleSC::StopService "koala_clash_service" 1 30
DetailPrint "Stop Clash Verge Service..."
SimpleSC::StopService "clash_verge_service" 1 30
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
${If} $0 == 0
DetailPrint "Removing Koala Clash Service..."
SimpleSC::RemoveService "koala_clash_service"
DetailPrint "Removing Clash Verge Service..."
SimpleSC::RemoveService "clash_verge_service"
${ElseIf} $0 != 0
Push $0
SimpleSC::GetErrorMessage
Pop $0
MessageBox MB_OK|MB_ICONSTOP "Koala Clash Service Stop Error ($0)"
MessageBox MB_OK|MB_ICONSTOP "Clash Verge Service Stop Error ($0)"
${EndIf}
${ElseIf} $1 == 0
DetailPrint "Removing Koala Clash Service..."
SimpleSC::RemoveService "koala_clash_service"
DetailPrint "Removing Clash Verge Service..."
SimpleSC::RemoveService "clash_verge_service"
${EndIf}
${ElseIf} $0 != 0
Push $0
@@ -764,7 +764,7 @@ Section Install
SetOutPath $INSTDIR
nsExec::Exec 'netsh int tcp res'
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllKoalaProcesses
!insertmacro CheckAllVergeProcesses
; 清理自启动注册表项
DetailPrint "Cleaning auto-launch registry entries..."
@@ -772,32 +772,32 @@ Section Install
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
SetRegView 64
; 清理旧版本的注册表项 (Koala Clash)
ReadRegStr $R2 HKCU "$R1" "Koala Clash"
; 清理旧版本的注册表项 (Clash Verge)
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "Koala Clash"
DeleteRegValue HKCU "$R1" "Clash Verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "Koala Clash"
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "Koala Clash"
DeleteRegValue HKLM "$R1" "Clash Verge"
${EndIf}
; 清理新版本的注册表项 (koala-clash)
ReadRegStr $R2 HKCU "$R1" "koala-clash"
; 清理新版本的注册表项 (clash-verge)
ReadRegStr $R2 HKCU "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "koala-clash"
DeleteRegValue HKCU "$R1" "clash-verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "koala-clash"
ReadRegStr $R2 HKLM "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "koala-clash"
DeleteRegValue HKLM "$R1" "clash-verge"
${EndIf}
; Delete old files before installation
; Delete koala-clash.desktop
IfFileExists "$INSTDIR\Koala Clash.exe" 0 +2
Delete "$INSTDIR\Koala Clash.exe"
; Delete clash-verge.desktop
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
Delete "$INSTDIR\Clash Verge.exe"
; Copy main executable
File "${MAINBINARYSRCPATH}"
@@ -815,7 +815,7 @@ Section Install
File /a "/oname={{this}}" "{{@key}}"
{{/each}}
!insertmacro StartKoalaService
!insertmacro StartVergeService
; Create uninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
@@ -918,11 +918,11 @@ FunctionEnd
Section Uninstall
;删除 window-state.json 文件
SetShellVarContext current
Delete "$APPDATA\io.github.koala-clash\window-state.json"
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllKoalaProcesses
!insertmacro RemoveKoalaService
!insertmacro CheckAllVergeProcesses
!insertmacro RemoveVergeService
; 清理自启动注册表项
DetailPrint "Cleaning auto-launch registry entries..."
@@ -930,26 +930,26 @@ Section Uninstall
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
SetRegView 64
; 清理旧版本的注册表项 (Koala Clash)
ReadRegStr $R2 HKCU "$R1" "Koala Clash"
; 清理旧版本的注册表项 (Clash Verge)
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "Koala Clash"
DeleteRegValue HKCU "$R1" "Clash Verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "Koala Clash"
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "Koala Clash"
DeleteRegValue HKLM "$R1" "Clash Verge"
${EndIf}
; 清理新版本的注册表项 (koala-clash)
ReadRegStr $R2 HKCU "$R1" "koala-clash"
; 清理新版本的注册表项 (clash-verge)
ReadRegStr $R2 HKCU "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "koala-clash"
DeleteRegValue HKCU "$R1" "clash-verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "koala-clash"
ReadRegStr $R2 HKLM "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "koala-clash"
DeleteRegValue HKLM "$R1" "clash-verge"
${EndIf}
; Delete the app directory and its content from disk
@@ -966,9 +966,9 @@ Section Uninstall
Delete "$INSTDIR\\{{this}}"
{{/each}}
; Delete koala-clash.desktop
IfFileExists "$INSTDIR\Koala Clash.exe" 0 +2
Delete "$INSTDIR\Koala Clash.exe"
; Delete clash-verge.desktop
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
Delete "$INSTDIR\Clash Verge.exe"
; Delete uninstaller
Delete "$INSTDIR\uninstall.exe"
@@ -982,20 +982,20 @@ Section Uninstall
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
!insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk"
; 兼容旧名称快捷方式
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\koala-clash.lnk"
!insertmacro UnpinShortcut "$DESKTOP\koala-clash.lnk"
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
!insertmacro UnpinShortcut "$DESKTOP\clash-verge.lnk"
; Remove start menu shortcut
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
; 兼容旧名称快捷方式
Delete "$SMPROGRAMS\$AppStartMenuFolder\koala-clash.lnk"
Delete "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
; Remove desktop shortcuts
Delete "$DESKTOP\${PRODUCTNAME}.lnk"
; 兼容旧名称快捷方式
Delete "$DESKTOP\koala-clash.lnk"
Delete "$DESKTOP\clash-verge.lnk"
; Remove registry information for add/remove programs
!if "${INSTALLMODE}" == "both"
@@ -1017,7 +1017,7 @@ Section Uninstall
;删除 window-state.json 文件
SetShellVarContext current
Delete "$APPDATA\io.github.koala-clash\window-state.json"
Delete "$APPDATA\io.github.clash-verge-rev.clash-verge-rev\window-state.json"
${GetOptions} $CMDLINE "/P" $R0
IfErrors +2 0

View File

@@ -147,7 +147,7 @@ pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String>
Ok(icon_path.to_string_lossy().to_string())
} else {
let _ = std::fs::remove_file(&temp_path);
Err(format!("Downloaded content is not a valid image: {url}"))
Err(format!("下载的内容不是有效图片: {url}"))
}
}
@@ -209,17 +209,15 @@ pub fn copy_icon_file(path: String, icon_info: IconInfo) -> CmdResult<String> {
/// 通知UI已准备就绪
#[tauri::command]
pub fn notify_ui_ready() -> CmdResult<()> {
log::info!(target: "app", "Frontend UI is ready");
log::info!(target: "app", "前端UI已准备就绪");
crate::utils::resolve::mark_ui_ready();
// Flush any pending messages queued while UI was not ready (e.g. minimized to tray)
crate::core::handle::Handle::global().flush_ui_pending_messages();
Ok(())
}
/// UI加载阶段
#[tauri::command]
pub fn update_ui_stage(stage: String) -> CmdResult<()> {
log::info!(target: "app", "UI loading stage updated: {stage}");
log::info!(target: "app", "UI加载阶段更新: {stage}");
use crate::utils::resolve::UiReadyStage;
@@ -230,8 +228,8 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
"Ready" => UiReadyStage::Ready,
_ => {
log::warn!(target: "app", "Unknown UI loading stage: {stage}");
return Err(format!("Unknown UI loading stage: {stage}"));
log::warn!(target: "app", "未知的UI加载阶段: {stage}");
return Err(format!("未知的UI加载阶段: {stage}"));
}
};
@@ -242,7 +240,7 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
/// 重置UI就绪状态
#[tauri::command]
pub fn reset_ui_ready_state() -> CmdResult<()> {
log::info!(target: "app", "Reset UI ready state");
log::info!(target: "app", "重置UI就绪状态");
crate::utils::resolve::reset_ui_ready();
Ok(())
}

View File

@@ -7,7 +7,7 @@ use serde_yaml::Mapping;
/// get the system proxy
#[tauri::command]
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
log::debug!(target: "app", "Asynchronously getting system proxy configuration");
log::debug!(target: "app", "异步获取系统代理配置");
let current = AsyncProxyQuery::get_system_proxy().await;
@@ -19,14 +19,14 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
);
map.insert("bypass".into(), current.bypass.into());
log::debug!(target: "app", "Return system proxy configuration: enable={}, {}:{}", current.enable, current.host, current.port);
log::debug!(target: "app", "返回系统代理配置: enable={}, {}:{}", current.enable, current.host, current.port);
Ok(map)
}
/// 获取自动代理配置
#[tauri::command]
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
log::debug!(target: "app", "Start retrieving auto proxy configuration (event-driven)");
log::debug!(target: "app", "开始获取自动代理配置(事件驱动)");
let proxy_manager = EventDrivenProxyManager::global();
@@ -40,7 +40,7 @@ pub async fn get_auto_proxy() -> CmdResult<Mapping> {
map.insert("enable".into(), current.enable.into());
map.insert("url".into(), current.url.clone().into());
log::debug!(target: "app", "Return auto proxy configuration (cached): enable={}, url={}", current.enable, current.url);
log::debug!(target: "app", "返回自动代理配置(缓存): enable={}, url={}", current.enable, current.url);
Ok(map)
}

View File

@@ -6,14 +6,14 @@ use crate::{
utils::{dirs, help, logging::Type},
wrap_err,
};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use percent_encoding::percent_decode_str;
use serde_yaml::Value;
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use tokio::sync::{Mutex, RwLock};
use std::collections::BTreeMap;
use url::Url;
use serde_yaml::Value;
use base64::{engine::general_purpose::STANDARD, Engine as _};
use percent_encoding::percent_decode_str;
// 全局互斥锁防止并发配置更新
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
@@ -30,7 +30,7 @@ async fn cleanup_processing_state(sequence: u64, reason: &str) {
info,
Type::Cmd,
true,
"{}Cleanup status, serial number: {}",
"{}清理状态,序列号: {}",
reason,
sequence
);
@@ -55,14 +55,14 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
match latest_result {
Ok(Ok(profiles)) => {
logging!(info, Type::Cmd, false, "Quickly fetched profiles list successfully");
logging!(info, Type::Cmd, false, "快速获取配置列表成功");
return Ok(profiles);
}
Ok(Err(join_err)) => {
logging!(warn, Type::Cmd, true, "Quick profile list fetch task failed: {}", join_err);
logging!(warn, Type::Cmd, true, "快速获取配置任务失败: {}", join_err);
}
Err(_) => {
logging!(warn, Type::Cmd, true, "Quick profile list fetch timeout (500ms)");
logging!(warn, Type::Cmd, true, "快速获取配置超时(500ms)");
}
}
@@ -82,7 +82,7 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
match data_result {
Ok(Ok(profiles)) => {
logging!(info, Type::Cmd, false, "Fetched draft profile list successfully");
logging!(info, Type::Cmd, false, "获取draft配置列表成功");
return Ok(profiles);
}
Ok(Err(join_err)) => {
@@ -90,12 +90,12 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
error,
Type::Cmd,
true,
"Failed to obtain draft configuration task: {}",
"获取draft配置任务失败: {}",
join_err
);
}
Err(_) => {
logging!(error, Type::Cmd, true, "Draft profile list fetch timeout (2s)");
logging!(error, Type::Cmd, true, "获取draft配置超时(2)");
}
}
@@ -104,16 +104,16 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
warn,
Type::Cmd,
true,
"All attempts to obtain configuration policies failed. Trying fallback"
"所有获取配置策略都失败,尝试fallback"
);
match tokio::task::spawn_blocking(IProfiles::new).await {
Ok(profiles) => {
logging!(info, Type::Cmd, true, "Fallback profiles created successfully");
logging!(info, Type::Cmd, true, "使用fallback配置成功");
Ok(profiles)
}
Err(err) => {
logging!(error, Type::Cmd, true, "Fallback profiles creation failed: {}", err);
logging!(error, Type::Cmd, true, "fallback配置也失败: {}", err);
// 返回空配置避免崩溃
Ok(IProfiles {
current: None,
@@ -138,43 +138,20 @@ pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult
let profiles = Config::profiles();
let profiles = profiles.latest();
profiles
.items
.as_ref()
profiles.items.as_ref()
.and_then(|items| items.iter().find(|item| item.url.as_deref() == Some(&url)))
.and_then(|item| item.uid.clone())
};
if let Some(uid) = existing_uid {
logging!(
info,
Type::Cmd,
true,
"The profile with URL {} already exists (UID: {}). Running the update...",
url,
uid
);
logging!(info, Type::Cmd, true, "The profile with URL {} already exists (UID: {}). Running the update...", url, uid);
update_profile(uid, option).await
} else {
logging!(
info,
Type::Cmd,
true,
"Profile with URL {} not found. Create a new one...",
url
);
logging!(info, Type::Cmd, true, "Profile with URL {} not found. Create a new one...", url);
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
let new_uid = item.uid.clone().unwrap_or_default();
wrap_err!(Config::profiles().data().append_item(item))?;
if !new_uid.is_empty() {
let _ = patch_profiles_config(IProfiles {
current: Some(new_uid),
items: None,
})
.await?;
}
Ok(())
wrap_err!(Config::profiles().data().append_item(item))
}
}
/// 重新排序配置文件
@@ -187,17 +164,7 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
#[tauri::command]
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
let new_uid = item.uid.clone().unwrap_or_default();
wrap_err!(Config::profiles().data().append_item(item))?;
if !new_uid.is_empty() {
let _ = patch_profiles_config(IProfiles {
current: Some(new_uid),
items: None,
})
.await?;
}
Ok(())
wrap_err!(Config::profiles().data().append_item(item))
}
/// 更新配置文件
@@ -209,43 +176,7 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
/// 删除配置文件
#[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult {
let should_update;
{
let profiles_config = Config::profiles();
let mut profiles_data = profiles_config.data();
should_update = profiles_data
.delete_item(index.clone())
.map_err(|e| e.to_string())?;
let was_last_profile = profiles_data.items.as_ref().is_none_or(|items| {
!items
.iter()
.any(|item| matches!(item.itype.as_deref(), Some("remote") | Some("local")))
});
if was_last_profile {
logging!(
info,
Type::Cmd,
true,
"The last profile has been deleted. Disabling proxy modes..."
);
let verge_config = Config::verge();
let mut verge_data = verge_config.data();
if verge_data.enable_tun_mode == Some(true)
|| verge_data.enable_system_proxy == Some(true)
{
verge_data.enable_tun_mode = Some(false);
verge_data.enable_system_proxy = Some(false);
verge_data.save_file().map_err(|e| e.to_string())?;
handle::Handle::refresh_verge();
handle::Handle::notice_message("info", "All profiles deleted, proxy disabled.");
}
}
}
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
// 删除后自动清理冗余文件
let _ = Config::profiles().latest().auto_cleanup();
@@ -268,7 +199,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info,
Type::Cmd,
true,
"Starting to modify profiles, sequence: {}, target profile: {:?}",
"开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
current_sequence,
target_profile
);
@@ -285,7 +216,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info,
Type::Cmd,
true,
"Newer request detected (seq: {} < {}), abandoning current",
"检测到更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
@@ -295,7 +226,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info,
Type::Cmd,
true,
"Force acquiring lock to process latest request: {}",
"强制获取锁以处理最新请求: {}",
current_sequence
);
PROFILE_UPDATE_MUTEX.lock().await
@@ -308,7 +239,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info,
Type::Cmd,
true,
"After acquiring lock, found newer request (seq: {} < {}), abandoning current",
"获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
@@ -317,12 +248,12 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
// 保存当前配置,以便在验证失败时恢复
let current_profile = Config::profiles().latest().current.clone();
logging!(info, Type::Cmd, true, "Current profile: {:?}", current_profile);
logging!(info, Type::Cmd, true, "当前配置: {:?}", current_profile);
// 如果要切换配置,先检查目标配置文件是否有语法错误
if let Some(new_profile) = profiles.current.as_ref() {
if current_profile.as_ref() != Some(new_profile) {
logging!(info, Type::Cmd, true, "Switching to new profile: {}", new_profile);
logging!(info, Type::Cmd, true, "正在切换到新配置: {}", new_profile);
// 获取目标配置文件路径
let config_file_result = {
@@ -338,7 +269,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
}
}
Err(e) => {
logging!(error, Type::Cmd, true, "Failed to get target profile info: {}", e);
logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
None
}
}
@@ -351,7 +282,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
error,
Type::Cmd,
true,
"Target profile does not exist: {}",
"目标配置文件不存在: {}",
file_path.display()
);
handle::Handle::notice_message(
@@ -377,7 +308,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
match yaml_parse_result {
Ok(Ok(_)) => {
logging!(info, Type::Cmd, true, "Target profile file syntax is correct");
logging!(info, Type::Cmd, true, "目标配置文件语法正确");
}
Ok(Err(err)) => {
let error_msg = format!(" {err}");
@@ -385,7 +316,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
error,
Type::Cmd,
true,
"YAML syntax error in target profile file: {}",
"目标配置文件存在YAML语法错误:{}",
error_msg
);
handle::Handle::notice_message(
@@ -395,7 +326,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
return Ok(false);
}
Err(join_err) => {
let error_msg = format!("YAML parse task failed: {join_err}");
let error_msg = format!("YAML解析任务失败: {join_err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::yaml_parse_error",
@@ -406,7 +337,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
}
}
Ok(Err(err)) => {
let error_msg = format!("Failed to read target profile file: {err}");
let error_msg = format!("无法读取目标配置文件: {err}");
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_error",
@@ -415,7 +346,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
return Ok(false);
}
Err(_) => {
let error_msg = "Reading config file timed out (5s)".to_string();
let error_msg = "读取配置文件超时(5)".to_string();
logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message(
"config_validate::file_read_timeout",
@@ -435,7 +366,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info,
Type::Cmd,
true,
"Found newer request before core operation (seq: {} < {}), abandoning current",
"在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
@@ -448,7 +379,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info,
Type::Cmd,
true,
"Set current processing profile: {}, serial number: {}",
"设置当前处理profile: {}, 序列号: {}",
profile,
current_sequence
);
@@ -459,7 +390,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info,
Type::Cmd,
true,
"Updating draft profiles, sequence: {}",
"正在更新配置草稿,序列号: {}",
current_sequence
);
@@ -474,7 +405,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info,
Type::Cmd,
true,
"Detect updated requests before kernel interaction (sequence number: {} < {}) and abandon the current request.",
"在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence,
latest_sequence
);
@@ -487,7 +418,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info,
Type::Cmd,
true,
"Starting kernel config update, sequence: {}",
"开始内核配置更新,序列号: {}",
current_sequence
);
let update_result = tokio::time::timeout(
@@ -506,7 +437,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info,
Type::Cmd,
true,
"After kernel operation, an updated request was found (sequence number: {} < {}), ignore the current result.",
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
current_sequence,
latest_sequence
);
@@ -518,7 +449,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info,
Type::Cmd,
true,
"Configuration update successful, serial number: {}",
"配置更新成功,序列号: {}",
current_sequence
);
Config::profiles().apply();
@@ -527,22 +458,22 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
// 强制刷新代理缓存确保profile切换后立即获取最新节点数据
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = super::proxy::force_refresh_proxies().await {
log::warn!(target: "app", "Force refresh proxy cache failed: {e}");
log::warn!(target: "app", "强制刷新代理缓存失败: {e}");
}
});
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = Tray::global().update_tooltip() {
log::warn!(target: "app", "Async tray tooltip update failed: {e}");
log::warn!(target: "app", "异步更新托盘提示失败: {e}");
}
if let Err(e) = Tray::global().update_menu() {
log::warn!(target: "app", "Async tray menu update failed: {e}");
log::warn!(target: "app", "异步更新托盘菜单失败: {e}");
}
// 保存配置文件
if let Err(e) = Config::profiles().data().save_file() {
log::warn!(target: "app", "Async save profiles file failed: {e}");
log::warn!(target: "app", "异步保存配置文件失败: {e}");
}
});
@@ -552,19 +483,19 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info,
Type::Cmd,
true,
"Sending profile change event to frontend: {}, sequence: {}",
"向前端发送配置变更事件: {}, 序列号: {}",
current,
current_sequence
);
handle::Handle::notify_profile_changed(current.clone());
}
cleanup_processing_state(current_sequence, "Profile switch completed").await;
cleanup_processing_state(current_sequence, "配置切换完成").await;
Ok(true)
}
Ok(Ok((false, error_msg))) => {
logging!(warn, Type::Cmd, true, "Profile validation failed: {}", error_msg);
logging!(warn, Type::Cmd, true, "配置验证失败: {}", error_msg);
Config::profiles().discard();
// 如果验证失败,恢复到之前的配置
if let Some(prev_profile) = current_profile {
@@ -572,7 +503,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info,
Type::Cmd,
true,
"Attempting to restore previous profile: {}",
"尝试恢复到之前的配置: {}",
prev_profile
);
let restore_profiles = IProfiles {
@@ -585,17 +516,17 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = Config::profiles().data().save_file() {
log::warn!(target: "app", "Failed to save and restore configuration file asynchronously: {e}");
log::warn!(target: "app", "异步保存恢复配置文件失败: {e}");
}
});
logging!(info, Type::Cmd, true, "Successfully restored previous profile");
logging!(info, Type::Cmd, true, "成功恢复到之前的配置");
}
// 发送验证错误通知
handle::Handle::notice_message("config_validate::error", &error_msg);
cleanup_processing_state(current_sequence, "Profile validation failed").await;
cleanup_processing_state(current_sequence, "配置验证失败").await;
Ok(false)
}
@@ -604,25 +535,25 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
warn,
Type::Cmd,
true,
"Error occurred during update: {}, sequence: {}",
"更新过程发生错误: {}, 序列号: {}",
e,
current_sequence
);
Config::profiles().discard();
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
cleanup_processing_state(current_sequence, "Update process error").await;
cleanup_processing_state(current_sequence, "更新过程错误").await;
Ok(false)
}
Err(_) => {
// 超时处理
let timeout_msg = "Profile update timed out (30s), possibly due to validation or kernel communication";
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
logging!(
error,
Type::Cmd,
true,
"{}, sequence: {}",
"{}, 序列号: {}",
timeout_msg,
current_sequence
);
@@ -633,7 +564,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info,
Type::Cmd,
true,
"After timeout, attempting to restore previous profile: {}, sequence: {}",
"超时后尝试恢复到之前的配置: {}, 序列号: {}",
prev_profile,
current_sequence
);
@@ -647,7 +578,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
cleanup_processing_state(current_sequence, "Profile update timeout").await;
cleanup_processing_state(current_sequence, "配置更新超时").await;
Ok(false)
}
@@ -660,7 +591,7 @@ pub async fn patch_profiles_config_by_profile_index(
_app_handle: tauri::AppHandle,
profile_index: String,
) -> CmdResult<bool> {
logging!(info, Type::Cmd, true, "Switching profile to: {}", profile_index);
logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index);
let profiles = IProfiles {
current: Some(profile_index),
@@ -689,9 +620,9 @@ pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
if update_interval_changed {
let index_clone = index.clone();
crate::process::AsyncHandler::spawn(move || async move {
logging!(info, Type::Timer, "Timer update interval changed; refreshing timers...");
logging!(info, Type::Timer, "定时器更新间隔已变更,正在刷新定时器...");
if let Err(e) = crate::core::Timer::global().refresh() {
logging!(error, Type::Timer, "Failed to refresh timers: {}", e);
logging!(error, Type::Timer, "刷新定时器失败: {}", e);
} else {
// 刷新成功后发送自定义事件,不触发配置重载
crate::core::handle::Handle::notify_timer_updated(index_clone);
@@ -738,30 +669,23 @@ pub fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
Ok(next_time)
}
#[tauri::command]
pub async fn update_profiles_on_startup() -> CmdResult {
logging!(
info,
Type::Cmd,
true,
"Checking profiles for updates at startup..."
);
logging!(info, Type::Cmd, true, "Checking profiles for updates at startup...");
let profiles_to_update = {
let profiles = Config::profiles();
let profiles = profiles.latest();
profiles.items.as_ref().map_or_else(Vec::new, |items| {
items
.iter()
.filter(|item| {
item.option
.as_ref()
.is_some_and(|opt| opt.update_always == Some(true))
})
.filter_map(|item| item.uid.clone())
.collect()
})
profiles.items.as_ref()
.map_or_else(
Vec::new,
|items| items.iter()
.filter(|item| item.option.as_ref().is_some_and(|opt| opt.update_always == Some(true)))
.filter_map(|item| item.uid.clone())
.collect()
)
};
if profiles_to_update.is_empty() {
@@ -769,13 +693,7 @@ pub async fn update_profiles_on_startup() -> CmdResult {
return Ok(());
}
logging!(
info,
Type::Cmd,
true,
"Found profiles to update: {:?}",
profiles_to_update
);
logging!(info, Type::Cmd, true, "Found profiles to update: {:?}", profiles_to_update);
let mut update_futures = Vec::new();
for uid in profiles_to_update {
@@ -784,25 +702,13 @@ pub async fn update_profiles_on_startup() -> CmdResult {
let results = futures::future::join_all(update_futures).await;
if results.iter().any(|res| res.is_ok()) {
logging!(
info,
Type::Cmd,
true,
"The startup update is complete, restart the kernel..."
);
CoreManager::global()
.update_config()
.await
.map_err(|e| e.to_string())?;
logging!(info, Type::Cmd, true, "The startup update is complete, restart the kernel...");
CoreManager::global().update_config().await.map_err(|e| e.to_string())?;
handle::Handle::refresh_clash();
} else {
logging!(
warn,
Type::Cmd,
true,
"All updates completed with errors on startup."
);
logging!(warn, Type::Cmd, true, "All updates completed with errors on startup.");
}
Ok(())
@@ -810,6 +716,7 @@ pub async fn update_profiles_on_startup() -> CmdResult {
#[tauri::command]
pub async fn create_profile_from_share_link(link: String, template_name: String) -> CmdResult {
const DEFAULT_TEMPLATE: &str = r#"
mixed-port: 2080
allow-lan: true
@@ -1173,18 +1080,14 @@ pub async fn create_profile_from_share_link(link: String, template_name: String)
let parsed_url = Url::parse(&link).map_err(|e| e.to_string())?;
let scheme = parsed_url.scheme();
let proxy_name = parsed_url
.fragment()
let proxy_name = parsed_url.fragment()
.map(|f| percent_decode_str(f).decode_utf8_lossy().to_string())
.unwrap_or_else(|| "Proxy from Link".to_string());
let mut proxy_map: BTreeMap<String, Value> = BTreeMap::new();
proxy_map.insert("name".into(), proxy_name.clone().into());
proxy_map.insert("type".into(), scheme.into());
proxy_map.insert(
"server".into(),
parsed_url.host_str().unwrap_or_default().into(),
);
proxy_map.insert("server".into(), parsed_url.host_str().unwrap_or_default().into());
proxy_map.insert("port".into(), parsed_url.port().unwrap_or(443).into());
proxy_map.insert("udp".into(), true.into());
@@ -1200,29 +1103,16 @@ pub async fn create_profile_from_share_link(link: String, template_name: String)
"security" if value == "tls" => {
proxy_map.insert("tls".into(), true.into());
}
"flow" => {
proxy_map.insert("flow".into(), value.to_string().into());
}
"sni" => {
proxy_map.insert("servername".into(), value.to_string().into());
}
"fp" => {
proxy_map.insert("client-fingerprint".into(), value.to_string().into());
}
"pbk" => {
reality_opts.insert("public-key".into(), value.to_string().into());
}
"sid" => {
reality_opts.insert("short-id".into(), value.to_string().into());
}
"flow" => { proxy_map.insert("flow".into(), value.to_string().into()); }
"sni" => { proxy_map.insert("servername".into(), value.to_string().into()); }
"fp" => { proxy_map.insert("client-fingerprint".into(), value.to_string().into()); }
"pbk" => { reality_opts.insert("public-key".into(), value.to_string().into()); }
"sid" => { reality_opts.insert("short-id".into(), value.to_string().into()); }
_ => {}
}
}
if !reality_opts.is_empty() {
proxy_map.insert(
"reality-opts".into(),
serde_yaml::to_value(reality_opts).map_err(|e| e.to_string())?,
);
proxy_map.insert("reality-opts".into(), serde_yaml::to_value(reality_opts).map_err(|e| e.to_string())?);
}
}
"ss" => {
@@ -1238,32 +1128,19 @@ pub async fn create_profile_from_share_link(link: String, template_name: String)
"vmess" => {
if let Ok(decoded_bytes) = STANDARD.decode(parsed_url.host_str().unwrap_or_default()) {
if let Ok(json_str) = String::from_utf8(decoded_bytes) {
if let Ok(vmess_params) =
serde_json::from_str::<BTreeMap<String, Value>>(&json_str)
{
if let Some(add) = vmess_params.get("add") {
proxy_map.insert("server".into(), add.clone());
}
if let Some(port) = vmess_params.get("port") {
proxy_map.insert("port".into(), port.clone());
}
if let Some(id) = vmess_params.get("id") {
proxy_map.insert("uuid".into(), id.clone());
}
if let Some(aid) = vmess_params.get("aid") {
proxy_map.insert("alterId".into(), aid.clone());
}
if let Some(net) = vmess_params.get("net") {
proxy_map.insert("network".into(), net.clone());
}
if let Some(ps) = vmess_params.get("ps") {
proxy_map.insert("name".into(), ps.clone());
}
if let Ok(vmess_params) = serde_json::from_str::<BTreeMap<String, Value>>(&json_str) {
if let Some(add) = vmess_params.get("add") { proxy_map.insert("server".into(), add.clone()); }
if let Some(port) = vmess_params.get("port") { proxy_map.insert("port".into(), port.clone()); }
if let Some(id) = vmess_params.get("id") { proxy_map.insert("uuid".into(), id.clone()); }
if let Some(aid) = vmess_params.get("aid") { proxy_map.insert("alterId".into(), aid.clone()); }
if let Some(net) = vmess_params.get("net") { proxy_map.insert("network".into(), net.clone()); }
if let Some(ps) = vmess_params.get("ps") { proxy_map.insert("name".into(), ps.clone()); }
}
}
}
}
_ => {}
_ => {
}
}
let mut config: Value = serde_yaml::from_str(template_yaml).map_err(|e| e.to_string())?;
@@ -1273,15 +1150,10 @@ pub async fn create_profile_from_share_link(link: String, template_name: String)
proxies.push(serde_yaml::to_value(proxy_map).map_err(|e| e.to_string())?);
}
if let Some(groups) = config
.get_mut("proxy-groups")
.and_then(|v| v.as_sequence_mut())
{
if let Some(groups) = config.get_mut("proxy-groups").and_then(|v| v.as_sequence_mut()) {
for group in groups.iter_mut() {
if let Some(mapping) = group.as_mapping_mut() {
if let Some(proxies_list) =
mapping.get_mut("proxies").and_then(|p| p.as_sequence_mut())
{
if let Some(proxies_list) = mapping.get_mut("proxies").and_then(|p| p.as_sequence_mut()) {
let new_proxies_list: Vec<Value> = proxies_list
.iter()
.map(|p| {
@@ -1300,13 +1172,8 @@ pub async fn create_profile_from_share_link(link: String, template_name: String)
let new_yaml_content = serde_yaml::to_string(&config).map_err(|e| e.to_string())?;
let item = PrfItem::from_local(
proxy_name,
"Created from share link".into(),
Some(new_yaml_content),
None,
)
.map_err(|e| e.to_string())?;
let item = PrfItem::from_local(proxy_name, "Created from share link".into(), Some(new_yaml_content), None)
.map_err(|e| e.to_string())?;
wrap_err!(Config::profiles().data().append_item(item))
}
}

View File

@@ -33,7 +33,7 @@ pub async fn get_proxies() -> CmdResult<serde_json::Value> {
state.proxies = Box::new(proxies);
state.need_refresh = false;
}
log::debug!(target: "app", "Proxies refreshed successfully");
log::debug!(target: "app", "proxies刷新成功");
}
let proxies = {
@@ -50,7 +50,7 @@ pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
log::debug!(target: "app", "Force refresh proxy cache");
log::debug!(target: "app", "强制刷新代理缓存");
let proxies = manager.get_refresh_proxies().await?;
@@ -61,7 +61,7 @@ pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
state.last_refresh_time = Instant::now();
}
log::debug!(target: "app", "Force refresh proxy cache completed");
log::debug!(target: "app", "强制刷新代理缓存完成");
Ok(proxies)
}
@@ -88,7 +88,7 @@ pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
state.providers_proxies = Box::new(providers);
state.need_refresh = false;
}
log::debug!(target: "app", "providers_proxies refreshed successfully");
log::debug!(target: "app", "providers_proxies刷新成功");
}
let providers_proxies = {

View File

@@ -84,7 +84,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
wrap_err!(fs::write(&file_path, original_content))?;
// 发送合并文件专用错误通知
let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "Merge config file");
crate::cmd::validate::handle_yaml_validation_notice(&result, "合并配置文件");
return Ok(());
}
Err(e) => {
@@ -133,17 +133,17 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
|| (!file_path_str.ends_with(".js") && !is_script_error)
{
// 普通YAML错误使用YAML通知处理
log::info!(target: "app", "[cmd config save] YAML config file validation failed, sending notification");
log::info!(target: "app", "[cmd配置save] YAML配置文件验证失败,发送通知");
let result = (false, error_msg.clone());
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML config file");
crate::cmd::validate::handle_yaml_validation_notice(&result, "YAML配置文件");
} else if is_script_error {
// 脚本错误使用专门的通知处理
log::info!(target: "app", "[cmd config save] Script file validation failed, sending notification");
log::info!(target: "app", "[cmd配置save] 脚本文件验证失败,发送通知");
let result = (false, error_msg.clone());
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
} else {
// 普通配置错误使用一般通知
log::info!(target: "app", "[cmd config save] Other validation failure type, sending general notification");
log::info!(target: "app", "[cmd配置save] 其他类型验证失败,发送一般通知");
handle::Handle::notice_message("config_validate::error", &error_msg);
}
@@ -154,7 +154,7 @@ pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdR
error,
Type::Config,
true,
"[cmd config save] Error occurred during validation: {}",
"[cmd配置save] 验证过程发生错误: {}",
e
);
// 恢复原始配置文件

View File

@@ -32,7 +32,7 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
warn,
Type::Config,
true,
"{} validation failed: {}",
"{} 验证失败: {}",
file_type,
error_msg
);
@@ -43,14 +43,14 @@ pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str)
/// 验证指定脚本文件
#[tauri::command]
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
logging!(info, Type::Config, true, "Validating script file: {}", file_path);
logging!(info, Type::Config, true, "验证脚本文件: {}", file_path);
match CoreManager::global()
.validate_config_file(&file_path, None)
.await
{
Ok(result) => {
handle_script_validation_notice(&result, "Script file");
handle_script_validation_notice(&result, "脚本文件");
Ok(result.0) // 返回验证结果布尔值
}
Err(e) => {
@@ -129,7 +129,7 @@ pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
info,
Type::Config,
true,
"[Notice] Sending notice: status={}, msg={}",
"[通知] 发送通知: status={}, msg={}",
status,
error_msg
);

View File

@@ -42,7 +42,7 @@ impl IClashTemp {
tun.insert("enable".into(), false.into());
tun.insert("stack".into(), "gvisor".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"].into());
#[cfg(not(target_os = "windows"))]
@@ -129,7 +129,7 @@ impl IClashTemp {
help::save_yaml(
&dirs::clash_path()?,
&self.0,
Some("# Generated by Koala Clash"),
Some("# Generated by Clash Verge"),
)
}

View File

@@ -11,8 +11,8 @@ use once_cell::sync::OnceCell;
use std::path::PathBuf;
use tokio::time::{sleep, Duration};
pub const RUNTIME_CONFIG: &str = "koala-clash.yaml";
pub const CHECK_CONFIG: &str = "koala-clash-check.yaml";
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
pub struct Config {
clash_config: Draft<Box<IClashTemp>>,
@@ -69,9 +69,9 @@ impl Config {
}
// 生成运行时配置
if let Err(err) = Self::generate().await {
logging!(error, Type::Config, true, "Failed to generate runtime config: {}", err);
logging!(error, Type::Config, true, "生成运行时配置失败: {}", err);
} else {
logging!(info, Type::Config, true, "Runtime config generated successfully");
logging!(info, Type::Config, true, "生成运行时配置成功");
}
// 生成运行时配置文件并验证
@@ -79,7 +79,7 @@ impl Config {
let validation_result = if config_result.is_ok() {
// 验证配置文件
logging!(info, Type::Config, true, "Starting config validation");
logging!(info, Type::Config, true, "开始验证配置");
match CoreManager::global().validate_config().await {
Ok((is_valid, error_msg)) => {
@@ -88,7 +88,7 @@ impl Config {
warn,
Type::Config,
true,
"[First launch] Config validation failed, starting with minimal default config: {}",
"[首次启动] 配置验证失败,使用默认最小配置启动: {}",
error_msg
);
CoreManager::global()
@@ -96,12 +96,12 @@ impl Config {
.await?;
Some(("config_validate::boot_error", error_msg))
} else {
logging!(info, Type::Config, true, "Config validation succeeded");
logging!(info, Type::Config, true, "配置验证成功");
Some(("config_validate::success", String::new()))
}
}
Err(err) => {
logging!(warn, Type::Config, true, "Validation process execution failed: {}", err);
logging!(warn, Type::Config, true, "验证进程执行失败: {}", err);
CoreManager::global()
.use_default_config("config_validate::process_terminated", "")
.await?;
@@ -109,7 +109,7 @@ impl Config {
}
}
} else {
logging!(warn, Type::Config, true, "Failed to generate config file; using default config");
logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置");
CoreManager::global()
.use_default_config("config_validate::error", "")
.await?;
@@ -141,7 +141,7 @@ impl Config {
.as_ref()
.ok_or(anyhow!("failed to get runtime config"))?;
help::save_yaml(&path, &config, Some("# Generated by Koala Clash"))?;
help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?;
Ok(path)
}

View File

@@ -19,11 +19,11 @@ macro_rules! draft_define {
impl Draft<Box<$id>> {
#[allow(unused)]
pub fn data(&self) -> MappedMutexGuard<'_, Box<$id>> {
pub fn data(&self) -> MappedMutexGuard<Box<$id>> {
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
}
pub fn latest(&self) -> MappedMutexGuard<'_, Box<$id>> {
pub fn latest(&self) -> MappedMutexGuard<Box<$id>> {
MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() {
&mut inner.0
@@ -33,7 +33,7 @@ macro_rules! draft_define {
})
}
pub fn draft(&self) -> MappedMutexGuard<'_, Box<$id>> {
pub fn draft(&self) -> MappedMutexGuard<Box<$id>> {
MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() {
inner.1 = Some(inner.0.clone());

View File

@@ -4,11 +4,11 @@ use crate::utils::{
tmpl,
};
use anyhow::{bail, Context, Result};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use std::{fs, time::Duration};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use url::Url;
use super::Config;
@@ -326,7 +326,7 @@ impl PrfItem {
if let Ok(mut parsed_url) = Url::parse(url) {
if parsed_url.set_host(Some(new_domain)).is_ok() {
final_url = parsed_url.to_string();
log::info!(target: "app", "URL host updated to -> {final_url}");
log::info!(target: "app", "URL host updated to -> {}", final_url);
}
}
}
@@ -407,8 +407,7 @@ impl PrfItem {
Some(value) => {
let str_value = value.to_str().unwrap_or("");
if let Some(b64_data) = str_value.strip_prefix("base64:") {
STANDARD
.decode(b64_data)
STANDARD.decode(b64_data)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())
} else {
@@ -424,7 +423,7 @@ impl PrfItem {
bail!(announce_msg.clone());
}
}
let announce_url = match header.get("announce-url") {
Some(value) => {
let str_value = value.to_str().unwrap_or("");
@@ -437,8 +436,7 @@ impl PrfItem {
Some(value) => {
let str_value = value.to_str().unwrap_or("");
if let Some(b64_data) = str_value.strip_prefix("base64:") {
STANDARD
.decode(b64_data)
STANDARD.decode(b64_data)
.ok()
.and_then(|bytes| String::from_utf8(bytes).ok())
} else {
@@ -450,9 +448,7 @@ impl PrfItem {
let uid = help::get_uid("R");
let file = format!("{uid}.yaml");
let name = name
.or(profile_title)
.unwrap_or(filename.unwrap_or("Remote File".into()));
let name = name.or(profile_title).unwrap_or(filename.unwrap_or("Remote File".into()));
let data = resp.text_with_charset("utf-8").await?;
// process the charset "UTF-8 with BOM"
@@ -504,23 +500,13 @@ impl PrfItem {
selected: None,
extra,
option: Some(PrfOption {
user_agent: user_agent.clone(),
with_proxy: if with_proxy { Some(true) } else { None },
self_proxy: if self_proxy { Some(true) } else { None },
update_interval,
update_always,
timeout_seconds: Some(timeout),
danger_accept_invalid_certs: if accept_invalid_certs {
Some(true)
} else {
None
},
merge,
script,
rules,
proxies,
groups,
use_hwid: Some(use_hwid),
..PrfOption::default()
}),
home,

View File

@@ -66,7 +66,7 @@ impl IProfiles {
help::save_yaml(
&dirs::profiles_path()?,
self,
Some("# Profiles Config for Koala Clash"),
Some("# Profiles Config for Clash Verge"),
)
}
@@ -136,9 +136,10 @@ impl IProfiles {
.with_context(|| format!("failed to write to file \"{file}\""))?;
}
if item.itype == Some("remote".to_string()) || item.itype == Some("local".to_string()) {
// Always switch current to the newly created remote/local profile
self.current = uid.clone();
if self.current.is_none()
&& (item.itype == Some("remote".to_string()) || item.itype == Some("local".to_string()))
{
self.current = uid;
}
if self.items.is_none() {
@@ -535,7 +536,7 @@ impl IProfiles {
if Self::is_profile_file(file_name) {
// 检查是否为全局扩展文件
if protected_files.contains(file_name) {
log::debug!(target: "app", "Protect global extension config file: {file_name}");
log::debug!(target: "app", "保护全局扩展配置文件: {file_name}");
continue;
}
@@ -544,11 +545,11 @@ impl IProfiles {
match std::fs::remove_file(&path) {
Ok(_) => {
deleted_files.push(file_name.to_string());
log::info!(target: "app", "Cleaned up redundant file: {file_name}");
log::info!(target: "app", "已清理冗余文件: {file_name}");
}
Err(e) => {
failed_deletions.push(format!("{file_name}: {e}"));
log::warn!(target: "app", "Failed to clean file: {file_name} - {e}");
log::warn!(target: "app", "清理文件失败: {file_name} - {e}");
}
}
}
@@ -678,14 +679,14 @@ impl IProfiles {
if !result.deleted_files.is_empty() {
log::info!(
target: "app",
"Auto cleanup completed, deleted {} redundant files",
"自动清理完成,删除了 {} 个冗余文件",
result.deleted_files.len()
);
}
Ok(())
}
Err(e) => {
log::warn!(target: "app", "Auto cleanup failed: {e}");
log::warn!(target: "app", "自动清理失败: {e}");
Ok(())
}
}

View File

@@ -238,7 +238,7 @@ pub struct IVergeTheme {
impl IVerge {
/// 有效的clash核心名称
pub const VALID_CLASH_CORES: &'static [&'static str] = &["koala-mihomo", "koala-mihomo-alpha"];
pub const VALID_CLASH_CORES: &'static [&'static str] = &["verge-mihomo", "verge-mihomo-alpha"];
/// 验证并修正配置文件中的clash_core值
pub fn validate_and_fix_config() -> Result<()> {
@@ -257,10 +257,10 @@ impl IVerge {
warn,
Type::Config,
true,
"Invalid clash_core config detected at startup: '{}', auto-fixing to 'koala-mihomo'",
"启动时发现无效的clash_core配置: '{}', 将自动修正为 'verge-mihomo'",
core
);
config.clash_core = Some("koala-mihomo".to_string());
config.clash_core = Some("verge-mihomo".to_string());
needs_fix = true;
}
} else {
@@ -268,21 +268,21 @@ impl IVerge {
info,
Type::Config,
true,
"clash_core not configured at startup; setting default to 'koala-mihomo'"
"启动时发现未配置clash_core, 将设置为默认值 'verge-mihomo'"
);
config.clash_core = Some("koala-mihomo".to_string());
config.clash_core = Some("verge-mihomo".to_string());
needs_fix = true;
}
// 修正后保存配置
if needs_fix {
logging!(info, Type::Config, true, "Saving fixed configuration file...");
help::save_yaml(&config_path, &config, Some("# Koala Clash Config"))?;
logging!(info, Type::Config, true, "正在保存修正后的配置文件...");
help::save_yaml(&config_path, &config, Some("# Clash Verge Config"))?;
logging!(
info,
Type::Config,
true,
"Configuration file fixed; reloading config required"
"配置文件修正完成,需要重新加载配置"
);
Self::reload_config_after_fix(config)?;
@@ -291,7 +291,7 @@ impl IVerge {
info,
Type::Config,
true,
"clash_core config validation passed: {:?}",
"clash_core配置验证通过: {:?}",
config.clash_core
);
}
@@ -321,7 +321,7 @@ impl IVerge {
pub fn get_valid_clash_core(&self) -> String {
self.clash_core
.clone()
.unwrap_or_else(|| "koala-mihomo".to_string())
.unwrap_or_else(|| "verge-mihomo".to_string())
}
fn get_system_language() -> String {
@@ -340,17 +340,18 @@ impl IVerge {
}
pub fn new() -> Self {
dirs::verge_path()
.and_then(|path| help::read_yaml::<IVerge>(&path))
.unwrap_or_else(|err| {
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
Ok(config) => config,
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
})
}
}
}
pub fn template() -> Self {
Self {
clash_core: Some("koala-mihomo".into()),
clash_core: Some("verge-mihomo".into()),
language: Some(Self::get_system_language()),
theme_mode: Some("system".into()),
#[cfg(not(target_os = "windows"))]
@@ -414,7 +415,7 @@ impl IVerge {
/// Save IVerge App Config
pub fn save_file(&self) -> Result<()> {
help::save_yaml(&dirs::verge_path()?, &self, Some("# Koala Clash Config"))
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config"))
}
/// patch verge config

View File

@@ -39,15 +39,15 @@ impl AsyncProxyQuery {
pub async fn get_auto_proxy() -> AsyncAutoproxy {
match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await {
Ok(Ok(proxy)) => {
log::debug!(target: "app", "Async auto proxy fetch succeeded: enable={}, url={}", proxy.enable, proxy.url);
log::debug!(target: "app", "异步获取自动代理成功: enable={}, url={}", proxy.enable, proxy.url);
proxy
}
Ok(Err(e)) => {
log::warn!(target: "app", "Async auto proxy fetch failed: {e}");
log::warn!(target: "app", "异步获取自动代理失败: {e}");
AsyncAutoproxy::default()
}
Err(_) => {
log::warn!(target: "app", "Async auto proxy fetch timed out");
log::warn!(target: "app", "异步获取自动代理超时");
AsyncAutoproxy::default()
}
}
@@ -57,15 +57,15 @@ impl AsyncProxyQuery {
pub async fn get_system_proxy() -> AsyncSysproxy {
match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await {
Ok(Ok(proxy)) => {
log::debug!(target: "app", "Async system proxy fetch succeeded: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port);
log::debug!(target: "app", "异步获取系统代理成功: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port);
proxy
}
Ok(Err(e)) => {
log::warn!(target: "app", "Async system proxy fetch failed: {e}");
log::warn!(target: "app", "异步获取系统代理失败: {e}");
AsyncSysproxy::default()
}
Err(_) => {
log::warn!(target: "app", "Async system proxy fetch timed out");
log::warn!(target: "app", "异步获取系统代理超时");
AsyncSysproxy::default()
}
}
@@ -97,7 +97,7 @@ impl AsyncProxyQuery {
RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey);
if result != 0 {
log::debug!(target: "app", "Unable to open registry key");
log::debug!(target: "app", "无法打开注册表项");
return Ok(AsyncAutoproxy::default());
}
@@ -123,7 +123,7 @@ impl AsyncProxyQuery {
.position(|&x| x == 0)
.unwrap_or(url_buffer.len());
pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]);
log::debug!(target: "app", "Read PAC URL from registry: {}", pac_url);
log::debug!(target: "app", "从注册表读取到PAC URL: {}", pac_url);
}
// 2. 检查自动检测设置是否启用
@@ -148,7 +148,7 @@ impl AsyncProxyQuery {
|| (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0);
if pac_enabled {
log::debug!(target: "app", "PAC configuration enabled: URL={}, AutoDetect={}", pac_url, auto_detect);
log::debug!(target: "app", "PAC配置启用: URL={}, AutoDetect={}", pac_url, auto_detect);
if pac_url.is_empty() && auto_detect != 0 {
pac_url = "auto-detect".to_string();
@@ -159,7 +159,7 @@ impl AsyncProxyQuery {
url: pac_url,
})
} else {
log::debug!(target: "app", "PAC configuration not enabled");
log::debug!(target: "app", "PAC配置未启用");
Ok(AsyncAutoproxy::default())
}
}
@@ -194,7 +194,7 @@ impl AsyncProxyQuery {
}
}
log::debug!(target: "app", "Parse result: pac_enabled={pac_enabled}, pac_url={pac_url}");
log::debug!(target: "app", "解析结果: pac_enabled={pac_enabled}, pac_url={pac_url}");
Ok(AsyncAutoproxy {
enable: pac_enabled && !pac_url.is_empty(),
@@ -361,7 +361,7 @@ impl AsyncProxyQuery {
(proxy_server, 8080)
};
log::debug!(target: "app", "Read proxy settings from registry: {}:{}, bypass: {}", host, port, bypass_list);
log::debug!(target: "app", "从注册表读取到代理设置: {}:{}, bypass: {}", host, port, bypass_list);
Ok(AsyncSysproxy {
enable: true,
@@ -518,7 +518,7 @@ impl AsyncProxyQuery {
};
if host.is_empty() {
return Err(anyhow!("Invalid proxy URL"));
return Err(anyhow!("无效的代理URL"));
}
Ok(AsyncSysproxy {

View File

@@ -108,11 +108,11 @@ impl WebDavClient {
reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(op.timeout()))
.user_agent(format!("koala-clash/{APP_VERSION} ({OS} WebDAV-Client)"))
.user_agent(format!("clash-verge/{APP_VERSION} ({OS} WebDAV-Client)"))
.redirect(reqwest::redirect::Policy::custom(|attempt| {
// 允许所有请求类型的重定向包括PUT
if attempt.previous().len() >= 5 {
attempt.error("Too many redirects")
attempt.error("重定向次数过多")
} else {
attempt.follow()
}

View File

@@ -72,7 +72,7 @@ impl CoreManager {
warn,
Type::Config,
true,
"Failed to read file to detect type: {}, error: {}",
"无法读取文件以检测类型: {}, 错误: {}",
path,
err
);
@@ -130,7 +130,7 @@ impl CoreManager {
debug,
Type::Config,
true,
"Unable to determine file type, defaulting to YAML handling: {}",
"无法确定文件类型默认当作YAML处理: {}",
path
);
Ok(false)
@@ -146,19 +146,14 @@ impl CoreManager {
help::save_yaml(
&runtime_path,
&Config::clash().latest().0,
Some("# Koala Clash Runtime"),
Some("# Clash Verge Runtime"),
)?;
handle::Handle::notice_message(msg_type, msg_content);
Ok(())
}
/// 验证运行时配置
pub async fn validate_config(&self) -> Result<(bool, String)> {
logging!(
info,
Type::Config,
true,
"Generate temporary config file for validation"
);
logging!(info, Type::Config, true, "生成临时配置文件用于验证");
let config_path = Config::generate_file(ConfigType::Check)?;
let config_path = dirs::path_to_str(&config_path)?;
self.validate_config_internal(config_path).await
@@ -171,12 +166,7 @@ impl CoreManager {
) -> Result<(bool, String)> {
// 检查程序是否正在退出,如果是则跳过验证
if handle::Handle::global().is_exiting() {
logging!(
info,
Type::Core,
true,
"App is exiting, skipping validation"
);
logging!(info, Type::Core, true, "应用正在退出,跳过验证");
return Ok((true, String::new()));
}
@@ -193,7 +183,7 @@ impl CoreManager {
info,
Type::Config,
true,
"Detected merge file, performing syntax check only: {}",
"检测到Merge文件仅进行语法检查: {}",
config_path
);
return self.validate_file_syntax(config_path).await;
@@ -211,7 +201,7 @@ impl CoreManager {
warn,
Type::Config,
true,
"Unable to determine file type: {}, error: {}",
"无法确定文件类型: {}, 错误: {}",
config_path,
err
);
@@ -225,7 +215,7 @@ impl CoreManager {
info,
Type::Config,
true,
"Detected script file, validating with JavaScript: {}",
"检测到脚本文件,使用JavaScript验证: {}",
config_path
);
return self.validate_script_file(config_path).await;
@@ -236,7 +226,7 @@ impl CoreManager {
info,
Type::Config,
true,
"Validating config file with Clash core: {}",
"使用Clash内核验证配置文件: {}",
config_path
);
self.validate_config_internal(config_path).await
@@ -245,12 +235,7 @@ impl CoreManager {
async fn validate_config_internal(&self, config_path: &str) -> Result<(bool, String)> {
// 检查程序是否正在退出,如果是则跳过验证
if handle::Handle::global().is_exiting() {
logging!(
info,
Type::Core,
true,
"App is exiting, skipping validation"
);
logging!(info, Type::Core, true, "应用正在退出,跳过验证");
return Ok((true, String::new()));
}
@@ -258,23 +243,17 @@ impl CoreManager {
info,
Type::Config,
true,
"Starting validation for config file: {}",
"开始验证配置文件: {}",
config_path
);
let clash_core = Config::verge().latest().get_valid_clash_core();
logging!(info, Type::Config, true, "Using core: {}", clash_core);
logging!(info, Type::Config, true, "使用内核: {}", clash_core);
let app_handle = handle::Handle::global().app_handle().unwrap();
let app_dir = dirs::app_home_dir()?;
let app_dir_str = dirs::path_to_str(&app_dir)?;
logging!(
info,
Type::Config,
true,
"Validation directory: {}",
app_dir_str
);
logging!(info, Type::Config, true, "验证目录: {}", app_dir_str);
// 使用子进程运行clash验证配置
let output = app_handle
@@ -292,84 +271,56 @@ impl CoreManager {
let has_error =
!output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
logging!(
info,
Type::Config,
true,
"-------- Validation Result --------"
);
logging!(info, Type::Config, true, "-------- 验证结果 --------");
if !stderr.is_empty() {
logging!(info, Type::Config, true, "stderr output:\n{}", stderr);
logging!(info, Type::Config, true, "stderr输出:\n{}", stderr);
}
if has_error {
logging!(
info,
Type::Config,
true,
"Errors found, processing error details"
);
logging!(info, Type::Config, true, "发现错误,开始处理错误信息");
let error_msg = if !stdout.is_empty() {
stdout.to_string()
} else if !stderr.is_empty() {
stderr.to_string()
} else if let Some(code) = output.status.code() {
format!("Validation process exited abnormally, exit code: {code}")
format!("验证进程异常退出,退出码: {code}")
} else {
"Validation process was terminated".to_string()
"验证进程被终止".to_string()
};
logging!(info, Type::Config, true, "-------- Validation End --------");
logging!(info, Type::Config, true, "-------- 验证结束 --------");
Ok((false, error_msg)) // 返回错误消息给调用者处理
} else {
logging!(info, Type::Config, true, "Validation succeeded");
logging!(info, Type::Config, true, "-------- Validation End --------");
logging!(info, Type::Config, true, "验证成功");
logging!(info, Type::Config, true, "-------- 验证结束 --------");
Ok((true, String::new()))
}
}
/// 只进行文件语法检查,不进行完整验证
async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
logging!(
info,
Type::Config,
true,
"Starting file check: {}",
config_path
);
logging!(info, Type::Config, true, "开始检查文件: {}", config_path);
// 读取文件内容
let content = match std::fs::read_to_string(config_path) {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read file: {err}");
logging!(
error,
Type::Config,
true,
"Failed to read file: {}",
error_msg
);
logging!(error, Type::Config, true, "无法读取文件: {}", error_msg);
return Ok((false, error_msg));
}
};
// 对YAML文件尝试解析只检查语法正确性
logging!(info, Type::Config, true, "Performing YAML syntax check");
logging!(info, Type::Config, true, "进行YAML语法检查");
match serde_yaml::from_str::<serde_yaml::Value>(&content) {
Ok(_) => {
logging!(info, Type::Config, true, "YAML syntax check passed");
logging!(info, Type::Config, true, "YAML语法检查通过");
Ok((true, String::new()))
}
Err(err) => {
// 使用标准化的前缀,以便错误处理函数能正确识别
let error_msg = format!("YAML syntax error: {err}");
logging!(
error,
Type::Config,
true,
"YAML syntax error: {}",
error_msg
);
logging!(error, Type::Config, true, "YAML语法错误: {}", error_msg);
Ok((false, error_msg))
}
}
@@ -381,19 +332,13 @@ impl CoreManager {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read script file: {err}");
logging!(warn, Type::Config, true, "Script syntax error: {}", err);
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
return Ok((false, error_msg));
}
};
logging!(
debug,
Type::Config,
true,
"Validating script file: {}",
path
);
logging!(debug, Type::Config, true, "验证脚本文件: {}", path);
// 使用boa引擎进行基本语法检查
use boa_engine::{Context, Source};
@@ -403,13 +348,7 @@ impl CoreManager {
match result {
Ok(_) => {
logging!(
debug,
Type::Config,
true,
"Script syntax validation passed: {}",
path
);
logging!(debug, Type::Config, true, "脚本语法验证通过: {}", path);
// 检查脚本是否包含main函数
if !content.contains("function main")
@@ -417,13 +356,7 @@ impl CoreManager {
&& !content.contains("let main")
{
let error_msg = "Script must contain a main function";
logging!(
warn,
Type::Config,
true,
"Script missing main function: {}",
path
);
logging!(warn, Type::Config, true, "脚本缺少main函数: {}", path);
//handle::Handle::notice_message("config_validate::script_missing_main", error_msg);
return Ok((false, error_msg.to_string()));
}
@@ -432,7 +365,7 @@ impl CoreManager {
}
Err(err) => {
let error_msg = format!("Script syntax error: {err}");
logging!(warn, Type::Config, true, "Script syntax error: {}", err);
logging!(warn, Type::Config, true, "脚本语法错误: {}", err);
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
Ok((false, error_msg))
}
@@ -442,55 +375,33 @@ impl CoreManager {
pub async fn update_config(&self) -> Result<(bool, String)> {
// 检查程序是否正在退出,如果是则跳过完整验证流程
if handle::Handle::global().is_exiting() {
logging!(
info,
Type::Config,
true,
"App is exiting, skipping validation"
);
logging!(info, Type::Config, true, "应用正在退出,跳过验证");
return Ok((true, String::new()));
}
logging!(info, Type::Config, true, "Starting config update");
logging!(info, Type::Config, true, "开始更新配置");
// 1. 先生成新的配置内容
logging!(
info,
Type::Config,
true,
"Generating new configuration content"
);
logging!(info, Type::Config, true, "生成新的配置内容");
Config::generate().await?;
// 2. 验证配置
match self.validate_config().await {
Ok((true, _)) => {
logging!(info, Type::Config, true, "Configuration validation passed");
logging!(info, Type::Config, true, "配置验证通过");
// 4. 验证通过后,生成正式的运行时配置
logging!(info, Type::Config, true, "Generating runtime configuration");
logging!(info, Type::Config, true, "生成运行时配置");
let run_path = Config::generate_file(ConfigType::Run)?;
logging_error!(Type::Config, true, self.put_configs_force(run_path).await);
Ok((true, "something".into()))
}
Ok((false, error_msg)) => {
logging!(
warn,
Type::Config,
true,
"Configuration validation failed: {}",
error_msg
);
logging!(warn, Type::Config, true, "配置验证失败: {}", error_msg);
Config::runtime().discard();
Ok((false, error_msg))
}
Err(e) => {
logging!(
warn,
Type::Config,
true,
"Error occurred during validation: {}",
e
);
logging!(warn, Type::Config, true, "验证过程发生错误: {}", e);
Config::runtime().discard();
Err(e)
}
@@ -524,12 +435,7 @@ impl CoreManager {
impl CoreManager {
/// 清理多余的 mihomo 进程
async fn cleanup_orphaned_mihomo_processes(&self) -> Result<()> {
logging!(
info,
Type::Core,
true,
"Starting cleanup of orphaned mihomo processes"
);
logging!(info, Type::Core, true, "开始清理多余的 mihomo 进程");
// 获取当前管理的进程 PID
let current_pid = {
@@ -537,7 +443,7 @@ impl CoreManager {
child_guard.as_ref().map(|child| child.pid())
};
let target_processes = ["koala-mihomo", "koala-mihomo-alpha"];
let target_processes = ["verge-mihomo", "verge-mihomo-alpha"];
// 并行查找所有目标进程
let mut process_futures = Vec::new();
@@ -565,7 +471,7 @@ impl CoreManager {
debug,
Type::Core,
true,
"Skipping currently managed process: {} (PID: {})",
"跳过当前管理的进程: {} (PID: {})",
process_name,
pid
);
@@ -576,24 +482,13 @@ impl CoreManager {
}
}
Err(e) => {
logging!(
debug,
Type::Core,
true,
"Error occurred while finding processes: {}",
e
);
logging!(debug, Type::Core, true, "查找进程时发生错误: {}", e);
}
}
}
if pids_to_kill.is_empty() {
logging!(
debug,
Type::Core,
true,
"No orphaned mihomo processes found"
);
logging!(debug, Type::Core, true, "未发现多余的 mihomo 进程");
return Ok(());
}
@@ -611,7 +506,7 @@ impl CoreManager {
info,
Type::Core,
true,
"Cleanup complete, a total of {} redundant mihomo processes terminated",
"清理完成,共终止了 {} 个多余的 mihomo 进程",
killed_count
);
}
@@ -720,7 +615,7 @@ impl CoreManager {
info,
Type::Core,
true,
"Attempt to terminate process: {} (PID: {})",
"尝试终止进程: {} (PID: {})",
process_name,
pid
);
@@ -767,7 +662,7 @@ impl CoreManager {
warn,
Type::Core,
true,
"Process {} (PID: {}) Termination command successful, but process still running",
"进程 {} (PID: {}) 终止命令成功但进程仍在运行",
process_name,
pid
);
@@ -777,7 +672,7 @@ impl CoreManager {
info,
Type::Core,
true,
"Successfully terminated process: {} (PID: {})",
"成功终止进程: {} (PID: {})",
process_name,
pid
);
@@ -788,7 +683,7 @@ impl CoreManager {
warn,
Type::Core,
true,
"Unable to terminate process: {} (PID: {})",
"无法终止进程: {} (PID: {})",
process_name,
pid
);
@@ -942,29 +837,19 @@ impl CoreManager {
// 当服务安装失败时的回退逻辑
async fn attempt_service_init(&self) -> Result<()> {
if service::check_service_needs_reinstall().await {
logging!(
info,
Type::Core,
true,
"Service version mismatch or abnormal status, performing reinstallation"
);
logging!(info, Type::Core, true, "服务版本不匹配或状态异常,执行重装");
if let Err(e) = service::reinstall_service().await {
logging!(
warn,
Type::Core,
true,
"Service reinstallation failed during attempt_service_init: {}",
"服务重装失败 during attempt_service_init: {}",
e
);
return Err(e);
}
// 如果重装成功,还需要尝试启动服务
logging!(
info,
Type::Core,
true,
"Service reinstalled successfully, attempting to start"
);
logging!(info, Type::Core, true, "服务重装成功,尝试启动服务");
}
if let Err(e) = self.start_core_by_service().await {
@@ -972,20 +857,20 @@ impl CoreManager {
warn,
Type::Core,
true,
"Failed to start core via service during attempt_service_init: {}",
"通过服务启动核心失败 during attempt_service_init: {}",
e
);
// 确保 prefer_sidecar 在 start_core_by_service 失败时也被设置
let mut state = service::ServiceState::get();
if !state.prefer_sidecar {
state.prefer_sidecar = true;
state.last_error = Some(format!("Failed to start core via service: {e}"));
state.last_error = Some(format!("通过服务启动核心失败: {e}"));
if let Err(save_err) = state.save() {
logging!(
error,
Type::Core,
true,
"Failed to save ServiceState (in attempt_service_init/start_core_by_service): {}",
"保存ServiceState失败 (in attempt_service_init/start_core_by_service): {}",
save_err
);
}
@@ -1004,7 +889,7 @@ impl CoreManager {
warn,
Type::Core,
true,
"Failed to clean up unnecessary mihomo processes during application initialization: {}",
"应用初始化时清理多余 mihomo 进程失败: {}",
e
);
}
@@ -1016,16 +901,11 @@ impl CoreManager {
info,
Type::Core,
true,
"Service currently available or appears available; attempting to start/reinstall via service mode"
"服务当前可用或看似可用,尝试通过服务模式启动/重装"
);
match self.attempt_service_init().await {
Ok(_) => {
logging!(
info,
Type::Core,
true,
"Service mode successfully started core"
);
logging!(info, Type::Core, true, "服务模式成功启动核心");
core_started_successfully = true;
}
Err(_err) => {
@@ -1033,7 +913,7 @@ impl CoreManager {
warn,
Type::Core,
true,
"Service mode start or reinstall failed. Will attempt Sidecar fallback."
"服务模式启动或重装失败。将尝试Sidecar模式回退。"
);
}
}
@@ -1042,7 +922,7 @@ impl CoreManager {
info,
Type::Core,
true,
"Service initially unavailable (is_service_available call failed)"
"服务初始不可用 (is_service_available 调用失败)"
);
}
@@ -1051,7 +931,7 @@ impl CoreManager {
info,
Type::Core,
true,
"Core not started via service mode; performing Sidecar fallback or first-time install logic"
"核心未通过服务模式启动执行Sidecar回退或首次安装逻辑"
);
let service_state = service::ServiceState::get();
@@ -1061,7 +941,7 @@ impl CoreManager {
info,
Type::Core,
true,
"User prefers Sidecar mode or previous service start failed; starting with Sidecar mode"
"用户偏好Sidecar模式或先前服务启动失败使用Sidecar模式启动"
);
self.start_core_by_sidecar().await?;
// 如果 sidecar 启动成功,我们可以认为核心初始化流程到此结束
@@ -1073,41 +953,26 @@ impl CoreManager {
info,
Type::Core,
true,
"No service installation record (first run or state reset); attempting to install service"
"无服务安装记录 (首次运行或状态重置),尝试安装服务"
);
match service::install_service().await {
Ok(_) => {
logging!(
info,
Type::Core,
true,
"Service installed successfully (first attempt)"
);
logging!(info, Type::Core, true, "服务安装成功(首次尝试)");
let mut new_state = service::ServiceState::default();
new_state.record_install();
new_state.prefer_sidecar = false;
new_state.save()?;
if service::is_service_available().await.is_ok() {
logging!(
info,
Type::Core,
true,
"Newly installed service available; attempting to start"
);
logging!(info, Type::Core, true, "新安装的服务可用,尝试启动");
if self.start_core_by_service().await.is_ok() {
logging!(
info,
Type::Core,
true,
"Newly installed service started successfully"
);
logging!(info, Type::Core, true, "新安装的服务启动成功");
} else {
logging!(
warn,
Type::Core,
true,
"Newly installed service failed to start; falling back to Sidecar mode"
"新安装的服务启动失败,回退到Sidecar模式"
);
let mut final_state = service::ServiceState::get();
final_state.prefer_sidecar = true;
@@ -1121,7 +986,7 @@ impl CoreManager {
warn,
Type::Core,
true,
"Service installed successfully but not connectable/immediately available; falling back to Sidecar mode"
"服务安装成功但未能连接/立即可用,回退到Sidecar模式"
);
let mut final_state = service::ServiceState::get();
final_state.prefer_sidecar = true;
@@ -1134,13 +999,7 @@ impl CoreManager {
}
}
Err(err) => {
logging!(
warn,
Type::Core,
true,
"Service first-time installation failed: {}",
err
);
logging!(warn, Type::Core, true, "服务首次安装失败: {}", err);
let new_state = service::ServiceState {
last_error: Some(err.to_string()),
prefer_sidecar: true,
@@ -1158,7 +1017,7 @@ impl CoreManager {
info,
Type::Core,
true,
"There is a service installation record, but the service is unavailable/not started. Force switch to Sidecar mode"
"有服务安装记录但服务不可用/未启动,强制切换到Sidecar模式"
);
let mut final_state = service::ServiceState::get();
if !final_state.prefer_sidecar {
@@ -1166,7 +1025,7 @@ impl CoreManager {
warn,
Type::Core,
true,
"prefer_sidecar is false, but is forced to true due to service startup failure or unavailability"
"prefer_sidecar false,因服务启动失败或不可用而强制设置为 true"
);
final_state.prefer_sidecar = true;
final_state.last_error =
@@ -1203,12 +1062,7 @@ impl CoreManager {
if service::check_service_needs_reinstall().await {
service::reinstall_service().await?;
}
logging!(
info,
Type::Core,
true,
"Service available; starting in service mode"
);
logging!(info, Type::Core, true, "服务可用,使用服务模式启动");
self.start_core_by_service().await?;
} else {
// 服务不可用,检查用户偏好
@@ -1218,16 +1072,11 @@ impl CoreManager {
info,
Type::Core,
true,
"Service unavailable; starting in Sidecar mode per user preference"
"服务不可用根据用户偏好使用Sidecar模式"
);
self.start_core_by_sidecar().await?;
} else {
logging!(
info,
Type::Core,
true,
"Service unavailable; starting in Sidecar mode"
);
logging!(info, Type::Core, true, "服务不可用使用Sidecar模式");
self.start_core_by_sidecar().await?;
}
}

View File

@@ -78,7 +78,7 @@ struct QueryRequest {
response_tx: oneshot::Sender<Autoproxy>,
}
// Configuration structure moved to external
// 配置结构体移到外部
struct ProxyConfig {
sys_enabled: bool,
pac_enabled: bool,
@@ -106,59 +106,59 @@ impl EventDrivenProxyManager {
}
}
/// Get automatic proxy configuration (cached)
/// 获取自动代理配置(缓存)
pub fn get_auto_proxy_cached(&self) -> Autoproxy {
self.state.read().auto_proxy.clone()
}
/// Asynchronously get the latest automatic proxy configuration
/// 异步获取最新的自动代理配置
pub async fn get_auto_proxy_async(&self) -> Autoproxy {
let (tx, rx) = oneshot::channel();
let query = QueryRequest { response_tx: tx };
if self.query_sender.send(query).is_err() {
log::error!(target: "app", "Failed to send query request, returning cached data");
log::error!(target: "app", "发送查询请求失败,返回缓存数据");
return self.get_auto_proxy_cached();
}
match timeout(Duration::from_secs(5), rx).await {
Ok(Ok(result)) => result,
_ => {
log::warn!(target: "app", "Query timed out, returning cached data");
log::warn!(target: "app", "查询超时,返回缓存数据");
self.get_auto_proxy_cached()
}
}
}
/// Notify configuration changed
/// 通知配置变更
pub fn notify_config_changed(&self) {
self.send_event(ProxyEvent::ConfigChanged);
}
/// Notify application started
/// 通知应用启动
pub fn notify_app_started(&self) {
self.send_event(ProxyEvent::AppStarted);
}
/// Notify application stopping
/// 通知应用即将关闭
#[allow(dead_code)]
pub fn notify_app_stopping(&self) {
self.send_event(ProxyEvent::AppStopping);
}
/// Enable system proxy
/// 启用系统代理
#[allow(dead_code)]
pub fn enable_proxy(&self) {
self.send_event(ProxyEvent::EnableProxy);
}
/// Disable system proxy
/// 禁用系统代理
#[allow(dead_code)]
pub fn disable_proxy(&self) {
self.send_event(ProxyEvent::DisableProxy);
}
/// Force check proxy status
/// 强制检查代理状态
#[allow(dead_code)]
pub fn force_check(&self) {
self.send_event(ProxyEvent::ForceCheck);
@@ -166,7 +166,7 @@ impl EventDrivenProxyManager {
fn send_event(&self, event: ProxyEvent) {
if let Err(e) = self.event_sender.send(event) {
log::error!(target: "app", "Failed to send proxy event: {e}");
log::error!(target: "app", "发送代理事件失败: {e}");
}
}
@@ -176,18 +176,18 @@ impl EventDrivenProxyManager {
mut query_rx: mpsc::UnboundedReceiver<QueryRequest>,
) {
tokio::spawn(async move {
log::info!(target: "app", "Event-driven proxy manager started");
log::info!(target: "app", "事件驱动代理管理器启动");
loop {
tokio::select! {
event = event_rx.recv() => {
match event {
Some(event) => {
log::debug!(target: "app", "Handling proxy event: {event:?}");
log::debug!(target: "app", "处理代理事件: {event:?}");
Self::handle_event(&state, event).await;
}
None => {
log::info!(target: "app", "Event channel closed, proxy manager stopped");
log::info!(target: "app", "事件通道关闭,代理管理器停止");
break;
}
}
@@ -199,7 +199,7 @@ impl EventDrivenProxyManager {
let _ = query.response_tx.send(result);
}
None => {
log::info!(target: "app", "Query channel closed");
log::info!(target: "app", "查询通道关闭");
break;
}
}
@@ -230,7 +230,7 @@ impl EventDrivenProxyManager {
Self::initialize_proxy_state(state).await;
}
ProxyEvent::AppStopping => {
log::info!(target: "app", "Cleaning up proxy state");
log::info!(target: "app", "清理代理状态");
}
}
}
@@ -246,7 +246,7 @@ impl EventDrivenProxyManager {
}
async fn initialize_proxy_state(state: &Arc<RwLock<ProxyState>>) {
log::info!(target: "app", "Initializing proxy state");
log::info!(target: "app", "初始化代理状态");
let config = Self::get_proxy_config();
let auto_proxy = Self::get_auto_proxy_with_timeout().await;
@@ -260,11 +260,11 @@ impl EventDrivenProxyManager {
s.is_healthy = true;
});
log::info!(target: "app", "Proxy state initialized: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
log::info!(target: "app", "代理状态初始化完成: sys={}, pac={}", config.sys_enabled, config.pac_enabled);
}
async fn update_proxy_config(state: &Arc<RwLock<ProxyState>>) {
log::debug!(target: "app", "Updating proxy configuration");
log::debug!(target: "app", "更新代理配置");
let config = Self::get_proxy_config();
@@ -288,7 +288,7 @@ impl EventDrivenProxyManager {
return;
}
log::debug!(target: "app", "Checking proxy status");
log::debug!(target: "app", "检查代理状态");
if pac_enabled {
Self::check_and_restore_pac_proxy(state).await;
@@ -306,7 +306,7 @@ impl EventDrivenProxyManager {
});
if !current.enable || current.url != expected.url {
log::info!(target: "app", "PAC proxy setting abnormal, recovering...");
log::info!(target: "app", "PAC代理设置异常,正在恢复...");
Self::restore_pac_proxy(&expected.url).await;
sleep(Duration::from_millis(500)).await;
@@ -328,7 +328,7 @@ impl EventDrivenProxyManager {
});
if !current.enable || current.host != expected.host || current.port != expected.port {
log::info!(target: "app", "System proxy setting abnormal, recovering...");
log::info!(target: "app", "系统代理设置异常,正在恢复...");
Self::restore_sys_proxy(&expected).await;
sleep(Duration::from_millis(500)).await;
@@ -344,7 +344,7 @@ impl EventDrivenProxyManager {
}
async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) {
log::info!(target: "app", "Enabling system proxy");
log::info!(target: "app", "启用系统代理");
let pac_enabled = state.read().pac_enabled;
@@ -360,7 +360,7 @@ impl EventDrivenProxyManager {
}
async fn disable_system_proxy(_state: &Arc<RwLock<ProxyState>>) {
log::info!(target: "app", "Disabling system proxy");
log::info!(target: "app", "禁用系统代理");
#[cfg(not(target_os = "windows"))]
{
@@ -373,7 +373,7 @@ impl EventDrivenProxyManager {
}
async fn switch_proxy_mode(state: &Arc<RwLock<ProxyState>>, to_pac: bool) {
log::info!(target: "app", "Switching to {} mode", if to_pac { "PAC" } else { "HTTP Proxy" });
log::info!(target: "app", "切换到{}模式", if to_pac { "PAC" } else { "HTTP代理" });
if to_pac {
let disabled_sys = Sysproxy::default();
@@ -396,7 +396,7 @@ impl EventDrivenProxyManager {
async fn get_auto_proxy_with_timeout() -> Autoproxy {
let async_proxy = AsyncProxyQuery::get_auto_proxy().await;
// Convert to compatible structure
// 转换为兼容的结构
Autoproxy {
enable: async_proxy.enable,
url: async_proxy.url,
@@ -406,7 +406,7 @@ impl EventDrivenProxyManager {
async fn get_sys_proxy_with_timeout() -> Sysproxy {
let async_proxy = AsyncProxyQuery::get_system_proxy().await;
// Convert to compatible structure
// 转换为兼容的结构
Sysproxy {
enable: async_proxy.enable,
host: async_proxy.host,
@@ -415,7 +415,7 @@ impl EventDrivenProxyManager {
}
}
// Unified state update method
// 统一的状态更新方法
fn update_state_timestamp<F>(state: &Arc<RwLock<ProxyState>>, update_fn: F)
where
F: FnOnce(&mut ProxyState),
@@ -534,14 +534,14 @@ impl EventDrivenProxyManager {
let binary_path = match dirs::service_path() {
Ok(path) => path,
Err(e) => {
log::error!(target: "app", "Failed to get service path: {}", e);
log::error!(target: "app", "获取服务路径失败: {}", e);
return;
}
};
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() {
log::error!(target: "app", "sysproxy.exe does not exist");
log::error!(target: "app", "sysproxy.exe 不存在");
return;
}
@@ -554,17 +554,17 @@ impl EventDrivenProxyManager {
match output {
Ok(output) => {
if !output.status.success() {
log::error!(target: "app", "Failed to execute sysproxy command: {:?}", args);
log::error!(target: "app", "执行sysproxy命令失败: {:?}", args);
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() {
log::error!(target: "app", "sysproxy stderr: {}", stderr);
log::error!(target: "app", "sysproxy错误输出: {}", stderr);
}
} else {
log::debug!(target: "app", "Successfully executed sysproxy command: {:?}", args);
log::debug!(target: "app", "成功执行sysproxy命令: {:?}", args);
}
}
Err(e) => {
log::error!(target: "app", "Error executing sysproxy command: {}", e);
log::error!(target: "app", "执行sysproxy命令出错: {}", e);
}
}
}

View File

@@ -258,8 +258,6 @@ pub struct Handle {
startup_errors: Arc<RwLock<Vec<ErrorMessage>>>,
startup_completed: Arc<RwLock<bool>>,
notification_system: Arc<RwLock<Option<NotificationSystem>>>,
/// Messages that should be emitted only after UI is really ready
ui_pending_messages: Arc<RwLock<Vec<ErrorMessage>>>,
}
impl Default for Handle {
@@ -270,7 +268,6 @@ impl Default for Handle {
startup_errors: Arc::new(RwLock::new(Vec::new())),
startup_completed: Arc::new(RwLock::new(false)),
notification_system: Arc::new(RwLock::new(Some(NotificationSystem::new()))),
ui_pending_messages: Arc::new(RwLock::new(Vec::new())),
}
}
}
@@ -298,10 +295,6 @@ impl Handle {
}
pub fn get_window(&self) -> Option<WebviewWindow> {
// If we are in lightweight mode, treat as no window (webview may be destroyed)
if crate::module::lightweight::is_in_lightweight_mode() {
return None;
}
let app_handle = self.app_handle()?;
let window: Option<WebviewWindow> = app_handle.get_webview_window("main");
if window.is_none() {
@@ -418,13 +411,12 @@ impl Handle {
let status_str = status.into();
let msg_str = msg.into();
// If startup not completed, buffer messages (existing behavior)
if !*handle.startup_completed.read() {
logging!(
info,
Type::Frontend,
true,
"Error found during startup; queued: {} - {}",
"启动过程中发现错误,加入消息队列: {} - {}",
status_str,
msg_str
);
@@ -437,23 +429,6 @@ impl Handle {
return;
}
// If UI is not yet ready (e.g., window re-created from tray or lightweight mode),
// buffer messages to emit after UI signals readiness.
if !crate::utils::resolve::is_ui_ready() {
log::debug!(
target: "app",
"UI not ready, queue notice message: {} - {}",
status_str,
msg_str
);
let mut pendings = handle.ui_pending_messages.write();
pendings.push(ErrorMessage {
status: status_str,
message: msg_str,
});
return;
}
if handle.is_exiting() {
return;
}
@@ -467,34 +442,6 @@ impl Handle {
}
}
/// Flush messages buffered while UI was not ready
pub fn flush_ui_pending_messages(&self) {
let pending = {
let mut msgs = self.ui_pending_messages.write();
std::mem::take(&mut *msgs)
};
if pending.is_empty() {
return;
}
if self.is_exiting() {
return;
}
let system_opt = self.notification_system.read();
if let Some(system) = system_opt.as_ref() {
for msg in pending {
system.send_event(FrontendEvent::NoticeMessage {
status: msg.status,
message: msg.message,
});
// small pacing to avoid flooding immediately on resume
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
}
pub fn mark_startup_completed(&self) {
{
let mut completed = self.startup_completed.write();
@@ -519,7 +466,7 @@ impl Handle {
info,
Type::Frontend,
true,
"Sending {} accumulated startup error messages",
"发送{}条启动时累积的错误消息",
errors.len()
);

View File

@@ -346,7 +346,7 @@ pub async fn reinstall_service() -> Result<()> {
Ok(())
}
Err(err) => {
let error = format!("failed to install service: {err}");
let error = format!("failed to install service: {}", err);
service_state.last_error = Some(error.clone());
service_state.prefer_sidecar = true;
service_state.save()?;
@@ -477,12 +477,7 @@ pub async fn reinstall_service() -> Result<()> {
/// 检查服务状态 - 使用IPC通信
pub async fn check_ipc_service_status() -> Result<JsonResponse> {
logging!(
info,
Type::Service,
true,
"Starting service status check (IPC)"
);
logging!(info, Type::Service, true, "开始检查服务状态 (IPC)");
// 使用IPC通信
let payload = serde_json::json!({});
@@ -500,16 +495,8 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
); */
if !response.success {
let err_msg = response
.error
.unwrap_or_else(|| "Unknown service error".to_string());
logging!(
error,
Type::Service,
true,
"Service response error: {}",
err_msg
);
let err_msg = response.error.unwrap_or_else(|| "未知服务错误".to_string());
logging!(error, Type::Service, true, "服务响应错误: {}", err_msg);
bail!(err_msg);
}
@@ -529,7 +516,7 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
warn,
Type::Service,
true,
"Failed to parse nested ResponseBody: {}; trying alternative",
"解析嵌套的ResponseBody失败: {}; 尝试其他方式",
e
);
None
@@ -549,7 +536,7 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
info,
Type::Service,
true,
"Service check succeeded: code={}, msg={}, data_present={}",
"服务检测成功: code={}, msg={}, data存在={}",
json_response.code,
json_response.msg,
json_response.data.is_some()
@@ -563,7 +550,7 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
info,
Type::Service,
true,
"Service check succeeded: code={}, msg={}",
"服务检测成功: code={}, msg={}",
json_response.code,
json_response.msg
);
@@ -574,42 +561,31 @@ pub async fn check_ipc_service_status() -> Result<JsonResponse> {
error,
Type::Service,
true,
"Failed to parse service response: {}; raw data: {:?}",
"解析服务响应失败: {}; 原始数据: {:?}",
e,
data
);
bail!("Unable to parse service response data: {}", e)
bail!("无法解析服务响应数据: {}", e)
}
}
}
}
None => {
logging!(error, Type::Service, true, "No data in service response");
bail!("No data in service response")
logging!(error, Type::Service, true, "服务响应中没有数据");
bail!("服务响应中没有数据")
}
}
}
Err(e) => {
logging!(
error,
Type::Service,
true,
"IPC communication failed: {}",
e
);
bail!("Unable to connect to Koala Clash Service: {}", e)
logging!(error, Type::Service, true, "IPC通信失败: {}", e);
bail!("无法连接到Clash Verge Service: {}", e)
}
}
}
/// 检查服务版本 - 使用IPC通信
pub async fn check_service_version() -> Result<String> {
logging!(
info,
Type::Service,
true,
"Starting service version check (IPC)"
);
logging!(info, Type::Service, true, "开始检查服务版本 (IPC)");
let payload = serde_json::json!({});
// logging!(debug, Type::Service, true, "发送GetVersion请求");
@@ -628,14 +604,8 @@ pub async fn check_service_version() -> Result<String> {
if !response.success {
let err_msg = response
.error
.unwrap_or_else(|| "Failed to get service version".to_string());
logging!(
error,
Type::Service,
true,
"Failed to get service version: {}",
err_msg
);
.unwrap_or_else(|| "获取服务版本失败".to_string());
logging!(error, Type::Service, true, "获取版本错误: {}", err_msg);
bail!(err_msg);
}
@@ -648,7 +618,7 @@ pub async fn check_service_version() -> Result<String> {
info,
Type::Service,
true,
"Service version: {}",
"获取到服务版本: {}",
version_str
);
return Ok(version_str.to_string());
@@ -658,7 +628,7 @@ pub async fn check_service_version() -> Result<String> {
error,
Type::Service,
true,
"Nested data does not contain version field: {:?}",
"嵌套数据中没有version字段: {:?}",
nested_data
);
} else {
@@ -669,7 +639,7 @@ pub async fn check_service_version() -> Result<String> {
info,
Type::Service,
true,
"Received service version: {}",
"获取到服务版本: {}",
version_response.version
);
return Ok(version_response.version);
@@ -679,55 +649,44 @@ pub async fn check_service_version() -> Result<String> {
error,
Type::Service,
true,
"Failed to parse version response: {}; raw data: {:?}",
"解析版本响应失败: {}; 原始数据: {:?}",
e,
data
);
bail!("Unable to parse service version data: {}", e)
bail!("无法解析服务版本数据: {}", e)
}
}
}
bail!("No valid version information found in response")
bail!("响应中未找到有效的版本信息")
}
None => {
logging!(error, Type::Service, true, "No data in version response");
bail!("No data in service version response")
logging!(error, Type::Service, true, "版本响应中没有数据");
bail!("服务版本响应中没有数据")
}
}
}
Err(e) => {
logging!(
error,
Type::Service,
true,
"IPC communication failed: {}",
e
);
bail!("Unable to connect to Koala Clash Service: {}", e)
logging!(error, Type::Service, true, "IPC通信失败: {}", e);
bail!("无法连接到Clash Verge Service: {}", e)
}
}
}
/// 检查服务是否需要重装
pub async fn check_service_needs_reinstall() -> bool {
logging!(
info,
Type::Service,
true,
"Checking whether service needs reinstallation"
);
logging!(info, Type::Service, true, "开始检查服务是否需要重装");
let service_state = ServiceState::get();
if !service_state.can_reinstall() {
log::info!(target: "app", "Service reinstall check: in cooldown period or max attempts reached");
log::info!(target: "app", "服务重装检查: 处于冷却期或已达最大尝试次数");
return false;
}
// 检查版本和可用性
match check_service_version().await {
Ok(version) => {
log::info!(target: "app", "Service version check: current={version}, required={REQUIRED_SERVICE_VERSION}");
log::info!(target: "app", "服务版本检测:当前={version}, 要求={REQUIRED_SERVICE_VERSION}");
/* logging!(
info,
Type::Service,
@@ -739,36 +698,25 @@ pub async fn check_service_needs_reinstall() -> bool {
let needs_reinstall = version != REQUIRED_SERVICE_VERSION;
if needs_reinstall {
log::warn!(target: "app", "Service version mismatch detected, reinstallation required! current={version}, required={REQUIRED_SERVICE_VERSION}");
logging!(
warn,
Type::Service,
true,
"Service version mismatch, reinstallation required"
);
log::warn!(target: "app", "发现服务版本不匹配,需要重装! 当前={version}, 要求={REQUIRED_SERVICE_VERSION}");
logging!(warn, Type::Service, true, "服务版本不匹配,需要重装");
// log::debug!(target: "app", "当前版本字节: {:?}", version.as_bytes());
// log::debug!(target: "app", "要求版本字节: {:?}", REQUIRED_SERVICE_VERSION.as_bytes());
} else {
log::info!(target: "app", "Service version matches, no reinstallation needed");
log::info!(target: "app", "服务版本匹配,无需重装");
// logging!(info, Type::Service, true, "服务版本匹配,无需重装");
}
needs_reinstall
}
Err(err) => {
logging!(
error,
Type::Service,
true,
"Failed to check service version: {}",
err
);
logging!(error, Type::Service, true, "检查服务版本失败: {}", err);
// 检查服务是否可用
match is_service_available().await {
Ok(()) => {
log::info!(target: "app", "Service is running but version check failed: {err}");
log::info!(target: "app", "服务正在运行但版本检查失败: {err}");
/* logging!(
info,
Type::Service,
@@ -779,7 +727,7 @@ pub async fn check_service_needs_reinstall() -> bool {
false
}
_ => {
log::info!(target: "app", "Service unavailable or not running, reinstallation needed");
log::info!(target: "app", "服务不可用或未运行,需要重装");
// logging!(info, Type::Service, true, "服务不可用或未运行,需要重装");
true
}
@@ -790,7 +738,7 @@ pub async fn check_service_needs_reinstall() -> bool {
/// 尝试使用服务启动core
pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result<()> {
log::info!(target:"app", "Attempting to start core with existing service (IPC)");
log::info!(target:"app", "尝试使用现有服务启动核心 (IPC)");
// logging!(info, Type::Service, true, "尝试使用现有服务启动核心");
let clash_core = Config::verge().latest().get_valid_clash_core();
@@ -833,16 +781,8 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
); */
if !response.success {
let err_msg = response
.error
.unwrap_or_else(|| "Failed to start core".to_string());
logging!(
error,
Type::Service,
true,
"Failed to start core: {}",
err_msg
);
let err_msg = response.error.unwrap_or_else(|| "启动核心失败".to_string());
logging!(error, Type::Service, true, "启动核心失败: {}", err_msg);
bail!(err_msg);
}
@@ -853,140 +793,127 @@ pub(super) async fn start_with_existing_service(config_file: &PathBuf) -> Result
let msg = data
.get("msg")
.and_then(|m| m.as_str())
.unwrap_or("Unknown error");
.unwrap_or("未知错误");
if code_value != 0 {
logging!(
error,
Type::Service,
true,
"Start core returned error: code={}, msg={}",
"启动核心返回错误: code={}, msg={}",
code_value,
msg
);
bail!("Failed to start core: {}", msg);
bail!("启动核心失败: {}", msg);
}
}
}
logging!(
info,
Type::Service,
true,
"Service successfully started core"
);
logging!(info, Type::Service, true, "服务成功启动核心");
Ok(())
}
Err(e) => {
logging!(
error,
Type::Service,
true,
"Failed to start core via IPC: {}",
e
);
bail!("Unable to connect to Koala Clash Service: {}", e)
logging!(error, Type::Service, true, "启动核心IPC通信失败: {}", e);
bail!("无法连接到Clash Verge Service: {}", e)
}
}
}
// 以服务启动core
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
log::info!(target: "app", "Attempting to start core via service");
log::info!(target: "app", "正在尝试通过服务启动核心");
// 先检查服务版本,不受冷却期限制
let version_check = match check_service_version().await {
Ok(version) => {
log::info!(target: "app", "Detected service version: {version}, required: {REQUIRED_SERVICE_VERSION}");
log::info!(target: "app", "检测到服务版本: {version}, 要求版本: {REQUIRED_SERVICE_VERSION}");
if version.as_bytes() != REQUIRED_SERVICE_VERSION.as_bytes() {
log::warn!(target: "app", "Service version mismatch, reinstallation required");
log::warn!(target: "app", "服务版本不匹配,需要重装");
false
} else {
log::info!(target: "app", "Service version matches");
log::info!(target: "app", "服务版本匹配");
true
}
}
Err(err) => {
log::warn!(target: "app", "Failed to get service version: {err}");
log::warn!(target: "app", "无法获取服务版本: {err}");
false
}
};
if version_check && is_service_available().await.is_ok() {
log::info!(target: "app", "Service is running and version matches, attempting to use it");
log::info!(target: "app", "服务已在运行且版本匹配,尝试使用");
return start_with_existing_service(config_file).await;
}
if !version_check {
log::info!(target: "app", "Service version mismatch, attempting reinstallation");
log::info!(target: "app", "服务版本不匹配,尝试重装");
let service_state = ServiceState::get();
if !service_state.can_reinstall() {
log::warn!(target: "app", "Cannot reinstall service due to limitations");
log::warn!(target: "app", "由于限制无法重装服务");
if let Ok(()) = start_with_existing_service(config_file).await {
log::info!(target: "app", "Service started successfully despite version mismatch");
log::info!(target: "app", "尽管版本不匹配,但成功启动了服务");
return Ok(());
} else {
bail!("Service version mismatch and cannot reinstall; startup failed");
bail!("服务版本不匹配且无法重装,启动失败");
}
}
log::info!(target: "app", "Starting service reinstallation");
log::info!(target: "app", "开始重装服务");
if let Err(err) = reinstall_service().await {
log::warn!(target: "app", "Service reinstallation failed: {err}");
log::warn!(target: "app", "服务重装失败: {err}");
log::info!(target: "app", "Attempting to use existing service");
log::info!(target: "app", "尝试使用现有服务");
return start_with_existing_service(config_file).await;
}
log::info!(target: "app", "Service reinstalled successfully, attempting to start");
log::info!(target: "app", "服务重装成功,尝试启动");
return start_with_existing_service(config_file).await;
}
// Check service status
// 检查服务状态
match check_ipc_service_status().await {
Ok(_) => {
log::info!(target: "app", "Service available but core not running, attempting to start");
log::info!(target: "app", "服务可用但未运行核心,尝试启动");
if let Ok(()) = start_with_existing_service(config_file).await {
return Ok(());
}
}
Err(err) => {
log::warn!(target: "app", "Service check failed: {err}");
log::warn!(target: "app", "服务检查失败: {err}");
}
}
// Service unavailable or startup failed, check if reinstallation is needed
// 服务不可用或启动失败,检查是否需要重装
if check_service_needs_reinstall().await {
log::info!(target: "app", "Service needs reinstallation");
log::info!(target: "app", "服务需要重装");
if let Err(err) = reinstall_service().await {
log::warn!(target: "app", "Service reinstallation failed: {err}");
log::warn!(target: "app", "服务重装失败: {err}");
bail!("Failed to reinstall service: {}", err);
}
log::info!(target: "app", "Service reinstallation completed, attempting to start core");
log::info!(target: "app", "服务重装完成,尝试启动核心");
start_with_existing_service(config_file).await
} else {
log::warn!(target: "app", "Service unavailable and cannot be reinstalled");
log::warn!(target: "app", "服务不可用且无法重装");
bail!("Service is not available and cannot be reinstalled at this time")
}
}
/// 通过服务停止core
pub(super) async fn stop_core_by_service() -> Result<()> {
logging!(info, Type::Service, true, "Stopping core via service (IPC)");
logging!(info, Type::Service, true, "通过服务停止核心 (IPC)");
let payload = serde_json::json!({});
let response = send_ipc_request(IpcCommand::StopClash, payload)
.await
.context("Unable to connect to Koala Clash Service")?;
.context("无法连接到Clash Verge Service")?;
if !response.success {
bail!(response
.error
.unwrap_or_else(|| "Failed to stop core".to_string()));
bail!(response.error.unwrap_or_else(|| "停止核心失败".to_string()));
}
if let Some(data) = &response.data {
@@ -995,18 +922,18 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
let msg = data
.get("msg")
.and_then(|m| m.as_str())
.unwrap_or("Unknown error");
.unwrap_or("未知错误");
if code_value != 0 {
logging!(
error,
Type::Service,
true,
"Stop core returned error: code={}, msg={}",
"停止核心返回错误: code={}, msg={}",
code_value,
msg
);
bail!("Failed to stop core: {}", msg);
bail!("停止核心失败: {}", msg);
}
}
}
@@ -1016,24 +943,19 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
/// 检查服务是否正在运行
pub async fn is_service_available() -> Result<()> {
logging!(
info,
Type::Service,
true,
"Checking whether service is running"
);
logging!(info, Type::Service, true, "开始检查服务是否正在运行");
match check_ipc_service_status().await {
Ok(resp) => {
if resp.code == 0 && resp.msg == "ok" && resp.data.is_some() {
logging!(info, Type::Service, true, "Service is running");
logging!(info, Type::Service, true, "服务正在运行");
Ok(())
} else {
logging!(
warn,
Type::Service,
true,
"Service not running normally: code={}, msg={}",
"服务未正常运行: code={}, msg={}",
resp.code,
resp.msg
);
@@ -1041,13 +963,7 @@ pub async fn is_service_available() -> Result<()> {
}
}
Err(err) => {
logging!(
error,
Type::Service,
true,
"Failed to check service running status: {}",
err
);
logging!(error, Type::Service, true, "检查服务运行状态失败: {}", err);
Err(err)
}
}
@@ -1055,21 +971,21 @@ pub async fn is_service_available() -> Result<()> {
/// 强制重装服务UI修复按钮
pub async fn force_reinstall_service() -> Result<()> {
log::info!(target: "app", "User requested forced service reinstallation");
log::info!(target: "app", "用户请求强制重装服务");
let service_state = ServiceState::default();
service_state.save()?;
log::info!(target: "app", "Service state reset, starting reinstallation");
log::info!(target: "app", "已重置服务状态,开始执行重装");
match reinstall_service().await {
Ok(()) => {
log::info!(target: "app", "Service reinstalled successfully");
log::info!(target: "app", "服务重装成功");
Ok(())
}
Err(err) => {
log::error!(target: "app", "Forced service reinstallation failed: {err}");
bail!("Forced service reinstallation failed: {}", err)
log::error!(target: "app", "强制重装服务失败: {err}");
bail!("强制重装服务失败: {}", err)
}
}
}

View File

@@ -6,9 +6,9 @@ use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH};
const IPC_SOCKET_NAME: &str = if cfg!(windows) {
r"\\.\pipe\koala-clash-service"
r"\\.\pipe\clash-verge-service"
} else {
"/tmp/koala-clash-service.sock"
"/tmp/clash-verge-service.sock"
};
// 定义命令类型
@@ -43,7 +43,7 @@ pub struct IpcResponse {
fn derive_secret_key() -> Vec<u8> {
// to do
// 从系统安全存储中获取或从程序安装时生成的密钥文件中读取
let unique_app_id = "koala-clash-app-secret-fuck-me-until-daylight";
let unique_app_id = "clash-verge-app-secret-fuck-me-until-daylight";
let mut hasher = Sha256::new();
hasher.update(unique_app_id.as_bytes());
hasher.finalize().to_vec()
@@ -85,7 +85,7 @@ fn sign_message(message: &str) -> Result<String> {
type HmacSha256 = Hmac<Sha256>;
let secret_key = derive_secret_key();
let mut mac = HmacSha256::new_from_slice(&secret_key).context("Failed to initialize HMAC")?;
let mut mac = HmacSha256::new_from_slice(&secret_key).context("HMAC初始化失败")?;
mac.update(message.as_bytes());
let result = mac.finalize();
@@ -129,25 +129,14 @@ pub async fn send_ipc_request(
winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE},
};
logging!(
info,
Type::Service,
true,
"Connecting to service (Windows)..."
);
logging!(info, Type::Service, true, "正在连接服务 (Windows)...");
let command_type = format!("{:?}", command);
let request = match create_signed_request(command, payload) {
Ok(req) => req,
Err(e) => {
logging!(
error,
Type::Service,
true,
"Failed to create signed request: {}",
e
);
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
return Err(e);
}
};
@@ -158,14 +147,8 @@ pub async fn send_ipc_request(
let c_pipe_name = match CString::new(IPC_SOCKET_NAME) {
Ok(name) => name,
Err(e) => {
logging!(
error,
Type::Service,
true,
"Failed to create CString: {}",
e
);
return Err(anyhow::anyhow!("Failed to create CString: {}", e));
logging!(error, Type::Service, true, "创建CString失败: {}", e);
return Err(anyhow::anyhow!("创建CString失败: {}", e));
}
};
@@ -187,110 +170,64 @@ pub async fn send_ipc_request(
error,
Type::Service,
true,
"Failed to connect to service named pipe: {}",
"连接到服务命名管道失败: {}",
error
);
return Err(anyhow::anyhow!("Unable to connect to service named pipe: {}", error));
return Err(anyhow::anyhow!("无法连接到服务命名管道: {}", error));
}
let mut pipe = unsafe { File::from_raw_handle(handle as RawHandle) };
logging!(
info,
Type::Service,
true,
"Service connection successful (Windows)"
);
logging!(info, Type::Service, true, "服务连接成功 (Windows)");
let request_bytes = request_json.as_bytes();
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
if let Err(e) = pipe.write_all(&len_bytes) {
logging!(
error,
Type::Service,
true,
"Failed to write request length: {}",
e
);
return Err(anyhow::anyhow!("Failed to write request length: {}", e));
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
}
if let Err(e) = pipe.write_all(request_bytes) {
logging!(
error,
Type::Service,
true,
"Failed to write request body: {}",
e
);
return Err(anyhow::anyhow!("Failed to write request body: {}", e));
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
}
if let Err(e) = pipe.flush() {
logging!(error, Type::Service, true, "Failed to flush pipe: {}", e);
return Err(anyhow::anyhow!("Failed to flush pipe: {}", e));
logging!(error, Type::Service, true, "刷新管道失败: {}", e);
return Err(anyhow::anyhow!("刷新管道失败: {}", e));
}
let mut response_len_bytes = [0u8; 4];
if let Err(e) = pipe.read_exact(&mut response_len_bytes) {
logging!(
error,
Type::Service,
true,
"Failed to read response length: {}",
e
);
return Err(anyhow::anyhow!("Failed to read response length: {}", e));
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
}
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
let mut response_bytes = vec![0u8; response_len];
if let Err(e) = pipe.read_exact(&mut response_bytes) {
logging!(
error,
Type::Service,
true,
"Failed to read response body: {}",
e
);
return Err(anyhow::anyhow!("Failed to read response body: {}", e));
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
}
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
Ok(r) => r,
Err(e) => {
logging!(
error,
Type::Service,
true,
"Failed to parse service response: {}",
e
);
return Err(anyhow::anyhow!("Failed to parse response: {}", e));
logging!(error, Type::Service, true, "服务响应解析失败: {}", e);
return Err(anyhow::anyhow!("解析响应失败: {}", e));
}
};
match verify_response_signature(&response) {
Ok(valid) => {
if !valid {
logging!(
error,
Type::Service,
true,
"Service response signature verification failed"
);
bail!("Service response signature verification failed");
logging!(error, Type::Service, true, "服务响应签名验证失败");
bail!("服务响应签名验证失败");
}
}
Err(e) => {
logging!(
error,
Type::Service,
true,
"Error verifying response signature: {}",
e
);
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
return Err(e);
}
}
@@ -299,7 +236,7 @@ pub async fn send_ipc_request(
info,
Type::Service,
true,
"IPC request completed: command={}, success={}",
"IPC请求完成: 命令={}, 成功={}",
command_type,
response.success
);
@@ -318,14 +255,14 @@ pub async fn send_ipc_request(
) -> Result<IpcResponse> {
use std::os::unix::net::UnixStream;
logging!(info, Type::Service, true, "Connecting to service (Unix)...");
logging!(info, Type::Service, true, "正在连接服务 (Unix)...");
let command_type = format!("{command:?}");
let request = match create_signed_request(command, payload) {
Ok(req) => req,
Err(e) => {
logging!(error, Type::Service, true, "Failed to create signed request: {}", e);
logging!(error, Type::Service, true, "创建签名请求失败: {}", e);
return Err(e);
}
};
@@ -334,23 +271,12 @@ pub async fn send_ipc_request(
let mut stream = match UnixStream::connect(IPC_SOCKET_NAME) {
Ok(s) => {
logging!(
info,
Type::Service,
true,
"Service connection successful (Unix)"
);
logging!(info, Type::Service, true, "服务连接成功 (Unix)");
s
}
Err(e) => {
logging!(
error,
Type::Service,
true,
"Failed to connect to Unix socket: {}",
e
);
return Err(anyhow::anyhow!("Unable to connect to service Unix socket: {}", e));
logging!(error, Type::Service, true, "连接到Unix套接字失败: {}", e);
return Err(anyhow::anyhow!("无法连接到服务Unix套接字: {}", e));
}
};
@@ -358,58 +284,46 @@ pub async fn send_ipc_request(
let len_bytes = (request_bytes.len() as u32).to_be_bytes();
if let Err(e) = std::io::Write::write_all(&mut stream, &len_bytes) {
logging!(error, Type::Service, true, "Failed to write request length: {}", e);
return Err(anyhow::anyhow!("Failed to write request length: {}", e));
logging!(error, Type::Service, true, "写入请求长度失败: {}", e);
return Err(anyhow::anyhow!("写入请求长度失败: {}", e));
}
if let Err(e) = std::io::Write::write_all(&mut stream, request_bytes) {
logging!(error, Type::Service, true, "Failed to write request body: {}", e);
return Err(anyhow::anyhow!("Failed to write request body: {}", e));
logging!(error, Type::Service, true, "写入请求内容失败: {}", e);
return Err(anyhow::anyhow!("写入请求内容失败: {}", e));
}
let mut response_len_bytes = [0u8; 4];
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_len_bytes) {
logging!(error, Type::Service, true, "Failed to read response length: {}", e);
return Err(anyhow::anyhow!("Failed to read response length: {}", e));
logging!(error, Type::Service, true, "读取响应长度失败: {}", e);
return Err(anyhow::anyhow!("读取响应长度失败: {}", e));
}
let response_len = u32::from_be_bytes(response_len_bytes) as usize;
let mut response_bytes = vec![0u8; response_len];
if let Err(e) = std::io::Read::read_exact(&mut stream, &mut response_bytes) {
logging!(error, Type::Service, true, "Failed to read response body: {}", e);
return Err(anyhow::anyhow!("Failed to read response body: {}", e));
logging!(error, Type::Service, true, "读取响应内容失败: {}", e);
return Err(anyhow::anyhow!("读取响应内容失败: {}", e));
}
let response: IpcResponse = match serde_json::from_slice::<IpcResponse>(&response_bytes) {
Ok(r) => r,
Err(e) => {
logging!(
error,
Type::Service,
true,
"Failed to parse service response: {}",
e,
);
return Err(anyhow::anyhow!("Failed to parse response: {}", e));
logging!(error, Type::Service, true, "服务响应解析失败: {}", e,);
return Err(anyhow::anyhow!("解析响应失败: {}", e));
}
};
match verify_response_signature(&response) {
Ok(valid) => {
if !valid {
logging!(error, Type::Service, true, "Service response signature verification failed");
bail!("Service response signature verification failed");
logging!(error, Type::Service, true, "服务响应签名验证失败");
bail!("服务响应签名验证失败");
}
}
Err(e) => {
logging!(
error,
Type::Service,
true,
"Error verifying response signature: {}",
e
);
logging!(error, Type::Service, true, "验证响应签名时出错: {}", e);
return Err(e);
}
}
@@ -418,7 +332,7 @@ pub async fn send_ipc_request(
info,
Type::Service,
true,
"IPC request completed: command={}, success={}",
"IPC请求完成: 命令={}, 成功={}",
command_type,
response.success
);

View File

@@ -63,7 +63,7 @@ impl Sysopt {
let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_app_started();
log::info!(target: "app", "Event-driven proxy guard enabled");
log::info!(target: "app", "已启用事件驱动代理守卫");
Ok(())
}
@@ -193,7 +193,7 @@ impl Sysopt {
let mut autoproxy = match Autoproxy::get_auto_proxy() {
Ok(ap) => ap,
Err(e) => {
log::warn!(target: "app", "Failed to get auto proxy config while resetting: {e}, using default config");
log::warn!(target: "app", "重置代理时获取自动代理配置失败: {e}, 使用默认配置");
Autoproxy {
enable: false,
url: "".to_string(),
@@ -248,14 +248,14 @@ impl Sysopt {
{
if is_enable {
if let Err(e) = startup_shortcut::create_shortcut() {
log::error!(target: "app", "Failed to create startup shortcut: {}", e);
log::error!(target: "app", "创建启动快捷方式失败: {}", e);
// 如果快捷方式创建失败,回退到原来的方法
self.try_original_autostart_method(is_enable);
} else {
return Ok(());
}
} else if let Err(e) = startup_shortcut::remove_shortcut() {
log::error!(target: "app", "Failed to remove startup shortcut: {}", e);
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
self.try_original_autostart_method(is_enable);
} else {
return Ok(());
@@ -290,11 +290,11 @@ impl Sysopt {
{
match startup_shortcut::is_shortcut_enabled() {
Ok(enabled) => {
log::info!(target: "app", "Shortcut auto-launch state: {}", enabled);
log::info!(target: "app", "快捷方式自启动状态: {}", enabled);
return Ok(enabled);
}
Err(e) => {
log::error!(target: "app", "Failed to check shortcut, falling back to original method: {}", e);
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {}", e);
}
}
}

View File

@@ -73,7 +73,7 @@ impl Timer {
logging!(
info,
Type::Timer,
"Registered timer task count: {}",
"已注册的定时任务数量: {}",
timer_map.len()
);
@@ -81,7 +81,7 @@ impl Timer {
logging!(
info,
Type::Timer,
"Registered timer task - uid={}, interval={}min, task_id={}",
"注册了定时任务 - uid={}, interval={}min, task_id={}",
uid,
task.interval_minutes,
task.task_id
@@ -100,12 +100,7 @@ impl Timer {
let uid = item.uid.as_ref()?;
if interval > 0 && cur_timestamp - updated >= interval * 60 {
logging!(
info,
Type::Timer,
"Profile requires immediate update: uid={}",
uid
);
logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid);
Some(uid.clone())
} else {
None
@@ -121,7 +116,7 @@ impl Timer {
logging!(
info,
Type::Timer,
"Number of profiles requiring immediate update: {}",
"需要立即更新的配置数量: {}",
profiles_to_update.len()
);
let timer_map = self.timer_map.read();
@@ -129,7 +124,7 @@ impl Timer {
for uid in profiles_to_update {
if let Some(task) = timer_map.get(&uid) {
logging!(info, Type::Timer, "Executing task immediately: uid={}", uid);
logging!(info, Type::Timer, "立即执行任务: uid={}", uid);
if let Err(e) = delay_timer.advance_task(task.task_id) {
logging!(warn, Type::Timer, "Failed to advance task {}: {}", uid, e);
}
@@ -242,7 +237,7 @@ impl Timer {
logging!(
debug,
Type::Timer,
"Found scheduled update config: uid={}, interval={}min",
"找到定时更新配置: uid={}, interval={}min",
uid,
interval
);
@@ -256,7 +251,7 @@ impl Timer {
logging!(
debug,
Type::Timer,
"Generated scheduled update config count: {}",
"生成的定时更新配置数量: {}",
new_map.len()
);
new_map
@@ -272,7 +267,7 @@ impl Timer {
logging!(
debug,
Type::Timer,
"Current timer_map size: {}",
"当前 timer_map 大小: {}",
timer_map.len()
);
@@ -284,7 +279,7 @@ impl Timer {
logging!(
debug,
Type::Timer,
"Timer task interval changed: uid={}, old={}, new={}",
"定时任务间隔变更: uid={}, ={}, ={}",
uid,
task.interval_minutes,
interval
@@ -293,12 +288,12 @@ impl Timer {
}
None => {
// Task no longer needed
logging!(debug, Type::Timer, "Timer task removed: uid={}", uid);
logging!(debug, Type::Timer, "定时任务已删除: uid={}", uid);
diff_map.insert(uid.clone(), DiffFlag::Del(task.task_id));
}
_ => {
// Task exists with same interval, no change needed
logging!(debug, Type::Timer, "Timer task unchanged: uid={}", uid);
logging!(debug, Type::Timer, "定时任务保持不变: uid={}", uid);
}
}
}
@@ -311,7 +306,7 @@ impl Timer {
logging!(
debug,
Type::Timer,
"Added timer task: uid={}, interval={}min",
"新增定时任务: uid={}, interval={}min",
uid,
interval
);
@@ -325,13 +320,7 @@ impl Timer {
*self.timer_count.lock() = next_id;
}
logging!(debug, Type::Timer, "Number of scheduled task changes: {}", diff_map.len());
logging!(
debug,
Type::Timer,
"Number of timer task changes: {}",
diff_map.len()
);
logging!(debug, Type::Timer, "定时任务变更数量: {}", diff_map.len());
diff_map
}
@@ -374,18 +363,13 @@ impl Timer {
/// Get next update time for a profile
pub fn get_next_update_time(&self, uid: &str) -> Option<i64> {
logging!(info, Type::Timer, "Getting next update time, uid={}", uid);
logging!(info, Type::Timer, "获取下次更新时间,uid={}", uid);
let timer_map = self.timer_map.read();
let task = match timer_map.get(uid) {
Some(t) => t,
None => {
logging!(
warn,
Type::Timer,
"Corresponding timer task not found, uid={}",
uid
);
logging!(warn, Type::Timer, "找不到对应的定时任务uid={}", uid);
return None;
}
};
@@ -396,7 +380,7 @@ impl Timer {
let items = match profiles.get_items() {
Some(i) => i,
None => {
logging!(warn, Type::Timer, "Failed to get profile list");
logging!(warn, Type::Timer, "获取配置列表失败");
return None;
}
};
@@ -404,12 +388,7 @@ impl Timer {
let profile = match items.iter().find(|item| item.uid.as_deref() == Some(uid)) {
Some(p) => p,
None => {
logging!(
warn,
Type::Timer,
"Corresponding profile not found, uid={}",
uid
);
logging!(warn, Type::Timer, "找不到对应的配置uid={}", uid);
return None;
}
};
@@ -422,7 +401,7 @@ impl Timer {
logging!(
info,
Type::Timer,
"Calculated next update time: {}, uid={}",
"计算得到下次更新时间: {}, uid={}",
next_time,
uid
);
@@ -431,7 +410,7 @@ impl Timer {
logging!(
warn,
Type::Timer,
"Invalid update time or interval, updated={}, interval={}",
"更新时间或间隔无效,updated={}, interval={}",
updated,
task.interval_minutes
);
@@ -463,7 +442,7 @@ impl Timer {
logging!(
info,
Type::Timer,
"Is profile {} currently active: {}",
"配置 {} 是否为当前激活配置: {}",
uid,
is_current
);

View File

@@ -3,6 +3,7 @@ use tauri::tray::TrayIconBuilder;
#[cfg(target_os = "macos")]
pub mod speed_rate;
use crate::{
cmd,
config::Config,
feat, logging,
module::{lightweight::is_in_lightweight_mode, mihomo::Rate},
@@ -45,7 +46,7 @@ fn should_handle_tray_click() -> bool {
*last_click = now;
true
} else {
log::debug!(target: "app", "Tray click ignored by debounce; time since last click: {:?}ms",
log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms",
now.duration_since(*last_click).as_millis());
false
}
@@ -230,7 +231,7 @@ impl Tray {
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::warn!(target: "app", "Failed to update tray menu: app_handle not found");
log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在");
return Ok(());
}
};
@@ -278,11 +279,11 @@ impl Tray {
profile_uid_and_name,
is_lightweight_mode,
)?));
log::debug!(target: "app", "Tray menu updated successfully");
log::debug!(target: "app", "托盘菜单更新成功");
Ok(())
}
None => {
log::warn!(target: "app", "Failed to update tray menu: tray not found");
log::warn!(target: "app", "更新托盘菜单失败: 托盘不存在");
Ok(())
}
}
@@ -294,7 +295,7 @@ impl Tray {
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::warn!(target: "app", "Failed to update tray icon: app_handle not found");
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
return Ok(());
}
};
@@ -302,7 +303,7 @@ impl Tray {
let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray,
None => {
log::warn!(target: "app", "Failed to update tray icon: tray not found");
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
return Ok(());
}
};
@@ -331,7 +332,7 @@ impl Tray {
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::warn!(target: "app", "Failed to update tray icon: app_handle not found");
log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
return Ok(());
}
};
@@ -339,7 +340,7 @@ impl Tray {
let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray,
None => {
log::warn!(target: "app", "Failed to update tray icon: tray not found");
log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
return Ok(());
}
};
@@ -375,7 +376,7 @@ impl Tray {
let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle,
None => {
log::warn!(target: "app", "Failed to update tray tooltip: app_handle not found");
log::warn!(target: "app", "更新托盘提示失败: app_handle不存在");
return Ok(());
}
};
@@ -383,7 +384,7 @@ impl Tray {
let version = match VERSION.get() {
Some(v) => v,
None => {
log::warn!(target: "app", "Failed to update tray tooltip: version info not found");
log::warn!(target: "app", "更新托盘提示失败: 版本信息不存在");
return Ok(());
}
};
@@ -413,7 +414,7 @@ impl Tray {
if let Some(tray) = app_handle.tray_by_id("main") {
let _ = tray.set_tooltip(Some(&format!(
"Koala Clash {version}\n{}: {}\n{}: {}\n{}: {}",
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
t("SysProxy"),
switch_map[system_proxy],
t("TUN"),
@@ -422,7 +423,7 @@ impl Tray {
current_profile_name
)));
} else {
log::warn!(target: "app", "Failed to update tray tooltip: tray not found");
log::warn!(target: "app", "更新托盘提示失败: 托盘不存在");
}
Ok(())
@@ -442,7 +443,7 @@ impl Tray {
pub fn unsubscribe_traffic(&self) {}
pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> {
log::info!(target: "app", "Creating system tray from AppHandle");
log::info!(target: "app", "正在从AppHandle创建系统托盘");
// 获取图标
let icon_bytes = TrayState::get_common_tray_icon().1;
@@ -490,20 +491,20 @@ impl Tray {
"tun_mode" => feat::toggle_tun_mode(None),
"main_window" => {
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Tray click: show main window");
log::info!(target: "app", "Tray点击事件: 显示主窗口");
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
log::info!(target: "app", "当前在轻量模式,正在退出轻量模式");
crate::module::lightweight::exit_lightweight_mode();
}
let result = WindowManager::show_main_window();
log::info!(target: "app", "Window show result: {result:?}");
log::info!(target: "app", "窗口显示结果: {result:?}");
}
_ => {}
}
}
});
tray.on_menu_event(on_menu_event);
log::info!(target: "app", "System tray created successfully");
log::info!(target: "app", "系统托盘创建成功");
Ok(())
}
@@ -600,6 +601,16 @@ fn create_tray_menu(
)
.unwrap();
let direct_mode = &CheckMenuItem::with_id(
app_handle,
"direct_mode",
t("Direct Mode"),
true,
mode == "direct",
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
)
.unwrap();
let profiles = &Submenu::with_id_and_items(
app_handle,
"profiles",
@@ -639,6 +650,45 @@ fn create_tray_menu(
)
.unwrap();
let copy_env =
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
let open_app_dir = &MenuItem::with_id(
app_handle,
"open_app_dir",
t("Conf Dir"),
true,
None::<&str>,
)
.unwrap();
let open_core_dir = &MenuItem::with_id(
app_handle,
"open_core_dir",
t("Core Dir"),
true,
None::<&str>,
)
.unwrap();
let open_logs_dir = &MenuItem::with_id(
app_handle,
"open_logs_dir",
t("Logs Dir"),
true,
None::<&str>,
)
.unwrap();
let open_dir = &Submenu::with_id_and_items(
app_handle,
"open_dir",
t("Open Dir"),
true,
&[open_app_dir, open_core_dir, open_logs_dir],
)
.unwrap();
let restart_clash = &MenuItem::with_id(
app_handle,
"restart_clash",
@@ -686,6 +736,7 @@ fn create_tray_menu(
separator,
rule_mode,
global_mode,
direct_mode,
separator,
profiles,
separator,
@@ -693,6 +744,8 @@ fn create_tray_menu(
tun_mode,
separator,
lighteweight_mode,
copy_env,
open_dir,
more,
separator,
quit,
@@ -717,18 +770,18 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
}
"open_window" => {
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Tray menu click: open window");
log::info!(target: "app", "托盘菜单点击: 打开窗口");
if !should_handle_tray_click() {
return;
}
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "Currently in lightweight mode, exiting");
log::info!(target: "app", "当前在轻量模式,正在退出");
crate::module::lightweight::exit_lightweight_mode();
}
let result = WindowManager::show_main_window();
log::info!(target: "app", "Window show result: {result:?}");
log::info!(target: "app", "窗口显示结果: {result:?}");
}
"system_proxy" => {
feat::toggle_system_proxy();
@@ -736,6 +789,16 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
"tun_mode" => {
feat::toggle_tun_mode(None);
}
"copy_env" => feat::copy_clash_env(),
"open_app_dir" => {
let _ = cmd::open_app_dir();
}
"open_core_dir" => {
let _ = cmd::open_core_dir();
}
"open_logs_dir" => {
let _ = cmd::open_logs_dir();
}
"restart_clash" => feat::restart_clash_core(),
"restart_app" => feat::restart_app(),
"entry_lightweight_mode" => {
@@ -753,7 +816,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
if was_lightweight {
use crate::utils::window_manager::WindowManager;
let result = WindowManager::show_main_window();
log::info!(target: "app", "Show main window after exiting lightweight mode: {result:?}");
log::info!(target: "app", "退出轻量模式后显示主窗口: {result:?}");
}
}
"quit" => {
@@ -767,6 +830,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
}
if let Err(e) = Tray::global().update_all_states() {
log::warn!(target: "app", "Failed to update tray state: {e}");
log::warn!(target: "app", "更新托盘状态失败: {e}");
}
}

View File

@@ -108,8 +108,8 @@ impl ChainSupport {
(self, core.as_str()),
(ChainSupport::All, _)
| (ChainSupport::Clash, "clash")
| (ChainSupport::ClashMeta, "koala-mihomo")
| (ChainSupport::ClashMetaAlpha, "koala-mihomo-alpha")
| (ChainSupport::ClashMeta, "verge-mihomo")
| (ChainSupport::ClashMetaAlpha, "verge-mihomo-alpha")
),
None => true,
}

View File

@@ -202,9 +202,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
});
let patch_tun = value.as_mapping().cloned().unwrap_or(Mapping::new());
for (key, value) in patch_tun.into_iter() {
if !tun.contains_key(&key) {
tun.insert(key, value);
}
tun.insert(key, value);
}
config.insert("tun".into(), tun.into());
} else {
@@ -241,7 +239,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
.map(|(_, c)| c)
.for_each(|item| {
log::debug!(target: "app", "run builtin script {0}", item.uid);
log::debug!(target: "app", "run builtin script {}", item.uid);
if let ChainType::Script(script) = item.data {
match use_script(script, config.to_owned(), "".to_string()) {
Ok((res_config, _)) => {

View File

@@ -141,8 +141,8 @@ fn test_script() {
fn test_escape_unescape() {
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
let escaped = escape_js_string_for_single_quote(test_string);
println!("Original: {test_string}");
println!("Escaped: {escaped}");
println!("Original: {}", test_string);
println!("Escaped: {}", escaped);
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
let parsed = parse_json_safely(json_str).unwrap();

View File

@@ -60,7 +60,7 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
#[cfg(target_os = "macos")]
{
crate::utils::resolve::restore_public_dns().await;
crate::utils::resolve::set_public_dns("8.8.8.8".to_string()).await;
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
}
}

View File

@@ -31,13 +31,7 @@ pub async fn update_profile(
option: Option<PrfOption>,
auto_refresh: Option<bool>,
) -> Result<()> {
logging!(
info,
Type::Config,
true,
"[Subscription Update] Start updating subscription {}",
uid
);
logging!(info, Type::Config, true, "[订阅更新] 开始更新订阅 {}", uid);
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true保持兼容性
let url_opt = {
@@ -47,14 +41,14 @@ pub async fn update_profile(
let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
if !is_remote {
log::info!(target: "app", "[Subscription Update] {uid} is not a remote subscription, skipping update");
log::info!(target: "app", "[订阅更新] {uid} 不是远程订阅,跳过更新");
None // 非远程订阅直接更新
} else if item.url.is_none() {
log::warn!(target: "app", "[Subscription Update] {uid} is missing URL, cannot update");
log::warn!(target: "app", "[订阅更新] {uid} 缺少URL无法更新");
bail!("failed to get the profile item url");
} else {
log::info!(target: "app",
"[Subscription Update] {} is a remote subscription, URL: {}",
"[订阅更新] {} 是远程订阅,URL: {}",
uid,
item.url.clone().unwrap()
);
@@ -64,24 +58,24 @@ pub async fn update_profile(
let should_update = match url_opt {
Some((url, opt)) => {
log::info!(target: "app", "[Subscription Update] Start downloading new subscription content");
log::info!(target: "app", "[订阅更新] 开始下载新的订阅内容");
let merged_opt = PrfOption::merge(opt.clone(), option.clone());
// 尝试使用正常设置更新
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
Ok(item) => {
log::info!(target: "app", "[Subscription Update] Subscription config updated successfully");
log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
let profiles = Config::profiles();
let mut profiles = profiles.latest();
profiles.update_item(uid.clone(), item)?;
let is_current = Some(uid.clone()) == profiles.get_current();
log::info!(target: "app", "[Subscription Update] Is current active subscription: {is_current}");
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
is_current && auto_refresh
}
Err(err) => {
// 首次更新失败尝试使用Clash代理
log::warn!(target: "app", "[Subscription Update] Normal update failed: {err}, trying to update via Clash proxy");
log::warn!(target: "app", "[订阅更新] 正常更新失败: {err}尝试使用Clash代理更新");
// 发送通知
handle::Handle::notice_message("update_retry_with_clash", uid.clone());
@@ -98,7 +92,7 @@ pub async fn update_profile(
// 使用Clash代理重试
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
Ok(mut item) => {
log::info!(target: "app", "[Subscription Update] Update via Clash proxy succeeded");
log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功");
// 恢复原始代理设置到item
if let Some(option) = item.option.as_mut() {
@@ -118,11 +112,11 @@ pub async fn update_profile(
handle::Handle::notice_message("update_with_clash_proxy", profile_name);
let is_current = Some(uid.clone()) == profiles.get_current();
log::info!(target: "app", "[Subscription Update] Is current active subscription: {is_current}");
log::info!(target: "app", "[订阅更新] 是否为当前使用的订阅: {is_current}");
is_current && auto_refresh
}
Err(retry_err) => {
log::error!(target: "app", "[Subscription Update] Update via Clash proxy still failed: {retry_err}");
log::error!(target: "app", "[订阅更新] 使用Clash代理更新仍然失败: {retry_err}");
handle::Handle::notice_message(
"update_failed_even_with_clash",
format!("{retry_err}"),
@@ -137,30 +131,14 @@ pub async fn update_profile(
};
if should_update {
logging!(
info,
Type::Config,
true,
"[Subscription Update] Update core configuration"
);
logging!(info, Type::Config, true, "[订阅更新] 更新内核配置");
match CoreManager::global().update_config().await {
Ok(_) => {
logging!(
info,
Type::Config,
true,
"[Subscription Update] Update succeeded"
);
logging!(info, Type::Config, true, "[订阅更新] 更新成功");
handle::Handle::refresh_clash();
}
Err(err) => {
logging!(
error,
Type::Config,
true,
"[Subscription Update] Update failed: {}",
err
);
logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
handle::Handle::notice_message("update_failed", format!("{err}"));
log::error!(target: "app", "{err}");
}

View File

@@ -25,14 +25,14 @@ fn open_or_close_dashboard_internal(bypass_debounce: bool) {
use crate::process::AsyncHandler;
use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Attempting to open/close dashboard (bypass debounce: {bypass_debounce})");
log::info!(target: "app", "Attempting to open/close dashboard (绕过防抖: {bypass_debounce})");
// 热键调用调度到主线程执行,避免 WebView 创建死锁
if bypass_debounce {
log::info!(target: "app", "Hotkey invoked, dispatching window operation to main thread");
log::info!(target: "app", "热键调用,调度到主线程执行窗口操作");
AsyncHandler::spawn(move || async move {
log::info!(target: "app", "Executing hotkey window operation on main thread");
log::info!(target: "app", "主线程中执行热键窗口操作");
if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode");
@@ -64,7 +64,7 @@ fn open_or_close_dashboard_internal(bypass_debounce: bool) {
/// 异步优化的应用退出函数
pub fn quit() {
use crate::process::AsyncHandler;
logging!(debug, Type::System, true, "Start exit process");
logging!(debug, Type::System, true, "启动退出流程");
// 获取应用句柄并设置退出标志
let app_handle = handle::Handle::global().app_handle().unwrap();
@@ -73,24 +73,19 @@ pub fn quit() {
// 优先关闭窗口,提供立即反馈
if let Some(window) = handle::Handle::global().get_window() {
let _ = window.hide();
log::info!(target: "app", "Window hidden");
log::info!(target: "app", "窗口已隐藏");
}
// 使用异步任务处理资源清理,避免阻塞
AsyncHandler::spawn(move || async move {
logging!(
info,
Type::System,
true,
"Start asynchronous resource cleanup"
);
logging!(info, Type::System, true, "开始异步清理资源");
let cleanup_result = clean_async().await;
logging!(
info,
Type::System,
true,
"Resource cleanup completed, exit code: {}",
"资源清理完成,退出代码: {}",
if cleanup_result { 0 } else { 1 }
);
app_handle.exit(if cleanup_result { 0 } else { 1 });
@@ -100,12 +95,7 @@ pub fn quit() {
async fn clean_async() -> bool {
use tokio::time::{timeout, Duration};
logging!(
info,
Type::System,
true,
"Start executing asynchronous cleanup..."
);
logging!(info, Type::System, true, "开始执行异步清理操作...");
// 1. 处理TUN模式
let tun_task = async {
@@ -122,11 +112,11 @@ async fn clean_async() -> bool {
.await
{
Ok(_) => {
log::info!(target: "app", "TUN mode disabled");
log::info!(target: "app", "TUN模式已禁用");
true
}
Err(_) => {
log::warn!(target: "app", "Timeout disabling TUN mode");
log::warn!(target: "app", "禁用TUN模式超时");
false
}
}
@@ -144,11 +134,11 @@ async fn clean_async() -> bool {
.await
{
Ok(_) => {
log::info!(target: "app", "System proxy reset");
log::info!(target: "app", "系统代理已重置");
true
}
Err(_) => {
log::warn!(target: "app", "Timeout resetting system proxy");
log::warn!(target: "app", "重置系统代理超时");
false
}
}
@@ -158,11 +148,11 @@ async fn clean_async() -> bool {
let core_task = async {
match timeout(Duration::from_secs(3), CoreManager::global().stop_core()).await {
Ok(_) => {
log::info!(target: "app", "Core service stopped");
log::info!(target: "app", "核心服务已停止");
true
}
Err(_) => {
log::warn!(target: "app", "Timeout stopping core service");
log::warn!(target: "app", "停止核心服务超时");
false
}
}
@@ -178,11 +168,11 @@ async fn clean_async() -> bool {
.await
{
Ok(_) => {
log::info!(target: "app", "DNS settings restored");
log::info!(target: "app", "DNS设置已恢复");
true
}
Err(_) => {
log::warn!(target: "app", "Timeout restoring DNS settings");
log::warn!(target: "app", "恢复DNS设置超时");
false
}
}
@@ -202,7 +192,7 @@ async fn clean_async() -> bool {
info,
Type::System,
true,
"Asynchronous cleanup completed - TUN: {}, Proxy: {}, Core: {}, DNS: {}, Overall: {}",
"异步清理操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
tun_success,
proxy_success,
core_success,
@@ -219,7 +209,7 @@ pub fn clean() -> bool {
let (tx, rx) = std::sync::mpsc::channel();
AsyncHandler::spawn(move || async move {
logging!(info, Type::System, true, "Start executing cleanup...");
logging!(info, Type::System, true, "开始执行清理操作...");
// 使用已有的异步清理函数
let cleanup_result = clean_async().await;
@@ -230,13 +220,7 @@ pub fn clean() -> bool {
match rx.recv_timeout(std::time::Duration::from_secs(8)) {
Ok(result) => {
logging!(
info,
Type::System,
true,
"Cleanup completed, result: {}",
result
);
logging!(info, Type::System, true, "清理操作完成,结果: {}", result);
result
}
Err(_) => {
@@ -244,7 +228,7 @@ pub fn clean() -> bool {
warn,
Type::System,
true,
"Cleanup timed out, returning success to avoid blocking"
"清理操作超时,返回成功状态避免阻塞"
);
true
}

View File

@@ -7,7 +7,11 @@ mod module;
mod process;
mod state;
mod utils;
use crate::{core::hotkey, process::AsyncHandler, utils::resolve};
use crate::{
core::hotkey,
process::AsyncHandler,
utils::{resolve, resolve::resolve_scheme, server},
};
use config::Config;
use std::sync::{Mutex, Once};
use tauri::AppHandle;
@@ -82,75 +86,85 @@ impl AppHandleManager {
#[allow(clippy::panic)]
pub fn run() {
// Capture early deep link before any async setup (cold start on macOS)
utils::resolve::capture_early_deep_link_from_args();
utils::network::NetworkManager::global().init();
utils::network::NetworkManager::global().init();
let _ = utils::dirs::init_portable_flag();
// 异步单例检测
AsyncHandler::spawn(move || async move {
logging!(info, Type::Setup, true, "开始检查单例实例...");
match timeout(Duration::from_secs(3), server::check_singleton()).await {
Ok(result) => {
if result.is_err() {
logging!(info, Type::Setup, true, "检测到已有应用实例运行");
if let Some(app_handle) = AppHandleManager::global().get() {
app_handle.exit(0);
} else {
std::process::exit(0);
}
} else {
logging!(info, Type::Setup, true, "未检测到其他应用实例");
}
}
Err(_) => {
logging!(
warn,
Type::Setup,
true,
"单例检查超时,假定没有其他实例运行"
);
}
}
});
#[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
#[cfg(debug_assertions)]
let devtools = tauri_plugin_devtools::init();
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|_app, argv, _cwd| {
// Handle deep link when a second instance is invoked: forward URL to the running instance
if let Some(url) = argv
.iter()
.find(|a| a.starts_with("clash://") || a.starts_with("koala-clash://"))
.cloned()
{
// Robust scheduling avoids races with lightweight/window
resolve::schedule_handle_deep_link(url);
}
}))
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init())
.setup(|app| {
logging!(info, Type::Setup, true, "Starting app initialization...");
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_deep_link::init())
.setup(|app| {
logging!(info, Type::Setup, true, "开始应用初始化...");
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
#[cfg(target_os = "macos")]
{
auto_start_plugin_builder = auto_start_plugin_builder
.macos_launcher(MacosLauncher::LaunchAgent)
.app_name(app.config().identifier.clone());
}
let _ = app.handle().plugin(auto_start_plugin_builder.build());
// Register deep link handler as early as possible to not miss cold-start events (macOS)
app.deep_link().on_open_url(|event| {
let urls: Vec<String> = event.urls().iter().map(|u| u.to_string()).collect();
logging!(info, Type::Setup, true, "on_open_url received: {:?}", urls);
if let Some(url) = urls.first().cloned() {
resolve::schedule_handle_deep_link(url);
}
});
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{
use tauri_plugin_deep_link::DeepLinkExt;
logging!(info, Type::Setup, true, "注册深层链接...");
logging_error!(Type::System, true, app.deep_link().register_all());
}
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
#[cfg(target_os = "macos")]
{
auto_start_plugin_builder = auto_start_plugin_builder
.macos_launcher(MacosLauncher::LaunchAgent)
.app_name(app.config().identifier.clone());
}
let _ = app.handle().plugin(auto_start_plugin_builder.build());
// Ensure URL schemes are registered with the OS (all platforms)
logging!(info, Type::Setup, true, "Registering deep links with OS...");
logging_error!(Type::System, true, app.deep_link().register_all());
// Deep link handler will be registered AFTER core handle init to ensure window creation works
app.deep_link().on_open_url(|event| {
AsyncHandler::spawn(move || {
let url = event.urls().first().map(|u| u.to_string());
async move {
if let Some(url) = url {
logging_error!(Type::Setup, true, resolve_scheme(url).await);
}
}
});
});
// 窗口管理
logging!(
info,
Type::Setup,
true,
"Initializing window state management..."
);
logging!(info, Type::Setup, true, "初始化窗口状态管理...");
let window_state_plugin = tauri_plugin_window_state::Builder::new()
.with_filename("window_state.json")
.with_state_flags(tauri_plugin_window_state::StateFlags::default())
@@ -160,12 +174,7 @@ pub fn run() {
// 异步处理
let app_handle = app.handle().clone();
AsyncHandler::spawn(move || async move {
logging!(
info,
Type::Setup,
true,
"Executing app setup asynchronously..."
);
logging!(info, Type::Setup, true, "异步执行应用设置...");
match timeout(
Duration::from_secs(30),
resolve::resolve_setup_async(&app_handle),
@@ -173,81 +182,49 @@ pub fn run() {
.await
{
Ok(_) => {
logging!(info, Type::Setup, true, "App setup completed successfully");
logging!(info, Type::Setup, true, "应用设置成功完成");
}
Err(_) => {
logging!(
error,
Type::Setup,
true,
"App setup timed out (30s), continuing with subsequent steps"
"应用设置超时(30秒),继续执行后续流程"
);
}
}
});
logging!(
info,
Type::Setup,
true,
"Executing main setup operations..."
);
logging!(info, Type::Setup, true, "执行主要设置操作...");
logging!(info, Type::Setup, true, "Initializing AppHandleManager...");
logging!(info, Type::Setup, true, "初始化AppHandleManager...");
AppHandleManager::global().init(app.handle().clone());
logging!(info, Type::Setup, true, "Initializing core handle...");
logging!(info, Type::Setup, true, "初始化核心句柄...");
core::handle::Handle::global().init(app.handle());
logging!(info, Type::Setup, true, "Initializing config...");
logging!(info, Type::Setup, true, "初始化配置...");
if let Err(e) = utils::init::init_config() {
logging!(
error,
Type::Setup,
true,
"Failed to initialize config: {}",
e
);
logging!(error, Type::Setup, true, "初始化配置失败: {}", e);
}
logging!(info, Type::Setup, true, "Initializing resources...");
logging!(info, Type::Setup, true, "初始化资源...");
if let Err(e) = utils::init::init_resources() {
logging!(
error,
Type::Setup,
true,
"Failed to initialize resources: {}",
e
);
logging!(error, Type::Setup, true, "初始化资源失败: {}", e);
}
app.manage(Mutex::new(state::proxy::CmdProxyState::default()));
app.manage(Mutex::new(state::lightweight::LightWeightState::default()));
// If an early deep link was captured from argv, schedule it now (after core and window can be created)
utils::resolve::replay_early_deep_link();
tauri::async_runtime::spawn(async {
tokio::time::sleep(Duration::from_secs(5)).await;
logging!(info, Type::Cmd, true, "Running profile updates at startup...");
if let Err(e) = crate::cmd::update_profiles_on_startup().await {
log::error!("Failed to update profiles on startup: {}", e);
}
});
// (deep link handler already registered above)
tauri::async_runtime::spawn(async {
tokio::time::sleep(Duration::from_secs(5)).await;
logging!(
info,
Type::Cmd,
true,
"Running profile updates at startup..."
);
if let Err(e) = crate::cmd::update_profiles_on_startup().await {
log::error!("Failed to update profiles on startup: {e}");
}
});
logging!(
info,
Type::Setup,
true,
"Initialization completed, continuing"
);
logging!(info, Type::Setup, true, "初始化完成,继续执行");
Ok(())
})
.invoke_handler(tauri::generate_handler![
@@ -367,7 +344,7 @@ pub fn run() {
app.run(|app_handle, e| match e {
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => {
logging!(info, Type::System, true, "App ready or resumed");
logging!(info, Type::System, true, "应用就绪或恢复");
AppHandleManager::global().init(app_handle.clone());
#[cfg(target_os = "macos")]
{
@@ -375,7 +352,7 @@ pub fn run() {
.get_handle()
.get_webview_window("main")
{
logging!(info, Type::Window, true, "Setting macOS window title");
logging!(info, Type::Window, true, "设置macOS窗口标题");
let _ = window.set_title("Koala Clash");
}
}
@@ -387,10 +364,6 @@ pub fn run() {
} => {
if !has_visible_windows {
AppHandleManager::global().set_activation_policy_regular();
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
AppHandleManager::global().init(app_handle.clone());
}
@@ -411,6 +384,7 @@ pub fn run() {
match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
#[cfg(target_os = "macos")]
AppHandleManager::global().set_activation_policy_accessory();
if core::handle::Handle::global().is_exiting() {
return;
}
@@ -419,12 +393,7 @@ pub fn run() {
if let Some(window) = core::handle::Handle::global().get_window() {
let _ = window.hide();
} else {
logging!(
warn,
Type::Window,
true,
"Tried to hide window but it does not exist"
);
logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在");
}
}
tauri::WindowEvent::Focused(true) => {

View File

@@ -46,7 +46,7 @@ pub fn run_once_auto_lightweight() {
info,
Type::Lightweight,
true,
"Silent start detected: create window, then attach auto lightweight-mode listener"
"在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器"
);
set_lightweight_mode(false);
enable_auto_light_weight_mode();
@@ -70,7 +70,7 @@ pub fn auto_lightweight_mode_init() {
info,
Type::Lightweight,
true,
"Non-silent start: directly attach auto lightweight-mode listener"
"非静默启动直接挂载自动进入轻量模式监听器!"
);
set_lightweight_mode(true);
enable_auto_light_weight_mode();
@@ -102,26 +102,26 @@ pub fn set_lightweight_mode(value: bool) {
pub fn enable_auto_light_weight_mode() {
Timer::global().init().unwrap();
logging!(info, Type::Lightweight, true, "Enable auto lightweight mode");
logging!(info, Type::Lightweight, true, "开启自动轻量模式");
setup_window_close_listener();
setup_webview_focus_listener();
}
pub fn disable_auto_light_weight_mode() {
logging!(info, Type::Lightweight, true, "Disable auto lightweight mode");
logging!(info, Type::Lightweight, true, "关闭自动轻量模式");
let _ = cancel_light_weight_timer();
cancel_window_close_listener();
}
pub fn entry_lightweight_mode() {
use crate::utils::window_manager::WindowManager;
crate::utils::resolve::reset_ui_ready();
let result = WindowManager::hide_main_window();
logging!(
info,
Type::Lightweight,
true,
"Lightweight mode window hide result: {:?}",
"轻量模式隐藏窗口结果: {:?}",
result
);
@@ -150,7 +150,7 @@ pub fn exit_lightweight_mode() {
info,
Type::Lightweight,
true,
"Lightweight mode exit already in progress; skipping duplicate call"
"轻量模式退出操作已在进行中,跳过重复调用"
);
return;
}
@@ -162,7 +162,7 @@ pub fn exit_lightweight_mode() {
// 确保当前确实处于轻量模式才执行退出操作
if !is_in_lightweight_mode() {
logging!(info, Type::Lightweight, true, "Not in lightweight mode; skip exit");
logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出");
return;
}
@@ -192,7 +192,7 @@ fn setup_window_close_listener() -> u32 {
info,
Type::Lightweight,
true,
"Close requested; starting lightweight-mode timer"
"监听到关闭请求,开始轻量模式计时"
);
});
return handler;
@@ -207,7 +207,7 @@ fn setup_webview_focus_listener() -> u32 {
logging!(
info,
Type::Lightweight,
"Window focused; cancel lightweight-mode timer"
"监听到窗口获得焦点,取消轻量模式计时"
);
});
return handler;
@@ -218,7 +218,7 @@ fn setup_webview_focus_listener() -> u32 {
fn cancel_window_close_listener() {
if let Some(window) = handle::Handle::global().get_window() {
window.unlisten(setup_window_close_listener());
logging!(info, Type::Lightweight, true, "Removed window close listener");
logging!(info, Type::Lightweight, true, "取消了窗口关闭监听");
}
}
@@ -243,7 +243,7 @@ fn setup_light_weight_timer() -> Result<()> {
.set_maximum_parallel_runnable_num(1)
.set_frequency_once_by_minutes(once_by_minutes)
.spawn_async_routine(move || async move {
logging!(info, Type::Timer, true, "Timer expired; entering lightweight mode");
logging!(info, Type::Timer, true, "计时器到期,开始进入轻量模式");
entry_lightweight_mode();
})
.context("failed to create timer task")?;
@@ -271,7 +271,7 @@ fn setup_light_weight_timer() -> Result<()> {
info,
Type::Timer,
true,
"Timer set; will auto-enter lightweight mode after {} minute(s)",
"计时器已设置,{} 分钟后将自动进入轻量模式",
once_by_minutes
);
@@ -286,7 +286,7 @@ fn cancel_light_weight_timer() -> Result<()> {
delay_timer
.remove_task(task.task_id)
.context("failed to remove timer task")?;
logging!(info, Type::Timer, true, "Timer canceled");
logging!(info, Type::Timer, true, "计时器已取消");
}
Ok(())

View File

@@ -28,9 +28,9 @@ impl LightWeightState {
pub fn set_lightweight_mode(&mut self, value: bool) -> &Self {
self.is_lightweight = value;
if value {
logging!(info, Type::Lightweight, true, "Lightweight mode enabled");
logging!(info, Type::Lightweight, true, "轻量模式已开启");
} else {
logging!(info, Type::Lightweight, true, "Lightweight mode disabled");
logging!(info, Type::Lightweight, true, "轻量模式已关闭");
}
self
}

View File

@@ -9,7 +9,7 @@ use std::{fs, os::windows::process::CommandExt, path::Path, path::PathBuf};
/// Windows 下的开机启动文件夹路径
#[cfg(target_os = "windows")]
pub fn get_startup_dir() -> Result<PathBuf> {
let appdata = std::env::var("APPDATA").map_err(|_| anyhow!("Unable to obtain APPDATA environment variable"))?;
let appdata = std::env::var("APPDATA").map_err(|_| anyhow!("无法获取 APPDATA 环境变量"))?;
let startup_dir = Path::new(&appdata)
.join("Microsoft")
@@ -19,7 +19,7 @@ pub fn get_startup_dir() -> Result<PathBuf> {
.join("Startup");
if !startup_dir.exists() {
return Err(anyhow!("Startup directory does not exist: {:?}", startup_dir));
return Err(anyhow!("Startup 目录不存在: {:?}", startup_dir));
}
Ok(startup_dir)
@@ -29,7 +29,7 @@ pub fn get_startup_dir() -> Result<PathBuf> {
#[cfg(target_os = "windows")]
pub fn get_exe_path() -> Result<PathBuf> {
let exe_path =
std::env::current_exe().map_err(|e| anyhow!("Unable to obtain the path of the current executable file: {}", e))?;
std::env::current_exe().map_err(|e| anyhow!("无法获取当前可执行文件路径: {}", e))?;
Ok(exe_path)
}
@@ -39,11 +39,11 @@ pub fn get_exe_path() -> Result<PathBuf> {
pub fn create_shortcut() -> Result<()> {
let exe_path = get_exe_path()?;
let startup_dir = get_startup_dir()?;
let shortcut_path = startup_dir.join("Koala-Clash.lnk");
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
// If the shortcut already exists, return success directly
// 如果快捷方式已存在,直接返回成功
if shortcut_path.exists() {
info!(target: "app", "Startup shortcut already exists");
info!(target: "app", "启动快捷方式已存在");
return Ok(());
}
@@ -59,36 +59,36 @@ pub fn create_shortcut() -> Result<()> {
let output = std::process::Command::new("powershell")
.args(["-Command", &powershell_command])
// Hide the PowerShell window
// 隐藏 PowerShell 窗口
.creation_flags(0x08000000) // CREATE_NO_WINDOW
.output()
.map_err(|e| anyhow!("Failed to execute PowerShell command: {}", e))?;
.map_err(|e| anyhow!("执行 PowerShell 命令失败: {}", e))?;
if !output.status.success() {
let error_msg = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("Failed to create shortcut: {}", error_msg));
return Err(anyhow!("创建快捷方式失败: {}", error_msg));
}
info!(target: "app", "Successfully created startup shortcut");
info!(target: "app", "成功创建启动快捷方式");
Ok(())
}
/// Remove the shortcut
/// 删除快捷方式
#[cfg(target_os = "windows")]
pub fn remove_shortcut() -> Result<()> {
let startup_dir = get_startup_dir()?;
let shortcut_path = startup_dir.join("Koala-Clash.lnk");
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
// If the shortcut does not exist, return success directly
// 如果快捷方式不存在,直接返回成功
if !shortcut_path.exists() {
info!(target: "app", "Startup shortcut does not exist, nothing to remove");
info!(target: "app", "启动快捷方式不存在,无需删除");
return Ok(());
}
// Delete the shortcut
fs::remove_file(&shortcut_path).map_err(|e| anyhow!("Failed to delete shortcut: {}", e))?;
// 删除快捷方式
fs::remove_file(&shortcut_path).map_err(|e| anyhow!("删除快捷方式失败: {}", e))?;
info!(target: "app", "Successfully removed startup shortcut");
info!(target: "app", "成功删除启动快捷方式");
Ok(())
}
@@ -96,7 +96,7 @@ pub fn remove_shortcut() -> Result<()> {
#[cfg(target_os = "windows")]
pub fn is_shortcut_enabled() -> Result<bool> {
let startup_dir = get_startup_dir()?;
let shortcut_path = startup_dir.join("Koala-Clash.lnk");
let shortcut_path = startup_dir.join("Clash-Verge.lnk");
Ok(shortcut_path.exists())
}

View File

@@ -5,14 +5,14 @@ use std::{fs, path::PathBuf};
use tauri::Manager;
#[cfg(not(feature = "verge-dev"))]
pub static APP_ID: &str = "io.github.koala-clash";
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev";
#[cfg(not(feature = "verge-dev"))]
pub static BACKUP_DIR: &str = "io.github.koala-clash-backup";
pub static BACKUP_DIR: &str = "clash-verge-rev-backup";
#[cfg(feature = "verge-dev")]
pub static APP_ID: &str = "io.github.koala-clash.dev";
pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.dev";
#[cfg(feature = "verge-dev")]
pub static BACKUP_DIR: &str = "io.github.koala-clash-backup-dev";
pub static BACKUP_DIR: &str = "clash-verge-rev-backup-dev";
pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new();
@@ -188,13 +188,13 @@ pub fn profiles_path() -> Result<PathBuf> {
#[cfg(target_os = "macos")]
pub fn service_path() -> Result<PathBuf> {
let res_dir = app_resources_dir()?;
Ok(res_dir.join("koala-clash-service"))
Ok(res_dir.join("clash-verge-service"))
}
#[cfg(windows)]
pub fn service_path() -> Result<PathBuf> {
let res_dir = app_resources_dir()?;
Ok(res_dir.join("koala-clash-service.exe"))
Ok(res_dir.join("clash-verge-service.exe"))
}
pub fn service_log_file() -> Result<PathBuf> {

View File

@@ -178,8 +178,9 @@ fn init_dns_config() -> Result<()> {
"default-nameserver".into(),
Value::Sequence(vec![
Value::String("system".into()),
Value::String("223.6.6.6".into()),
Value::String("8.8.8.8".into()),
Value::String("1.1.1.1".into()),
Value::String("2400:3200::1".into()),
Value::String("2001:4860:4860::8888".into()),
]),
),
@@ -188,8 +189,7 @@ fn init_dns_config() -> Result<()> {
Value::Sequence(vec![
Value::String("8.8.8.8".into()),
Value::String("https://doh.pub/dns-query".into()),
Value::String("https://dns.google/dns-query".into()),
Value::String("https://cloudflare-dns.com/dns-query".into()),
Value::String("https://dns.alidns.com/dns-query".into()),
]),
),
("fallback".into(), Value::Sequence(vec![])),
@@ -201,9 +201,8 @@ fn init_dns_config() -> Result<()> {
"proxy-server-nameserver".into(),
Value::Sequence(vec![
Value::String("https://doh.pub/dns-query".into()),
Value::String("https://dns.google/dns-query".into()),
Value::String("https://cloudflare-dns.com/dns-query".into()),
Value::String("tls://1.1.1.1".into()),
Value::String("https://dns.alidns.com/dns-query".into()),
Value::String("tls://223.5.5.5".into()),
]),
),
("direct-nameserver".into(), Value::Sequence(vec![])),
@@ -247,7 +246,7 @@ fn init_dns_config() -> Result<()> {
help::save_yaml(
&dns_path,
&default_dns_config,
Some("# Koala Clash DNS Config"),
Some("# Clash Verge DNS Config"),
)?;
}
@@ -275,14 +274,14 @@ pub fn init_config() -> Result<()> {
crate::log_err!(dirs::clash_path().map(|path| {
if !path.exists() {
help::save_yaml(&path, &IClashTemp::template().0, Some("# Koala Clash"))?;
help::save_yaml(&path, &IClashTemp::template().0, Some("# Clash Vergeasu"))?;
}
<Result<()>>::Ok(())
}));
crate::log_err!(dirs::verge_path().map(|path| {
if !path.exists() {
help::save_yaml(&path, &IVerge::template(), Some("# Koala Clash"))?;
help::save_yaml(&path, &IVerge::template(), Some("# Clash Verge"))?;
}
<Result<()>>::Ok(())
}));
@@ -292,7 +291,7 @@ pub fn init_config() -> Result<()> {
crate::log_err!(dirs::profiles_path().map(|path| {
if !path.exists() {
help::save_yaml(&path, &IProfiles::template(), Some("# Koala Clash"))?;
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?;
}
<Result<()>>::Ok(())
}));
@@ -372,8 +371,8 @@ pub fn init_scheme() -> Result<()> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let (clash, _) = hkcu.create_subkey("Software\\Classes\\Clash")?;
clash.set_value("", &"Koala Clash")?;
clash.set_value("URL Protocol", &"Koala Clash URL Scheme Protocol")?;
clash.set_value("", &"Clash Verge")?;
clash.set_value("URL Protocol", &"Clash Verge URL Scheme Protocol")?;
let (default_icon, _) = hkcu.create_subkey("Software\\Classes\\Clash\\DefaultIcon")?;
default_icon.set_value("", &app_exe)?;
let (command, _) = hkcu.create_subkey("Software\\Classes\\Clash\\Shell\\Open\\Command")?;
@@ -385,7 +384,7 @@ pub fn init_scheme() -> Result<()> {
pub fn init_scheme() -> Result<()> {
let output = std::process::Command::new("xdg-mime")
.arg("default")
.arg("koala-clash.desktop")
.arg("clash-verge.desktop")
.arg("x-scheme-handler/clash")
.output()?;
if !output.status.success() {
@@ -398,33 +397,6 @@ pub fn init_scheme() -> Result<()> {
}
#[cfg(target_os = "macos")]
pub fn init_scheme() -> Result<()> {
use std::process::Command;
use tauri::utils::platform::current_exe;
// Try to re-register the app bundle with LaunchServices to ensure URL schemes are active
if let Ok(exe) = current_exe() {
if let (Some(_parent1), Some(_parent2), Some(app_bundle)) =
(exe.parent(), exe.parent().and_then(|p| p.parent()), exe.parent().and_then(|p| p.parent()).and_then(|p| p.parent()))
{
let app_bundle_path = app_bundle.to_string_lossy().into_owned();
let lsregister = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
let output = Command::new(lsregister)
.args(["-f", "-R", &app_bundle_path])
.output();
match output {
Ok(out) => {
if !out.status.success() {
log::warn!(target: "app", "lsregister returned non-zero: {:?}", out.status);
} else {
log::info!(target: "app", "Re-registered URL schemes with LaunchServices");
}
}
Err(e) => {
log::warn!(target: "app", "Failed to run lsregister: {e}");
}
}
}
}
Ok(())
}

View File

@@ -8,6 +8,6 @@ pub mod network;
pub mod notification;
pub mod resolve;
pub mod server;
pub mod sys_info;
pub mod tmpl;
pub mod window_manager;
pub mod sys_info;

View File

@@ -40,7 +40,7 @@ impl NetworkManager {
// 创建专用的异步运行时线程数限制为4个
let runtime = Builder::new_multi_thread()
.worker_threads(4)
.thread_name("koala-clash-network")
.thread_name("clash-verge-network")
.enable_io()
.enable_time()
.build()
@@ -65,7 +65,7 @@ impl NetworkManager {
pub fn init(&self) {
self.init.call_once(|| {
self.runtime.spawn(async {
logging!(info, Type::Network, true, "Initializing network manager");
logging!(info, Type::Network, true, "初始化网络管理器");
// 创建无代理客户端
let no_proxy_client = ClientBuilder::new()
@@ -81,7 +81,7 @@ impl NetworkManager {
let mut no_proxy_guard = NETWORK_MANAGER.no_proxy_client.lock().unwrap();
*no_proxy_guard = Some(no_proxy_client);
logging!(info, Type::Network, true, "Network manager initialization completed");
logging!(info, Type::Network, true, "网络管理器初始化完成");
});
});
}
@@ -112,7 +112,7 @@ impl NetworkManager {
}
pub fn reset_clients(&self) {
logging!(info, Type::Network, true, "Resetting all HTTP clients");
logging!(info, Type::Network, true, "正在重置所有HTTP客户端");
{
let mut client = self.self_proxy_client.lock().unwrap();
*client = None;
@@ -323,8 +323,8 @@ impl NetworkManager {
use crate::utils::resolve::VERSION;
let version = match VERSION.get() {
Some(v) => format!("koala-clash/v{v}"),
None => "koala-clash/unknown".to_string(),
Some(v) => format!("clash-verge/v{v}"),
None => "clash-verge/unknown".to_string(),
};
builder = builder.user_agent(version);
@@ -409,7 +409,7 @@ impl NetworkManager {
let watchdog = tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(timeout_duration)).await;
let _ = cancel_tx.send(());
logging!(warn, Type::Network, true, "Request canceled due to timeout: {}", url_clone);
logging!(warn, Type::Network, true, "请求超时取消: {}", url_clone);
});
let result = tokio::select! {

View File

@@ -3,11 +3,10 @@ use crate::AppHandleManager;
use crate::{
config::{Config, IVerge, PrfItem},
core::*,
core::handle::Handle,
logging, logging_error,
module::lightweight::{self, auto_lightweight_mode_init},
process::AsyncHandler,
utils::{init, logging::Type, server, window_manager::WindowManager},
utils::{init, logging::Type, server},
wrap_err,
};
use anyhow::{bail, Result};
@@ -24,6 +23,7 @@ use tauri::{AppHandle, Manager};
use tokio::net::TcpListener;
use tauri::Url;
use crate::config::PrfOption;
//#[cfg(not(target_os = "linux"))]
// use window_shadows::set_shadow;
@@ -66,35 +66,6 @@ impl Default for UiReadyState {
// 获取UI就绪状态细节
static UI_READY_STATE: OnceCell<Arc<UiReadyState>> = OnceCell::new();
// Early deep link capture on cold start
static EARLY_DEEP_LINK: OnceCell<Mutex<Option<String>>> = OnceCell::new();
// Deduplication for deep links to avoid processing same URL twice in short time
static LAST_DEEP_LINK: OnceCell<Mutex<Option<(String, Instant)>>> = OnceCell::new();
fn get_early_deep_link() -> &'static Mutex<Option<String>> {
EARLY_DEEP_LINK.get_or_init(|| Mutex::new(None))
}
/// Capture deep link from process arguments as early as possible (cold start on macOS)
pub fn capture_early_deep_link_from_args() {
let args: Vec<String> = std::env::args().collect();
if let Some(url) = args.iter().find(|a| a.starts_with("clash://") || a.starts_with("koala-clash://")).cloned() {
println!("[DeepLink][argv] {}", url);
logging!(info, Type::Setup, true, "argv captured deep link: {}", url);
*get_early_deep_link().lock() = Some(url);
} else {
println!("[DeepLink][argv] none: {:?}", args);
logging!(info, Type::Setup, true, "no deep link found in argv at startup: {:?}", args);
}
}
/// If an early deep link was captured before setup, schedule it now
pub fn replay_early_deep_link() {
if let Some(url) = get_early_deep_link().lock().take() {
schedule_handle_deep_link(url);
}
}
fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> {
WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now())))
}
@@ -103,11 +74,6 @@ fn get_ui_ready() -> &'static Arc<RwLock<bool>> {
UI_READY.get_or_init(|| Arc::new(RwLock::new(false)))
}
/// Check whether the UI has finished initialization on the frontend side
pub fn is_ui_ready() -> bool {
*get_ui_ready().read()
}
fn get_ui_ready_state() -> &'static Arc<UiReadyState> {
UI_READY_STATE.get_or_init(|| Arc::new(UiReadyState::default()))
}
@@ -128,10 +94,7 @@ pub fn update_ui_ready_stage(stage: UiReadyStage) {
pub fn mark_ui_ready() {
let mut ready = get_ui_ready().write();
*ready = true;
logging!(info, Type::Window, true, "UI marked as fully ready");
// If any deep links were queued while UI was not ready, handle them now
// No queued deep links list anymore; early and runtime deep links are deduped
logging!(info, Type::Window, true, "UI已标记为完全就绪");
}
// 重置UI就绪状态
@@ -145,83 +108,7 @@ pub fn reset_ui_ready() {
let mut stage = state.stage.write();
*stage = UiReadyStage::NotStarted;
}
logging!(info, Type::Window, true, "UI readiness state has been reset");
}
/// Schedule robust deep-link handling to avoid races with lightweight mode and window creation
pub fn schedule_handle_deep_link(url: String) {
AsyncHandler::spawn(move || async move {
// Normalize dedup key to the actual subscription URL inside the deep link
let dedup_key = (|| {
if let Ok(parsed) = Url::parse(&url) {
for (k, v) in parsed.query_pairs() {
if k == "url" {
return percent_decode_str(&v).decode_utf8_lossy().to_string();
}
}
}
url.clone()
})();
// Deduplicate: if the same deep/subscription link was handled very recently, skip
{
let now = Instant::now();
let mut last = LAST_DEEP_LINK.get_or_init(|| Mutex::new(None)).lock();
if let Some((prev_url, prev_time)) = last.as_ref() {
if *prev_url == dedup_key && now.duration_since(*prev_time) < Duration::from_secs(5) {
log::warn!(target: "app", "Skip duplicate deep link within 5s: {}", dedup_key);
return;
}
}
*last = Some((dedup_key.clone(), now));
}
// Wait until app handle exists
for i in 0..100u8 {
if Handle::global().app_handle().is_some() {
break;
}
if i % 10 == 0 { logging!(info, Type::Setup, true, "waiting for app handle... ({}ms)", i as u64 * 20); }
tokio::time::sleep(Duration::from_millis(20)).await;
}
// Ensure we are not in lightweight mode (webview destroyed)
lightweight::exit_lightweight_mode();
for _ in 0..150u16 {
if !lightweight::is_in_lightweight_mode() {
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
// Ensure a window exists ASAP so UI can mount
#[cfg(target_os = "macos")]
{
AppHandleManager::global().set_activation_policy_regular();
}
// If lightweight mode was active, give it a bit of time to unwind before recreating window
if lightweight::is_in_lightweight_mode() {
tokio::time::sleep(Duration::from_millis(200)).await;
}
let _ = WindowManager::show_main_window();
// Ensure profiles directory exists on cold start
if let Ok(dir) = crate::utils::dirs::app_profiles_dir() {
if !dir.exists() {
let _ = std::fs::create_dir_all(&dir);
}
}
// Process deep link (add profile regardless of UI state)
logging!(info, Type::Setup, true, "processing deep link: {}", dedup_key);
if let Err(e) = resolve_scheme(url.clone()).await {
log::error!(target: "app", "Deep link handling failed: {e}");
}
// If UI is ready, small delay to let listeners settle before finishing
if is_ui_ready() {
tokio::time::sleep(Duration::from_millis(120)).await;
}
});
logging!(info, Type::Window, true, "UI就绪状态已重置");
}
pub async fn find_unused_port() -> Result<u16> {
@@ -244,12 +131,12 @@ pub async fn find_unused_port() -> Result<u16> {
/// 异步方式处理启动后的额外任务
pub async fn resolve_setup_async(app_handle: &AppHandle) {
let start_time = std::time::Instant::now();
logging!(info, Type::Setup, true, "Starting asynchronous setup tasks...");
logging!(info, Type::Setup, true, "开始执行异步设置任务...");
if VERSION.get().is_none() {
let version = app_handle.package_info().version.to_string();
VERSION.get_or_init(|| {
logging!(info, Type::Setup, true, "Initializing version information: {}", version);
logging!(info, Type::Setup, true, "初始化版本信息: {}", version);
version.clone()
});
}
@@ -268,40 +155,40 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
);
}
logging!(trace, Type::Config, true, "Initializing configuration...");
logging!(trace, Type::Config, true, "初始化配置...");
logging_error!(Type::Config, true, Config::init_config().await);
// 启动时清理冗余的 Profile 文件
logging!(info, Type::Setup, true, "Cleaning redundant profile files...");
logging!(info, Type::Setup, true, "清理冗余的Profile文件...");
let profiles = Config::profiles();
if let Err(e) = profiles.latest().auto_cleanup() {
logging!(warn, Type::Setup, true, "Failed to clean profile files at startup: {}", e);
logging!(warn, Type::Setup, true, "启动时清理Profile文件失败: {}", e);
} else {
logging!(info, Type::Setup, true, "Startup profile files cleanup completed");
logging!(info, Type::Setup, true, "启动时Profile文件清理完成");
}
logging!(trace, Type::Core, true, "Starting core manager...");
logging!(trace, Type::Core, true, "启动核心管理器...");
logging_error!(Type::Core, true, CoreManager::global().init().await);
log::trace!(target: "app", "Starting embedded server...");
log::trace!(target: "app", "启动内嵌服务器...");
server::embed_server();
logging_error!(Type::Tray, true, tray::Tray::global().init());
if let Some(app_handle) = handle::Handle::global().app_handle() {
logging!(info, Type::Tray, true, "Creating system tray...");
logging!(info, Type::Tray, true, "创建系统托盘...");
let result = tray::Tray::global().create_tray_from_handle(&app_handle);
if result.is_ok() {
logging!(info, Type::Tray, true, "System tray created successfully");
logging!(info, Type::Tray, true, "系统托盘创建成功");
} else if let Err(e) = result {
logging!(error, Type::Tray, true, "Failed to create system tray: {}", e);
logging!(error, Type::Tray, true, "系统托盘创建失败: {}", e);
}
} else {
logging!(
error,
Type::Tray,
true,
"Unable to create system tray: app_handle missing"
"无法创建系统托盘: app_handle不存在"
);
}
@@ -337,7 +224,7 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
logging_error!(Type::Tray, true, tray::Tray::global().update_part());
logging!(trace, Type::System, true, "Initializing hotkeys...");
logging!(trace, Type::System, true, "初始化热键...");
logging_error!(Type::System, true, hotkey::Hotkey::global().init());
let elapsed = start_time.elapsed();
@@ -345,7 +232,7 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
info,
Type::Setup,
true,
"Asynchronous task completed, time taken: {:?}",
"异步设置任务完成,耗时: {:?}",
elapsed
);
@@ -355,7 +242,7 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
warn,
Type::Setup,
true,
"Asynchronous task setup takes a long time ({:?})",
"异步设置任务耗时较长({:?})",
elapsed
);
}
@@ -387,12 +274,12 @@ pub fn create_window(is_show: bool) -> bool {
info,
Type::Window,
true,
"Creating/showing main window, is_show={}",
"开始创建/显示主窗口, is_show={}",
is_show
);
if !is_show {
logging!(info, Type::Window, true, "Silent start: do not create window");
logging!(info, Type::Window, true, "静默模式启动时不创建窗口");
lightweight::set_lightweight_mode(true);
handle::Handle::notify_startup_completed();
return false;
@@ -400,34 +287,21 @@ pub fn create_window(is_show: bool) -> bool {
if let Some(app_handle) = handle::Handle::global().app_handle() {
if let Some(window) = app_handle.get_webview_window("main") {
logging!(info, Type::Window, true, "Main window already exists; will try to show it");
logging!(info, Type::Window, true, "主窗口已存在,将显示现有窗口");
if is_show {
if window.is_minimized().unwrap_or(false) {
logging!(info, Type::Window, true, "Window is minimized; unminimizing");
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
let _ = window.unminimize();
}
let show_result = window.show();
let focus_result = window.set_focus();
let _ = window.show();
let _ = window.set_focus();
// If showing or focusing fails (possibly destroyed webview after lightweight), fallback to recreate
if show_result.is_err() || focus_result.is_err() {
logging!(
warn,
Type::Window,
true,
"Failed to show existing window; will destroy and recreate"
);
let _ = window.destroy();
} else {
#[cfg(target_os = "macos")]
{
AppHandleManager::global().set_activation_policy_regular();
}
return true;
#[cfg(target_os = "macos")]
{
AppHandleManager::global().set_activation_policy_regular();
}
} else {
return true;
}
return true;
}
}
@@ -442,7 +316,7 @@ pub fn create_window(is_show: bool) -> bool {
info,
Type::Window,
true,
"Window creation request ignored because recently created ({:?}ms)",
"窗口创建请求被忽略,因为最近创建过 ({:?}ms)",
elapsed.as_millis()
);
return false;
@@ -453,7 +327,7 @@ pub fn create_window(is_show: bool) -> bool {
// ScopeGuard 确保创建状态重置,防止 webview 卡死
let _guard = scopeguard::guard(creating, |mut creating_guard| {
*creating_guard = (false, Instant::now());
logging!(debug, Type::Window, true, "[ScopeGuard] Window creation state reset");
logging!(debug, Type::Window, true, "[ScopeGuard] 窗口创建状态已重置");
});
match tauri::WebviewWindowBuilder::new(
@@ -470,16 +344,16 @@ pub fn create_window(is_show: bool) -> bool {
.visible(true) // 立即显示窗口,避免用户等待
.initialization_script(
r#"
console.log('[Tauri] Window init script started');
console.log('[Tauri] 窗口初始化脚本开始执行');
function createLoadingOverlay() {
if (document.getElementById('initial-loading-overlay')) {
console.log('[Tauri] Loading indicator already exists');
console.log('[Tauri] 加载指示器已存在');
return;
}
console.log('[Tauri] Creating loading indicator');
console.log('[Tauri] 创建加载指示器');
const loadingDiv = document.createElement('div');
loadingDiv.id = 'initial-loading-overlay';
loadingDiv.innerHTML = `
@@ -498,7 +372,7 @@ pub fn create_window(is_show: bool) -> bool {
animation: spin 1s linear infinite;
"></div>
</div>
<div style="font-size: 14px; opacity: 0.7;">Loading Koala Clash...</div>
<div style="font-size: 14px; opacity: 0.7;">Loading Clash Verge...</div>
</div>
<style>
@keyframes spin {
@@ -530,13 +404,13 @@ pub fn create_window(is_show: bool) -> bool {
createLoadingOverlay();
}
console.log('[Tauri] Window init script finished');
console.log('[Tauri] 窗口初始化脚本执行完成');
"#,
)
.build()
{
Ok(newly_created_window) => {
logging!(debug, Type::Window, true, "Main window instance created successfully");
logging!(debug, Type::Window, true, "主窗口实例创建成功");
update_ui_ready_stage(UiReadyStage::NotStarted);
@@ -546,7 +420,7 @@ pub fn create_window(is_show: bool) -> bool {
debug,
Type::Window,
true,
"Async window task started (startup marked completed)"
"异步窗口任务开始 (启动已标记完成)"
);
// 先运行轻量模式检测
@@ -557,7 +431,7 @@ pub fn create_window(is_show: bool) -> bool {
debug,
Type::Window,
true,
"Sending verge://startup-completed event"
"发送 verge://startup-completed 事件"
);
handle::Handle::notify_startup_completed();
@@ -567,7 +441,7 @@ pub fn create_window(is_show: bool) -> bool {
// 立即显示窗口
let _ = window_clone.show();
let _ = window_clone.set_focus();
logging!(info, Type::Window, true, "Window shown immediately");
logging!(info, Type::Window, true, "窗口已立即显示");
#[cfg(target_os = "macos")]
{
AppHandleManager::global().set_activation_policy_regular();
@@ -583,7 +457,7 @@ pub fn create_window(is_show: bool) -> bool {
info,
Type::Window,
true,
"Start monitoring UI load status (up to {} seconds)...",
"开始监控UI加载状态 (最多{}秒)...",
timeout_seconds
);
@@ -602,7 +476,7 @@ pub fn create_window(is_show: bool) -> bool {
debug,
Type::Window,
true,
"UI loading status check... ({}s)",
"UI加载状态检查... ({})",
check_count / 10
);
}
@@ -612,7 +486,7 @@ pub fn create_window(is_show: bool) -> bool {
match wait_result {
Ok(_) => {
logging!(info, Type::Window, true, "UI fully loaded and ready");
logging!(info, Type::Window, true, "UI已完全加载就绪");
// 移除初始加载指示器
if let Some(window) = handle::Handle::global().get_window() {
let _ = window.eval(r#"
@@ -629,7 +503,7 @@ pub fn create_window(is_show: bool) -> bool {
warn,
Type::Window,
true,
"UI load monitoring timed out ({}s), but window is already visible",
"UI加载监控超时({}秒),但窗口已正常显示",
timeout_seconds
);
*get_ui_ready().write() = true;
@@ -637,20 +511,20 @@ pub fn create_window(is_show: bool) -> bool {
}
});
logging!(info, Type::Window, true, "Window display flow completed");
logging!(info, Type::Window, true, "窗口显示流程完成");
} else {
logging!(
debug,
Type::Window,
true,
"is_show is false; keeping window hidden"
"is_showfalse,窗口保持隐藏状态"
);
}
});
true
}
Err(e) => {
logging!(error, Type::Window, true, "Failed to build main window: {}", e);
logging!(error, Type::Window, true, "主窗口构建失败: {}", e);
false
}
}
@@ -675,16 +549,14 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
}
};
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "koala-clash" {
if link_parsed.scheme() == "clash" || link_parsed.scheme() == "clash-verge" {
let mut name: Option<String> = None;
let mut url_param: Option<String> = None;
for (key, value) in link_parsed.query_pairs() {
match key.as_ref() {
"name" => name = Some(value.into_owned()),
"url" => {
url_param = Some(percent_decode_str(&value).decode_utf8_lossy().to_string())
}
"url" => url_param = Some(percent_decode_str(&value).decode_utf8_lossy().to_string()),
_ => {}
}
}
@@ -693,12 +565,11 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
Some(url) => {
log::info!(target:"app", "decoded subscription url: {url}");
// Deep link inside resolver is now executed via schedule_handle_deep_link
create_window(true);
match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => {
let uid = item.uid.clone().unwrap();
let _ = wrap_err!(Config::profiles().data().append_item(item));
// If UI not ready yet, message will be queued and flushed on ready
handle::Handle::notice_message("import_sub_url::ok", uid);
}
Err(e) => {

View File

@@ -7,7 +7,8 @@ use crate::{
process::AsyncHandler,
utils::logging::Type,
};
use anyhow::Result;
use anyhow::{bail, Result};
use port_scanner::local_port_available;
use std::convert::Infallible;
use warp::Filter;
@@ -16,6 +17,32 @@ struct QueryParam {
param: String,
}
/// check whether there is already exists
pub async fn check_singleton() -> Result<()> {
let port = IVerge::get_singleton_port();
if !local_port_available(port) {
let argvs: Vec<String> = std::env::args().collect();
if argvs.len() > 1 {
#[cfg(not(target_os = "macos"))]
{
let param = argvs[1].as_str();
if param.starts_with("clash:") {
let _ = reqwest::get(format!(
"http://127.0.0.1:{port}/commands/scheme?param={param}"
))
.await;
}
}
} else {
let _ = reqwest::get(format!("http://127.0.0.1:{port}/commands/visible")).await;
}
log::error!("failed to setup singleton listen server");
bail!("app exists");
} else {
Ok(())
}
}
/// The embed server only be used to implement singleton process
/// maybe it can be used as pac server later
pub fn embed_server() {

View File

@@ -1,7 +1,7 @@
//! Some config file template
/// template for new a profile item
pub const ITEM_LOCAL: &str = "# Profile Template for Koala Clash
pub const ITEM_LOCAL: &str = "# Profile Template for Clash Verge
proxies: []
@@ -11,13 +11,13 @@ rules: []
";
/// enhanced profile
pub const ITEM_MERGE: &str = "# Profile Enhancement Merge Template for Koala Clash
pub const ITEM_MERGE: &str = "# Profile Enhancement Merge Template for Clash Verge
profile:
store-selected: true
";
pub const ITEM_MERGE_EMPTY: &str = "# Profile Enhancement Merge Template for Koala Clash
pub const ITEM_MERGE_EMPTY: &str = "# Profile Enhancement Merge Template for Clash Verge
";
@@ -30,7 +30,7 @@ function main(config, profileName) {
";
/// enhanced profile
pub const ITEM_RULES: &str = "# Profile Enhancement Rules Template for Koala Clash
pub const ITEM_RULES: &str = "# Profile Enhancement Rules Template for Clash Verge
prepend: []
@@ -40,7 +40,7 @@ delete: []
";
/// enhanced profile
pub const ITEM_PROXIES: &str = "# Profile Enhancement Proxies Template for Koala Clash
pub const ITEM_PROXIES: &str = "# Profile Enhancement Proxies Template for Clash Verge
prepend: []
@@ -50,7 +50,7 @@ delete: []
";
/// enhanced profile
pub const ITEM_GROUPS: &str = "# Profile Enhancement Groups Template for Koala Clash
pub const ITEM_GROUPS: &str = "# Profile Enhancement Groups Template for Clash Verge
prepend: []

View File

@@ -53,7 +53,7 @@ fn get_window_operation_debounce() -> &'static Mutex<Instant> {
fn should_handle_window_operation() -> bool {
if WINDOW_OPERATION_IN_PROGRESS.load(Ordering::Acquire) {
log::warn!(target: "app", "[debounce] Window operation already in progress, skipping duplicate call");
log::warn!(target: "app", "[防抖] 窗口操作已在进行中,跳过重复调用");
return false;
}
@@ -62,16 +62,16 @@ fn should_handle_window_operation() -> bool {
let now = Instant::now();
let elapsed = now.duration_since(*last_operation);
log::debug!(target: "app", "[debounce] Checking window operation interval: {}ms (need >={}ms)",
log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)",
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) {
*last_operation = now;
WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release);
log::info!(target: "app", "[debounce] Window operation allowed to execute");
log::info!(target: "app", "[防抖] 窗口操作被允许执行");
true
} else {
log::warn!(target: "app", "[debounce] Window operation ignored by debounce: {}ms since last < {}ms",
log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms",
elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
false
}
@@ -127,7 +127,7 @@ impl WindowManager {
finish_window_operation();
});
logging!(info, Type::Window, true, "Starting smart show for main window");
logging!(info, Type::Window, true, "开始智能显示主窗口");
logging!(
debug,
Type::Window,
@@ -140,18 +140,18 @@ impl WindowManager {
match current_state {
WindowState::NotExist => {
logging!(info, Type::Window, true, "Main window not found; creating new window");
logging!(info, Type::Window, true, "窗口不存在,创建新窗口");
if Self::create_new_window() {
logging!(info, Type::Window, true, "Window created successfully");
logging!(info, Type::Window, true, "窗口创建成功");
std::thread::sleep(std::time::Duration::from_millis(100));
WindowOperationResult::Created
} else {
logging!(warn, Type::Window, true, "Window creation failed");
logging!(warn, Type::Window, true, "窗口创建失败");
WindowOperationResult::Failed
}
}
WindowState::VisibleFocused => {
logging!(info, Type::Window, true, "Window already visible and focused; no action needed");
logging!(info, Type::Window, true, "窗口已经可见且有焦点,无需操作");
WindowOperationResult::NoAction
}
WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => {
@@ -184,14 +184,14 @@ impl WindowManager {
finish_window_operation();
});
logging!(info, Type::Window, true, "Toggling main window visibility");
logging!(info, Type::Window, true, "开始切换主窗口显示状态");
let current_state = Self::get_main_window_state();
logging!(
info,
Type::Window,
true,
"Current window state: {:?} | Details: {}",
"当前窗口状态: {:?} | 详细状态: {}",
current_state,
Self::get_window_status_info()
);
@@ -199,7 +199,7 @@ impl WindowManager {
match current_state {
WindowState::NotExist => {
// 窗口不存在,创建新窗口
logging!(info, Type::Window, true, "Main window not found; will create new window");
logging!(info, Type::Window, true, "窗口不存在,将创建新窗口");
// 由于已经有防抖保护,直接调用内部方法
if Self::create_new_window() {
WindowOperationResult::Created
@@ -212,26 +212,26 @@ impl WindowManager {
info,
Type::Window,
true,
"Window visible (focused: {}), hiding window",
"窗口可见(焦点状态: {}),将隐藏窗口",
if current_state == WindowState::VisibleFocused {
"focused"
"有焦点"
} else {
"unfocused"
"无焦点"
}
);
if let Some(window) = Self::get_main_window() {
match window.hide() {
Ok(_) => {
logging!(info, Type::Window, true, "Window hidden successfully");
logging!(info, Type::Window, true, "窗口已成功隐藏");
WindowOperationResult::Hidden
}
Err(e) => {
logging!(warn, Type::Window, true, "Failed to hide window: {}", e);
logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e);
WindowOperationResult::Failed
}
}
} else {
logging!(warn, Type::Window, true, "Unable to get window instance");
logging!(warn, Type::Window, true, "无法获取窗口实例");
WindowOperationResult::Failed
}
}
@@ -240,12 +240,12 @@ impl WindowManager {
info,
Type::Window,
true,
"Window exists but is hidden or minimized; activating"
"窗口存在但被隐藏或最小化,将激活窗口"
);
if let Some(window) = Self::get_main_window() {
Self::activate_window(&window)
} else {
logging!(warn, Type::Window, true, "Unable to get window instance");
logging!(warn, Type::Window, true, "无法获取窗口实例");
WindowOperationResult::Failed
}
}
@@ -254,35 +254,35 @@ impl WindowManager {
/// 激活窗口(取消最小化、显示、设置焦点)
fn activate_window(window: &WebviewWindow<Wry>) -> WindowOperationResult {
logging!(info, Type::Window, true, "Starting to activate window");
logging!(info, Type::Window, true, "开始激活窗口");
let mut operations_successful = true;
// 1. 如果窗口最小化,先取消最小化
if window.is_minimized().unwrap_or(false) {
logging!(info, Type::Window, true, "Window minimized; unminimizing");
logging!(info, Type::Window, true, "窗口已最小化,正在取消最小化");
if let Err(e) = window.unminimize() {
logging!(warn, Type::Window, true, "Failed to unminimize window: {}", e);
logging!(warn, Type::Window, true, "取消最小化失败: {}", e);
operations_successful = false;
}
}
// 2. 显示窗口
if let Err(e) = window.show() {
logging!(warn, Type::Window, true, "Failed to show window: {}", e);
logging!(warn, Type::Window, true, "显示窗口失败: {}", e);
operations_successful = false;
}
// 3. 设置焦点
if let Err(e) = window.set_focus() {
logging!(warn, Type::Window, true, "Failed to set window focus: {}", e);
logging!(warn, Type::Window, true, "设置窗口焦点失败: {}", e);
operations_successful = false;
}
// 4. 平台特定的激活策略
#[cfg(target_os = "macos")]
{
logging!(info, Type::Window, true, "Applying macOS-specific activation policy");
logging!(info, Type::Window, true, "应用 macOS 特定的激活策略");
AppHandleManager::global().set_activation_policy_regular();
}
@@ -294,7 +294,7 @@ impl WindowManager {
debug,
Type::Window,
true,
"Failed to set always-on-top (non-critical): {}",
"设置置顶失败(非关键错误): {}",
e
);
}
@@ -304,38 +304,38 @@ impl WindowManager {
debug,
Type::Window,
true,
"Failed to unset always-on-top (non-critical): {}",
"取消置顶失败(非关键错误): {}",
e
);
}
}
if operations_successful {
logging!(info, Type::Window, true, "Window activation successful");
logging!(info, Type::Window, true, "窗口激活成功");
WindowOperationResult::Shown
} else {
logging!(warn, Type::Window, true, "Window activation partially failed");
logging!(warn, Type::Window, true, "窗口激活部分失败");
WindowOperationResult::Failed
}
}
/// 隐藏主窗口
pub fn hide_main_window() -> WindowOperationResult {
logging!(info, Type::Window, true, "Starting to hide main window");
logging!(info, Type::Window, true, "开始隐藏主窗口");
match Self::get_main_window() {
Some(window) => match window.hide() {
Ok(_) => {
logging!(info, Type::Window, true, "Window hidden");
logging!(info, Type::Window, true, "窗口已隐藏");
WindowOperationResult::Hidden
}
Err(e) => {
logging!(warn, Type::Window, true, "Failed to hide window: {}", e);
logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e);
WindowOperationResult::Failed
}
},
None => {
logging!(info, Type::Window, true, "Window does not exist; nothing to hide");
logging!(info, Type::Window, true, "窗口不存在,无需隐藏");
WindowOperationResult::NoAction
}
}
@@ -376,7 +376,7 @@ impl WindowManager {
let is_minimized = Self::is_main_window_minimized();
format!(
"WindowState: {state:?} | visible: {is_visible} | focused: {is_focused} | minimized: {is_minimized}"
"窗口状态: {state:?} | 可见: {is_visible} | 有焦点: {is_focused} | 最小化: {is_minimized}"
)
}
}

View File

@@ -1,5 +1,5 @@
{
"version": "0.2.6",
"version": "0.2.4",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": {
"active": true,
@@ -13,7 +13,7 @@
],
"resources": ["resources", "resources/locales/*"],
"publisher": "Koala Clash",
"externalBin": ["sidecar/koala-mihomo", "sidecar/koala-mihomo-alpha"],
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
"copyright": "GNU General Public License v3.0",
"category": "DeveloperTool",
"shortDescription": "Koala Clash",
@@ -26,7 +26,7 @@
"devUrl": "http://localhost:3000/"
},
"productName": "Koala Clash",
"identifier": "io.github.koala-clash",
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERCQjQ1QjQ0QUJDQTU1RTkKUldUcFZjcXJSRnUwMjdXSERoZVQ1R0hHRDMrT3VkSmpvbDJmb01sN3ZpYWhVYnEwaWpYUWU4YU0K",
@@ -40,7 +40,7 @@
},
"deep-link": {
"desktop": {
"schemes": ["clash", "koala-clash"]
"schemes": ["clash", "clash-verge"]
}
}
},

View File

@@ -1,34 +1,34 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"identifier": "io.github.koala-clash",
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"bundle": {
"targets": ["deb", "rpm"],
"linux": {
"deb": {
"depends": ["openssl"],
"desktopTemplate": "./packages/linux/koala-clash.desktop",
"provides": ["koala-clash"],
"conflicts": ["koala-clash"],
"replaces": ["koala-clash"],
"desktopTemplate": "./packages/linux/clash-verge.desktop",
"provides": ["clash-verge"],
"conflicts": ["clash-verge"],
"replaces": ["clash-verge"],
"postInstallScript": "./packages/linux/post-install.sh",
"preRemoveScript": "./packages/linux/pre-remove.sh"
},
"rpm": {
"depends": ["openssl"],
"desktopTemplate": "./packages/linux/koala-clash.desktop",
"provides": ["koala-clash"],
"conflicts": ["koala-clash"],
"obsoletes": ["koala-clash"],
"desktopTemplate": "./packages/linux/clash-verge.desktop",
"provides": ["clash-verge"],
"conflicts": ["clash-verge"],
"obsoletes": ["clash-verge"],
"postInstallScript": "./packages/linux/post-install.sh",
"preRemoveScript": "./packages/linux/pre-remove.sh"
}
},
"externalBin": [
"./resources/koala-clash-service",
"./resources/clash-verge-service",
"./resources/install-service",
"./resources/uninstall-service",
"./sidecar/koala-mihomo",
"./sidecar/koala-mihomo-alpha"
"./sidecar/verge-mihomo",
"./sidecar/verge-mihomo-alpha"
]
}
}

View File

@@ -1,6 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"identifier": "io.github.koala-clash",
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"productName": "Koala Clash",
"bundle": {
"targets": ["app", "dmg"],

View File

@@ -1,6 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"identifier": "io.github.koala-clash",
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"bundle": {
"targets": ["nsis"],
"windows": {

View File

@@ -1,6 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"identifier": "io.github.koala-clash",
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"bundle": {
"targets": ["nsis"],
"windows": {
@@ -31,3 +31,4 @@
}
}
}

View File

@@ -1,6 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"identifier": "io.github.koala-clash",
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"bundle": {
"targets": ["nsis"],
"windows": {

View File

@@ -1,6 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"identifier": "io.github.koala-clash",
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"bundle": {
"targets": ["nsis"],
"windows": {

View File

@@ -1,11 +1,14 @@
import { AppDataProvider } from "./providers/app-data-provider";
import { ThemeProvider } from "@/components/layout/theme-provider";
import Layout from "./pages/_layout";
function App() {
return (
<AppDataProvider>
<Layout />
</AppDataProvider>
<ThemeProvider>
<AppDataProvider>
<Layout />
</AppDataProvider>
</ThemeProvider>
);
}
export default App;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 576 KiB

View File

@@ -1,108 +1,50 @@
<svg width="1024" height="963" viewBox="0 0 1024 963" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_f_40_29)">
<ellipse cx="512" cy="516" rx="254" ry="216" fill="url(#paint0_radial_40_29)" fill-opacity="0.3"/>
<svg version="1.1" id="layout1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 117 27" style="enable-background:new 0 0 117 27;" xml:space="preserve">
<g>
<defs>
<rect id="SVGID_1_" x="-39.9" width="157" height="27"/>
</defs>
<clipPath id="SVGID_00000023248255305809236420000007367745325967865768_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_00000023248255305809236420000007367745325967865768_);">
<path class="st1" d="M115.9,21.4c-0.5,0.3-1.1,0.5-1.8,0.7c-0.7,0.1-1.3,0.2-1.9,0.2c-2.1,0-3.8-0.5-4.9-1.5
c-1.1-1-1.6-2.4-1.6-4.3c0-1.8,0.5-3.2,1.5-4.2c1-1,2.3-1.5,4-1.5c1.7,0,3,0.5,4,1.5c1,1,1.5,2.3,1.5,4.2c0,0.2,0,0.5,0,0.9h-7.8
c0.3,1.7,1.4,2.6,3.4,2.6c1.4,0,2.6-0.4,3.7-1.2V21.4z M113.6,15.2c-0.2-0.7-0.5-1.2-0.9-1.5c-0.4-0.3-0.9-0.5-1.5-0.5
c-0.6,0-1,0.2-1.4,0.5c-0.4,0.3-0.7,0.8-0.8,1.5H113.6z"/>
<path class="st1" d="M98.5,26.6c-0.8,0-1.6-0.1-2.5-0.2c-0.8-0.1-1.5-0.3-2.2-0.5v-2.6c1.4,0.3,2.9,0.5,4.3,0.5
c0.9,0,1.6-0.2,2.1-0.6c0.5-0.4,0.7-1,0.7-1.7c-0.7,0.5-1.6,0.7-2.6,0.7c-1,0-1.9-0.2-2.6-0.7c-0.7-0.5-1.3-1.1-1.7-2
c-0.4-0.9-0.6-1.8-0.6-2.9c0-1.1,0.2-2.1,0.6-2.9c0.4-0.9,1-1.5,1.7-2c0.7-0.5,1.6-0.7,2.6-0.7c0.9,0,1.8,0.3,2.6,0.9v-0.7h3.1V22
C104,25,102.2,26.6,98.5,26.6z M96.4,16.6c0,0.6,0.1,1.2,0.4,1.7c0.3,0.5,0.6,0.9,1,1.2c0.4,0.3,0.8,0.4,1.3,0.4
c0.3,0,0.7-0.1,1.1-0.2c0.4-0.2,0.8-0.5,1.1-1l0.1-0.4v-3.7c-0.3-0.6-0.6-0.9-1.1-1.1c-0.4-0.2-0.8-0.3-1.2-0.3
c-0.5,0-0.9,0.1-1.3,0.4c-0.4,0.3-0.7,0.7-1,1.2C96.6,15.4,96.4,16,96.4,16.6z"/>
<path class="st1" d="M89.2,11.2v1.2c0.3-0.4,0.8-0.7,1.2-0.9c0.5-0.2,1-0.3,1.5-0.3c0.3,0,0.6,0,0.9,0.1v2.5
c-0.4-0.1-0.7-0.1-1.1-0.1c-0.5,0-1,0.1-1.4,0.3c-0.5,0.2-0.8,0.4-1.1,0.8V22H86V11.2H89.2z"/>
<path class="st1" d="M83.7,21.4c-0.5,0.3-1.1,0.5-1.8,0.7c-0.7,0.1-1.3,0.2-1.9,0.2c-2.1,0-3.8-0.5-4.9-1.5
c-1.1-1-1.6-2.4-1.6-4.3c0-1.8,0.5-3.2,1.5-4.2c1-1,2.3-1.5,4-1.5c1.7,0,3,0.5,4,1.5c1,1,1.5,2.3,1.5,4.2c0,0.2,0,0.5,0,0.9h-7.8
C76.9,19.1,78,20,80,20c1.4,0,2.6-0.4,3.7-1.2V21.4z M81.4,15.2c-0.2-0.7-0.5-1.2-0.9-1.5c-0.4-0.3-0.9-0.5-1.5-0.5
c-0.6,0-1,0.2-1.4,0.5c-0.4,0.3-0.7,0.8-0.8,1.5H81.4z"/>
<path class="st1" d="M59.5,8h3.6l3.4,11.8h0.1L69.9,8h3.6l-4.3,14h-5.3L59.5,8z"/>
<path class="st1" d="M46.4,6.6v5.7c0.5-0.4,1-0.7,1.6-0.9c0.6-0.2,1.2-0.3,1.8-0.3c1,0,1.8,0.3,2.4,0.9c0.6,0.6,0.9,1.4,0.9,2.3
V22h-3.2v-7.1c0-0.4-0.2-0.7-0.5-0.9c-0.3-0.3-0.7-0.4-1.1-0.4c-0.3,0-0.6,0.1-0.9,0.2c-0.4,0.2-0.7,0.4-1,0.6V22h-3.2V6.6H46.4z"
/>
<path class="st1" d="M37.9,22.2c-0.8,0-1.6,0-2.5-0.2c-0.8-0.2-1.5-0.4-2.2-0.8v-2.9c0.5,0.4,1.2,0.7,2,1c0.8,0.3,1.5,0.4,2,0.3
c0.4,0,0.7-0.1,0.9-0.3c0.2-0.2,0.3-0.3,0.3-0.5c0.1-0.4,0-0.7-0.3-0.9c-0.3-0.2-0.8-0.4-1.5-0.6c-0.8-0.2-1.5-0.5-1.9-0.8
c-0.5-0.3-0.8-0.6-1.1-1c-0.2-0.4-0.4-0.9-0.4-1.5c0-0.6,0.2-1.2,0.5-1.6c0.3-0.5,0.8-0.9,1.5-1.2c0.7-0.3,1.4-0.4,2.2-0.4
c0.6,0,1.2,0.1,1.8,0.2c0.6,0.1,1.1,0.3,1.5,0.4v2.6c-0.4-0.2-0.9-0.4-1.5-0.6c-0.6-0.2-1.1-0.3-1.5-0.3c-0.9,0-1.4,0.2-1.5,0.7
c0,0.3,0.1,0.5,0.4,0.7c0.3,0.2,0.7,0.4,1.3,0.6c0.8,0.3,1.5,0.5,2,0.8c0.5,0.3,0.9,0.6,1.2,1c0.3,0.4,0.4,1,0.4,1.6
c0,1-0.4,1.8-1.1,2.4C40,21.9,39,22.2,37.9,22.2z"/>
<path class="st1" d="M25.8,22.3c-1,0-1.9-0.2-2.7-0.7c-0.7-0.5-1.3-1.1-1.7-2c-0.4-0.8-0.6-1.8-0.6-2.9c0-1.1,0.2-2.1,0.6-2.9
c0.4-0.9,1-1.5,1.7-2c0.7-0.5,1.6-0.7,2.6-0.7c0.5,0,0.9,0.1,1.4,0.3c0.5,0.2,0.9,0.4,1.3,0.7v-0.7h3.2v8.3c0,1.1,0.1,1.9,0.4,2.5
h-3c-0.1-0.2-0.2-0.4-0.2-0.7C27.9,21.9,26.9,22.3,25.8,22.3z M23.9,16.6c0,0.6,0.1,1.2,0.4,1.7c0.3,0.5,0.6,0.9,1,1.2
c0.4,0.3,0.8,0.4,1.3,0.4c0.3,0,0.7-0.1,1.1-0.2c0.4-0.1,0.7-0.5,1-0.9v-4.5c-0.3-0.5-0.6-0.8-1-0.9c-0.4-0.1-0.7-0.2-1.1-0.2
c-0.5,0-0.9,0.1-1.3,0.4c-0.4,0.3-0.7,0.7-1,1.2C24,15.4,23.9,16,23.9,16.6z"/>
<path class="st1" d="M18.5,22.2c-1.2,0-2.1-0.3-2.7-1c-0.6-0.7-0.9-1.7-0.9-3V6.6H18v10.8c0,0.5,0,0.9,0.1,1.2
c0.1,0.3,0.2,0.5,0.4,0.6c0.1,0.1,0.3,0.2,0.5,0.2c0.2,0,0.5,0,1,0v2.6H18.5z"/>
<path class="st1" d="M8.8,22.3c-1.5,0-2.9-0.3-4.1-0.8C3.6,20.9,2.7,20,2,19c-0.7-1.1-1-2.3-1-3.8c0-1.5,0.3-2.9,1-4
c0.7-1.1,1.6-2,2.7-2.6c1.2-0.6,2.5-0.9,4-0.9c0.7,0,1.5,0.1,2.3,0.2s1.4,0.3,1.9,0.6V11c-1.3-0.5-2.6-0.7-3.8-0.7
c-1.4,0-2.5,0.4-3.4,1.2c-0.9,0.8-1.3,2-1.3,3.7c0,0.9,0.2,1.7,0.6,2.3c0.4,0.7,1,1.2,1.7,1.6c0.7,0.4,1.4,0.6,2.2,0.6l0.4,0
c0.6,0,1.2-0.1,1.8-0.3c0.6-0.2,1.1-0.4,1.6-0.7v2.8c-0.6,0.3-1.2,0.5-1.8,0.6C10.4,22.2,9.6,22.3,8.8,22.3z"/>
</g>
</g>
<g style="mix-blend-mode:hard-light" filter="url(#filter1_f_40_29)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M253.555 253.691C234.166 257.325 210.787 267.678 194.817 279.704C167.671 300.145 143 336.666 143 356.409C143 364.909 148.741 370.132 158.085 370.132C161.927 370.132 162.182 370.298 161.535 372.382C161.15 373.62 160.598 381.832 160.308 390.632C159.843 404.693 160.039 407.723 161.922 415.632C165.896 432.325 175.89 449.337 188.675 461.172C204.384 475.712 222.721 483.721 246.31 486.344C256.49 487.476 257.088 487.667 256.498 489.589C255.106 494.128 254.72 539.224 255.981 549.919C259.914 583.268 273.139 615.139 294.865 643.632C306.099 658.365 327.932 678.511 345 689.893C389.521 719.583 458.858 734.6 535.26 731.098C567.926 729.601 590.289 726.361 615.5 719.475C682.821 701.087 736.946 652.57 757.886 591.84C766.113 567.983 768.638 548.162 767.596 515.632C767.226 504.082 766.675 493.013 766.37 491.033L765.817 487.435L774.158 486.74C817.12 483.16 850.475 457.074 861.243 418.632C863.868 409.261 864.639 388.453 862.753 377.882L861.37 370.132L865.435 370.13C873.132 370.128 881 363.641 881 357.298C881 349.587 875.454 334.787 868.01 322.632C833.048 265.543 764.751 237.996 710.358 259.044C700.723 262.772 689.715 269.233 678 278.038C667.891 285.635 644.942 308.477 640 315.862C637.584 319.472 636.036 320.902 635 320.482C615.25 312.464 578.262 303.843 547 299.972C533.368 298.284 489.859 298.263 475.5 299.938C447.293 303.227 421.534 308.84 399.752 316.442L387.004 320.891L382.317 314.262C376.27 305.71 356.765 286.218 346.479 278.449C331.01 266.765 317.133 259.319 303 255.12C293.122 252.185 265.803 251.396 253.555 253.691ZM525.775 474.141C542.235 479.919 552.81 493.069 560.962 517.895C572.367 552.627 577.508 595.826 572.237 612.632C569.49 621.391 566.737 625.917 560.043 632.68C553.631 639.158 543.575 644.286 532.5 646.726C522.255 648.984 501.437 649.041 491.343 646.839C461.912 640.42 447.619 620.374 449.483 588.132C451.047 561.084 460.42 521.584 470.009 501.632C476.59 487.939 487.236 478.078 499.762 474.074C507.624 471.56 518.502 471.589 525.775 474.141ZM389.384 484.362C395.107 486.072 401.281 492.491 402.955 498.473C406.617 511.549 396.619 525.041 383.218 525.11C364.274 525.207 354.944 501.775 368.761 488.803C374.23 483.668 381.688 482.062 389.384 484.362ZM648.277 484.192C656.216 486.397 662.933 495.325 662.978 503.733C663.025 512.411 659.127 518.893 651.611 522.639C642.535 527.161 632.471 525.166 626.243 517.61C613.065 501.623 628.229 478.625 648.277 484.192Z" stroke="url(#paint1_linear_40_29)" stroke-width="6" stroke-linejoin="round"/>
</g>
<g style="mix-blend-mode:hard-light" filter="url(#filter2_f_40_29)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M253.555 253.691C234.166 257.325 210.787 267.678 194.817 279.704C167.671 300.145 143 336.666 143 356.409C143 364.909 148.741 370.132 158.085 370.132C161.927 370.132 162.182 370.298 161.535 372.382C161.15 373.62 160.598 381.832 160.308 390.632C159.843 404.693 160.039 407.723 161.922 415.632C165.896 432.325 175.89 449.337 188.675 461.172C204.384 475.712 222.721 483.721 246.31 486.344C256.49 487.476 257.088 487.667 256.498 489.589C255.106 494.128 254.72 539.224 255.981 549.919C259.914 583.268 273.139 615.139 294.865 643.632C306.099 658.365 327.932 678.511 345 689.893C389.521 719.583 458.858 734.6 535.26 731.098C567.926 729.601 590.289 726.361 615.5 719.475C682.821 701.087 736.946 652.57 757.886 591.84C766.113 567.983 768.638 548.162 767.596 515.632C767.226 504.082 766.675 493.013 766.37 491.033L765.817 487.435L774.158 486.74C817.12 483.16 850.475 457.074 861.243 418.632C863.868 409.261 864.639 388.453 862.753 377.882L861.37 370.132L865.435 370.13C873.132 370.128 881 363.641 881 357.298C881 349.587 875.454 334.787 868.01 322.632C833.048 265.543 764.751 237.996 710.358 259.044C700.723 262.772 689.715 269.233 678 278.038C667.891 285.635 644.942 308.477 640 315.862C637.584 319.472 636.036 320.902 635 320.482C615.25 312.464 578.262 303.843 547 299.972C533.368 298.284 489.859 298.263 475.5 299.938C447.293 303.227 421.534 308.84 399.752 316.442L387.004 320.891L382.317 314.262C376.27 305.71 356.765 286.218 346.479 278.449C331.01 266.765 317.133 259.319 303 255.12C293.122 252.185 265.803 251.396 253.555 253.691ZM525.775 474.141C542.235 479.919 552.81 493.069 560.962 517.895C572.367 552.627 577.508 595.826 572.237 612.632C569.49 621.391 566.737 625.917 560.043 632.68C553.631 639.158 543.575 644.286 532.5 646.726C522.255 648.984 501.437 649.041 491.343 646.839C461.912 640.42 447.619 620.374 449.483 588.132C451.047 561.084 460.42 521.584 470.009 501.632C476.59 487.939 487.236 478.078 499.762 474.074C507.624 471.56 518.502 471.589 525.775 474.141ZM389.384 484.362C395.107 486.072 401.281 492.491 402.955 498.473C406.617 511.549 396.619 525.041 383.218 525.11C364.274 525.207 354.944 501.775 368.761 488.803C374.23 483.668 381.688 482.062 389.384 484.362ZM648.277 484.192C656.216 486.397 662.933 495.325 662.978 503.733C663.025 512.411 659.127 518.893 651.611 522.639C642.535 527.161 632.471 525.166 626.243 517.61C613.065 501.623 628.229 478.625 648.277 484.192Z" stroke="url(#paint2_linear_40_29)" stroke-opacity="0.7" stroke-width="8" stroke-linejoin="round"/>
</g>
<g style="mix-blend-mode:hard-light" filter="url(#filter3_f_40_29)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M253.555 253.691C234.166 257.325 210.787 267.678 194.817 279.704C167.671 300.145 143 336.666 143 356.409C143 364.909 148.741 370.132 158.085 370.132C161.927 370.132 162.182 370.298 161.535 372.382C161.15 373.62 160.598 381.832 160.308 390.632C159.843 404.693 160.039 407.723 161.922 415.632C165.896 432.325 175.89 449.337 188.675 461.172C204.384 475.712 222.721 483.721 246.31 486.344C256.49 487.476 257.088 487.667 256.498 489.589C255.106 494.128 254.72 539.224 255.981 549.919C259.914 583.268 273.139 615.139 294.865 643.632C306.099 658.365 327.932 678.511 345 689.893C389.521 719.583 458.858 734.6 535.26 731.098C567.926 729.601 590.289 726.361 615.5 719.475C682.821 701.087 736.946 652.57 757.886 591.84C766.113 567.983 768.638 548.162 767.596 515.632C767.226 504.082 766.675 493.013 766.37 491.033L765.817 487.435L774.158 486.74C817.12 483.16 850.475 457.074 861.243 418.632C863.868 409.261 864.639 388.453 862.753 377.882L861.37 370.132L865.435 370.13C873.132 370.128 881 363.641 881 357.298C881 349.587 875.454 334.787 868.01 322.632C833.048 265.543 764.751 237.996 710.358 259.044C700.723 262.772 689.715 269.233 678 278.038C667.891 285.635 644.942 308.477 640 315.862C637.584 319.472 636.036 320.902 635 320.482C615.25 312.464 578.262 303.843 547 299.972C533.368 298.284 489.859 298.263 475.5 299.938C447.293 303.227 421.534 308.84 399.752 316.442L387.004 320.891L382.317 314.262C376.27 305.71 356.765 286.218 346.479 278.449C331.01 266.765 317.133 259.319 303 255.12C293.122 252.185 265.803 251.396 253.555 253.691ZM525.775 474.141C542.235 479.919 552.81 493.069 560.962 517.895C572.367 552.627 577.508 595.826 572.237 612.632C569.49 621.391 566.737 625.917 560.043 632.68C553.631 639.158 543.575 644.286 532.5 646.726C522.255 648.984 501.437 649.041 491.343 646.839C461.912 640.42 447.619 620.374 449.483 588.132C451.047 561.084 460.42 521.584 470.009 501.632C476.59 487.939 487.236 478.078 499.762 474.074C507.624 471.56 518.502 471.589 525.775 474.141ZM389.384 484.362C395.107 486.072 401.281 492.491 402.955 498.473C406.617 511.549 396.619 525.041 383.218 525.11C364.274 525.207 354.944 501.775 368.761 488.803C374.23 483.668 381.688 482.062 389.384 484.362ZM648.277 484.192C656.216 486.397 662.933 495.325 662.978 503.733C663.025 512.411 659.127 518.893 651.611 522.639C642.535 527.161 632.471 525.166 626.243 517.61C613.065 501.623 628.229 478.625 648.277 484.192Z" stroke="url(#paint3_linear_40_29)" stroke-opacity="0.4" stroke-width="7" stroke-linejoin="round"/>
</g>
<g style="mix-blend-mode:hard-light" filter="url(#filter4_ddif_40_29)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M253.555 253.691C234.166 257.325 210.787 267.678 194.817 279.704C167.671 300.145 143 336.666 143 356.409C143 364.909 148.741 370.132 158.085 370.132C161.927 370.132 162.182 370.298 161.535 372.382C161.15 373.62 160.598 381.832 160.308 390.632C159.843 404.693 160.039 407.723 161.922 415.632C165.896 432.325 175.89 449.337 188.675 461.172C204.384 475.712 222.721 483.721 246.31 486.344C256.49 487.476 257.088 487.667 256.498 489.589C255.106 494.128 254.72 539.224 255.981 549.919C259.914 583.268 273.139 615.139 294.865 643.632C306.099 658.365 327.932 678.511 345 689.893C389.521 719.583 458.858 734.6 535.26 731.098C567.926 729.601 590.289 726.361 615.5 719.475C682.821 701.087 736.946 652.57 757.886 591.84C766.113 567.983 768.638 548.162 767.596 515.632C767.226 504.082 766.675 493.013 766.37 491.033L765.817 487.435L774.158 486.74C817.12 483.16 850.475 457.074 861.243 418.632C863.868 409.261 864.639 388.453 862.753 377.882L861.37 370.132L865.435 370.13C873.132 370.128 881 363.641 881 357.298C881 349.587 875.454 334.787 868.01 322.632C833.048 265.543 764.751 237.996 710.358 259.044C700.723 262.772 689.715 269.233 678 278.038C667.891 285.635 644.942 308.477 640 315.862C637.584 319.472 636.036 320.902 635 320.482C615.25 312.464 578.262 303.843 547 299.972C533.368 298.284 489.859 298.263 475.5 299.938C447.293 303.227 421.534 308.84 399.752 316.442L387.004 320.891L382.317 314.262C376.27 305.71 356.765 286.218 346.479 278.449C331.01 266.765 317.133 259.319 303 255.12C293.122 252.185 265.803 251.396 253.555 253.691ZM525.775 474.141C542.235 479.919 552.81 493.069 560.962 517.895C572.367 552.627 577.508 595.826 572.237 612.632C569.49 621.391 566.737 625.917 560.043 632.68C553.631 639.158 543.575 644.286 532.5 646.726C522.255 648.984 501.437 649.041 491.343 646.839C461.912 640.42 447.619 620.374 449.483 588.132C451.047 561.084 460.42 521.584 470.009 501.632C476.59 487.939 487.236 478.078 499.762 474.074C507.624 471.56 518.502 471.589 525.775 474.141ZM389.384 484.362C395.107 486.072 401.281 492.491 402.955 498.473C406.617 511.549 396.619 525.041 383.218 525.11C364.274 525.207 354.944 501.775 368.761 488.803C374.23 483.668 381.688 482.062 389.384 484.362ZM648.277 484.192C656.216 486.397 662.933 495.325 662.978 503.733C663.025 512.411 659.127 518.893 651.611 522.639C642.535 527.161 632.471 525.166 626.243 517.61C613.065 501.623 628.229 478.625 648.277 484.192Z" stroke="url(#paint4_linear_40_29)" stroke-opacity="0.01" stroke-width="5" stroke-linejoin="round"/>
</g>
<g filter="url(#filter5_f_40_29)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M253.555 253.691C234.166 257.325 210.787 267.678 194.817 279.704C167.671 300.145 143 336.666 143 356.409C143 364.909 148.741 370.132 158.085 370.132C161.927 370.132 162.182 370.298 161.535 372.382C161.15 373.62 160.598 381.832 160.308 390.632C159.843 404.693 160.039 407.723 161.922 415.632C165.896 432.325 175.89 449.337 188.675 461.172C204.384 475.712 222.721 483.721 246.31 486.344C256.49 487.476 257.088 487.667 256.498 489.589C255.106 494.128 254.72 539.224 255.981 549.919C259.914 583.268 273.139 615.139 294.865 643.632C306.099 658.365 327.932 678.511 345 689.893C389.521 719.583 458.858 734.6 535.26 731.098C567.926 729.601 590.289 726.361 615.5 719.475C682.821 701.087 736.946 652.57 757.886 591.84C766.113 567.983 768.638 548.162 767.596 515.632C767.226 504.082 766.675 493.013 766.37 491.033L765.817 487.435L774.158 486.74C817.12 483.16 850.475 457.074 861.243 418.632C863.868 409.261 864.639 388.453 862.753 377.882L861.37 370.132L865.435 370.13C873.132 370.128 881 363.641 881 357.298C881 349.587 875.454 334.787 868.01 322.632C833.048 265.543 764.751 237.996 710.358 259.044C700.723 262.772 689.715 269.233 678 278.038C667.891 285.635 644.942 308.477 640 315.862C637.584 319.472 636.036 320.902 635 320.482C615.25 312.464 578.262 303.843 547 299.972C533.368 298.284 489.859 298.263 475.5 299.938C447.293 303.227 421.534 308.84 399.752 316.442L387.004 320.891L382.317 314.262C376.27 305.71 356.765 286.218 346.479 278.449C331.01 266.765 317.133 259.319 303 255.12C293.122 252.185 265.803 251.396 253.555 253.691ZM525.775 474.141C542.235 479.919 552.81 493.069 560.962 517.895C572.367 552.627 577.508 595.826 572.237 612.632C569.49 621.391 566.737 625.917 560.043 632.68C553.631 639.158 543.575 644.286 532.5 646.726C522.255 648.984 501.437 649.041 491.343 646.839C461.912 640.42 447.619 620.374 449.483 588.132C451.047 561.084 460.42 521.584 470.009 501.632C476.59 487.939 487.236 478.078 499.762 474.074C507.624 471.56 518.502 471.589 525.775 474.141ZM389.384 484.362C395.107 486.072 401.281 492.491 402.955 498.473C406.617 511.549 396.619 525.041 383.218 525.11C364.274 525.207 354.944 501.775 368.761 488.803C374.23 483.668 381.688 482.062 389.384 484.362ZM648.277 484.192C656.216 486.397 662.933 495.325 662.978 503.733C663.025 512.411 659.127 518.893 651.611 522.639C642.535 527.161 632.471 525.166 626.243 517.61C613.065 501.623 628.229 478.625 648.277 484.192Z" stroke="url(#paint5_linear_40_29)" stroke-width="9.5" stroke-linejoin="round"/>
</g>
<g filter="url(#filter6_f_40_29)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M253.555 253.691C234.166 257.325 210.787 267.678 194.817 279.704C167.671 300.145 143 336.666 143 356.409C143 364.909 148.741 370.132 158.085 370.132C161.927 370.132 162.182 370.298 161.535 372.382C161.15 373.62 160.598 381.832 160.308 390.632C159.843 404.693 160.039 407.723 161.922 415.632C165.896 432.325 175.89 449.337 188.675 461.172C204.384 475.712 222.721 483.721 246.31 486.344C256.49 487.476 257.088 487.667 256.498 489.589C255.106 494.128 254.72 539.224 255.981 549.919C259.914 583.268 273.139 615.139 294.865 643.632C306.099 658.365 327.932 678.511 345 689.893C389.521 719.583 458.858 734.6 535.26 731.098C567.926 729.601 590.289 726.361 615.5 719.475C682.821 701.087 736.946 652.57 757.886 591.84C766.113 567.983 768.638 548.162 767.596 515.632C767.226 504.082 766.675 493.013 766.37 491.033L765.817 487.435L774.158 486.74C817.12 483.16 850.475 457.074 861.243 418.632C863.868 409.261 864.639 388.453 862.753 377.882L861.37 370.132L865.435 370.13C873.132 370.128 881 363.641 881 357.298C881 349.587 875.454 334.787 868.01 322.632C833.048 265.543 764.751 237.996 710.358 259.044C700.723 262.772 689.715 269.233 678 278.038C667.891 285.635 644.942 308.477 640 315.862C637.584 319.472 636.036 320.902 635 320.482C615.25 312.464 578.262 303.843 547 299.972C533.368 298.284 489.859 298.263 475.5 299.938C447.293 303.227 421.534 308.84 399.752 316.442L387.004 320.891L382.317 314.262C376.27 305.71 356.765 286.218 346.479 278.449C331.01 266.765 317.133 259.319 303 255.12C293.122 252.185 265.803 251.396 253.555 253.691ZM525.775 474.141C542.235 479.919 552.81 493.069 560.962 517.895C572.367 552.627 577.508 595.826 572.237 612.632C569.49 621.391 566.737 625.917 560.043 632.68C553.631 639.158 543.575 644.286 532.5 646.726C522.255 648.984 501.437 649.041 491.343 646.839C461.912 640.42 447.619 620.374 449.483 588.132C451.047 561.084 460.42 521.584 470.009 501.632C476.59 487.939 487.236 478.078 499.762 474.074C507.624 471.56 518.502 471.589 525.775 474.141ZM389.384 484.362C395.107 486.072 401.281 492.491 402.955 498.473C406.617 511.549 396.619 525.041 383.218 525.11C364.274 525.207 354.944 501.775 368.761 488.803C374.23 483.668 381.688 482.062 389.384 484.362ZM648.277 484.192C656.216 486.397 662.933 495.325 662.978 503.733C663.025 512.411 659.127 518.893 651.611 522.639C642.535 527.161 632.471 525.166 626.243 517.61C613.065 501.623 628.229 478.625 648.277 484.192Z" stroke="white" stroke-width="2.9" stroke-linejoin="round"/>
</g>
<defs>
<filter id="filter0_f_40_29" x="-42" y="0" width="1108" height="1032" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="150" result="effect1_foregroundBlur_40_29"/>
</filter>
<filter id="filter1_f_40_29" x="118.94" y="227.932" width="786.12" height="527.726" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="10.53" result="effect1_foregroundBlur_40_29"/>
</filter>
<filter id="filter2_f_40_29" x="89" y="197.99" width="846" height="587.608" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="25" result="effect1_foregroundBlur_40_29"/>
</filter>
<filter id="filter3_f_40_29" x="132.48" y="241.471" width="759.04" height="500.647" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="3.51" result="effect1_foregroundBlur_40_29"/>
</filter>
<filter id="filter4_ddif_40_29" x="110.5" y="219.494" width="803" height="544.604" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3.9"/>
<feGaussianBlur stdDeviation="1.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.109804 0 0 0 0 0.886275 0 0 0 0 0.968627 0 0 0 1 0"/>
<feBlend mode="multiply" in2="BackgroundImageFix" result="effect1_dropShadow_40_29"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="7.02"/>
<feGaussianBlur stdDeviation="4.563"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.819608 0 0 0 0 0.054902 0 0 0 0 0.996078 0 0 0 1 0"/>
<feBlend mode="color-dodge" in2="effect1_dropShadow_40_29" result="effect2_dropShadow_40_29"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_40_29" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="-0.39" dy="0.78"/>
<feGaussianBlur stdDeviation="0.195"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
<feBlend mode="normal" in2="shape" result="effect3_innerShadow_40_29"/>
<feGaussianBlur stdDeviation="15" result="effect4_foregroundBlur_40_29"/>
</filter>
<filter id="filter5_f_40_29" x="137.15" y="246.138" width="749.7" height="491.31" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="0.55" result="effect1_foregroundBlur_40_29"/>
</filter>
<filter id="filter6_f_40_29" x="137.15" y="246.146" width="749.7" height="491.302" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.2" result="effect1_foregroundBlur_40_29"/>
</filter>
<radialGradient id="paint0_radial_40_29" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(512 516) rotate(20.8768) scale(331.625 512.736)">
<stop stop-color="#0CF6F7"/>
<stop offset="1" stop-color="#D10EFE"/>
</radialGradient>
<linearGradient id="paint1_linear_40_29" x1="826.587" y1="628.647" x2="310.413" y2="316.853" gradientUnits="userSpaceOnUse">
<stop stop-color="#0CF6F7"/>
<stop offset="0.461563" stop-color="#6B86FA"/>
<stop offset="1" stop-color="#D10EFE"/>
</linearGradient>
<linearGradient id="paint2_linear_40_29" x1="826.587" y1="628.647" x2="310.413" y2="316.853" gradientUnits="userSpaceOnUse">
<stop stop-color="#0CF6F7"/>
<stop offset="0.225986" stop-color="#6B86FA"/>
<stop offset="0.673077" stop-color="#6B86FA"/>
<stop offset="1" stop-color="#D10EFE"/>
</linearGradient>
<linearGradient id="paint3_linear_40_29" x1="826.587" y1="628.647" x2="310.413" y2="316.853" gradientUnits="userSpaceOnUse">
<stop stop-color="#0CF6F7"/>
<stop offset="0.225986" stop-color="#6B86FA"/>
<stop offset="0.673077" stop-color="#6B86FA"/>
<stop offset="1" stop-color="#D10EFE"/>
</linearGradient>
<linearGradient id="paint4_linear_40_29" x1="826.587" y1="628.647" x2="310.413" y2="316.853" gradientUnits="userSpaceOnUse">
<stop stop-color="#0CF6F7"/>
<stop offset="0.225986" stop-color="#6B86FA"/>
<stop offset="0.673077" stop-color="#6B86FA"/>
<stop offset="1" stop-color="#D10EFE"/>
</linearGradient>
<linearGradient id="paint5_linear_40_29" x1="826.587" y1="628.647" x2="310.413" y2="316.853" gradientUnits="userSpaceOnUse">
<stop stop-color="#0CF6F7"/>
<stop offset="0.461563" stop-color="#6B86FA"/>
<stop offset="1" stop-color="#D10EFE"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 453 KiB

View File

@@ -0,0 +1,30 @@
"use client";
import { Toaster, toast } from "sonner";
import { useEffect, useSyncExternalStore } from "react";
import {
getSnapshotNotices,
hideNotice,
subscribeNotices,
} from "@/services/noticeService";
export const NoticeManager = () => {
const currentNotices = useSyncExternalStore(
subscribeNotices,
getSnapshotNotices,
);
useEffect(() => {
for (const notice of currentNotices) {
const toastId = toast(notice.message, {
id: notice.id,
duration: notice.duration,
onDismiss: (t) => {
hideNotice(t.id as number);
},
});
}
}, [currentNotices]);
return <Toaster />;
};

View File

@@ -5,3 +5,4 @@ export { BaseLoading } from "./base-loading";
export { BaseErrorBoundary } from "./base-error-boundary";
export { Switch } from "./base-switch";
export { BaseLoadingOverlay } from "./base-loading-overlay";
export { NoticeManager } from "./NoticeManager";

View File

@@ -1,14 +1,15 @@
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import React, { useMemo, useState, useEffect, useRef } from "react";
import React, { useMemo, useState, useEffect, RefObject } from "react";
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
Header,
Row,
ColumnSizingState,
} from "@tanstack/react-table";
import { TableVirtuoso, TableComponents } from "react-virtuoso";
import {
Table,
@@ -26,30 +27,7 @@ import { cn } from "@root/lib/utils";
dayjs.extend(relativeTime);
interface IConnectionsItem {
id: string;
metadata: {
host: string;
destinationIP: string;
destinationPort: string;
remoteDestination: string;
process?: string;
processPath?: string;
sourceIP: string;
sourcePort: string;
type: string;
network: string;
};
rule: string;
rulePayload?: string;
chains: string[];
download: number;
upload: number;
curDownload?: number;
curUpload?: number;
start: string;
}
// Интерфейс для строки данных, которую использует react-table
interface ConnectionRow {
id: string;
host: string;
@@ -67,78 +45,22 @@ interface ConnectionRow {
connectionData: IConnectionsItem;
}
// Интерфейс для пропсов, которые компонент получает от родителя
interface Props {
connections: IConnectionsItem[];
onShowDetail: (data: IConnectionsItem) => void;
scrollerRef: (element: HTMLElement | Window | null) => void;
}
const ColumnResizer = ({
header,
}: {
header: Header<ConnectionRow, unknown>;
}) => {
return (
<div
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={cn(
"absolute right-0 top-0 h-full w-1 cursor-col-resize select-none touch-none",
"bg-transparent hover:bg-primary/50 active:bg-primary",
"transition-colors duration-150",
header.column.getIsResizing() && "bg-primary",
)}
style={{
transform: header.column.getIsResizing() ? `translateX(0px)` : "",
}}
/>
);
};
export const ConnectionTable = (props: Props) => {
const { connections, onShowDetail, scrollerRef } = props;
const tableContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (tableContainerRef.current && scrollerRef) {
scrollerRef(tableContainerRef.current);
}
}, [scrollerRef]);
const [columnSizing, setColumnSizing] = useState<ColumnSizingState>(() => {
try {
const saved = localStorage.getItem("connection-table-widths");
return saved
? JSON.parse(saved)
: {
host: 220,
download: 88,
upload: 88,
dlSpeed: 88,
ulSpeed: 88,
chains: 340,
rule: 280,
process: 220,
time: 120,
source: 200,
remoteDestination: 200,
type: 160,
};
return saved ? JSON.parse(saved) : {};
} catch {
return {
host: 220,
download: 88,
upload: 88,
dlSpeed: 88,
ulSpeed: 88,
chains: 340,
rule: 280,
process: 220,
time: 120,
source: 200,
remoteDestination: 200,
type: 160,
};
return {};
}
});
@@ -185,16 +107,13 @@ export const ConnectionTable = (props: Props) => {
header: () => t("Host"),
size: columnSizing?.host || 220,
minSize: 180,
maxSize: 400,
},
{
accessorKey: "download",
header: () => t("Downloaded"),
size: columnSizing?.download || 88,
minSize: 80,
maxSize: 150,
cell: ({ getValue }) => (
<div className="text-right font-mono text-sm">
<div className="text-right">
{parseTraffic(getValue<number>()).join(" ")}
</div>
),
@@ -203,10 +122,8 @@ export const ConnectionTable = (props: Props) => {
accessorKey: "upload",
header: () => t("Uploaded"),
size: columnSizing?.upload || 88,
minSize: 80,
maxSize: 150,
cell: ({ getValue }) => (
<div className="text-right font-mono text-sm">
<div className="text-right">
{parseTraffic(getValue<number>()).join(" ")}
</div>
),
@@ -215,10 +132,8 @@ export const ConnectionTable = (props: Props) => {
accessorKey: "dlSpeed",
header: () => t("DL Speed"),
size: columnSizing?.dlSpeed || 88,
minSize: 80,
maxSize: 150,
cell: ({ getValue }) => (
<div className="text-right font-mono text-sm">
<div className="text-right">
{parseTraffic(getValue<number>()).join(" ")}/s
</div>
),
@@ -227,10 +142,8 @@ export const ConnectionTable = (props: Props) => {
accessorKey: "ulSpeed",
header: () => t("UL Speed"),
size: columnSizing?.ulSpeed || 88,
minSize: 80,
maxSize: 150,
cell: ({ getValue }) => (
<div className="text-right font-mono text-sm">
<div className="text-right">
{parseTraffic(getValue<number>()).join(" ")}/s
</div>
),
@@ -240,30 +153,26 @@ export const ConnectionTable = (props: Props) => {
header: () => t("Chains"),
size: columnSizing?.chains || 340,
minSize: 180,
maxSize: 500,
},
{
accessorKey: "rule",
header: () => t("Rule"),
size: columnSizing?.rule || 280,
minSize: 180,
maxSize: 400,
},
{
accessorKey: "process",
header: () => t("Process"),
size: columnSizing?.process || 220,
minSize: 180,
maxSize: 350,
},
{
accessorKey: "time",
header: () => t("Time"),
size: columnSizing?.time || 120,
minSize: 100,
maxSize: 180,
cell: ({ getValue }) => (
<div className="text-right font-mono text-sm">
<div className="text-right">
{dayjs(getValue<string>()).fromNow()}
</div>
),
@@ -273,21 +182,18 @@ export const ConnectionTable = (props: Props) => {
header: () => t("Source"),
size: columnSizing?.source || 200,
minSize: 130,
maxSize: 300,
},
{
accessorKey: "remoteDestination",
header: () => t("Destination"),
size: columnSizing?.remoteDestination || 200,
minSize: 130,
maxSize: 300,
},
{
accessorKey: "type",
header: () => t("Type"),
size: columnSizing?.type || 160,
minSize: 100,
maxSize: 220,
},
],
[columnSizing],
@@ -300,91 +206,92 @@ export const ConnectionTable = (props: Props) => {
onColumnSizingChange: setColumnSizing,
getCoreRowModel: getCoreRowModel(),
columnResizeMode: "onChange",
enableColumnResizing: true,
});
const totalTableWidth = useMemo(() => {
return table.getCenterTotalSize();
}, [table.getState().columnSizing]);
if (connRows.length === 0) {
return (
<div className="flex h-full items-center justify-center">
<p className="text-muted-foreground">{t("No connections")}</p>
</div>
);
}
const VirtuosoTableComponents = useMemo<TableComponents<Row<ConnectionRow>>>(
() => ({
// Явно типизируем `ref` для каждого компонента
Scroller: React.forwardRef<HTMLDivElement>((props, ref) => (
<div className="h-full" {...props} ref={ref} />
)),
Table: (props) => <Table {...props} className="w-full border-collapse" />,
TableHead: React.forwardRef<HTMLTableSectionElement>((props, ref) => (
<TableHeader {...props} ref={ref} />
)),
// Явно типизируем пропсы и `ref` для TableRow
TableRow: React.forwardRef<
HTMLTableRowElement,
{ item: Row<ConnectionRow> } & React.HTMLAttributes<HTMLTableRowElement>
>(({ item: row, ...props }, ref) => {
// `Virtuoso` передает нам готовую строку `row` в пропсе `item`.
// Больше не нужно искать ее по индексу!
return (
<TableRow
{...props}
ref={ref}
data-state={row.getIsSelected() && "selected"}
className="cursor-pointer hover:bg-muted/50"
onClick={() => onShowDetail(row.original.connectionData)}
/>
);
}),
TableBody: React.forwardRef<HTMLTableSectionElement>((props, ref) => (
<TableBody {...props} ref={ref} />
)),
}),
[],
);
return (
<div className="rounded-md border relative bg-background">
<Table
className="w-full border-collapse table-fixed"
style={{
width: totalTableWidth,
minWidth: "100%",
}}
>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow
key={headerGroup.id}
className="hover:bg-transparent border-b-0 h-10"
>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
className={cn(
"sticky top-0 z-10",
"p-2 text-xs font-semibold select-none border-r last:border-r-0 bg-background h-10",
)}
style={{
width: header.getSize(),
minWidth: header.column.columnDef.minSize,
maxWidth: header.column.columnDef.maxSize,
}}
>
<div className="flex items-center justify-between h-full">
<div className="h-full rounded-md border overflow-hidden">
{connRows.length > 0 ? (
<TableVirtuoso
scrollerRef={scrollerRef}
data={table.getRowModel().rows}
components={VirtuosoTableComponents}
fixedHeaderContent={() =>
table.getHeaderGroups().map((headerGroup) => (
<TableRow
key={headerGroup.id}
className="hover:bg-transparent bg-background/95 backdrop-blur"
>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
style={{ width: header.getSize() }}
className="p-2"
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</div>
{header.column.getCanResize() && (
<ColumnResizer header={header} />
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => onShowDetail(row.original.connectionData)}
>
</TableHead>
))}
</TableRow>
))
}
itemContent={(index, row) => (
<>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className="p-2 whitespace-nowrap overflow-hidden text-ellipsis text-sm border-r last:border-r-0"
style={{
width: cell.column.getSize(),
minWidth: cell.column.columnDef.minSize,
maxWidth: cell.column.columnDef.maxSize,
}}
style={{ width: cell.column.getSize() }}
className="p-2 whitespace-nowrap"
onClick={() => onShowDetail(row.original.connectionData)}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</>
)}
/>
) : (
<div className="flex h-full items-center justify-center">
<p>No results.</p>
</div>
)}
</div>
);
};

View File

@@ -1,123 +0,0 @@
import React from "react";
import { motion, HTMLMotionProps, Transition, AnimatePresence } from "framer-motion";
import { cn } from "@root/lib/utils";
import { Power } from "lucide-react";
export interface PowerButtonProps extends HTMLMotionProps<"button"> {
checked?: boolean;
loading?: boolean;
}
export const PowerButton = React.forwardRef<HTMLButtonElement, PowerButtonProps>(
({ className, checked = false, loading = false, ...props }, ref) => {
const state = checked ? "on" : "off";
// Единые, мягкие настройки для всех пружинных анимаций
const sharedSpring: Transition = {
type: "spring",
stiffness: 100,
damping: 30,
mass: 1,
};
const glowColors = {
on: "rgba(74, 222, 128, 0.6)",
off: "rgba(239, 68, 68, 0.4)",
};
const shadows = {
on: "0px 0px 50px rgba(34, 197, 94, 1)",
off: "0px 0px 30px rgba(239, 68, 68, 0.6)",
disabled: "none",
};
const textColors = {
on: "rgb(255, 255, 255)",
off: "rgb(239, 68, 68)",
disabled: "rgb(100, 116, 139)",
};
const isDisabled = props.disabled && !loading;
const currentShadow = isDisabled ? shadows.disabled : checked ? shadows.on : shadows.off;
const currentColor = isDisabled ? textColors.disabled : checked ? textColors.on : textColors.off;
return (
<div className="relative flex items-center justify-center h-44 w-44">
<motion.div
className="absolute h-28 w-28 rounded-full blur-3xl"
animate={{
backgroundColor: state === "on" ? glowColors.on : glowColors.off,
opacity: isDisabled ? 0 : checked ? 1 : 0.3,
scale: checked ? 1.2 : 0.8,
}}
transition={sharedSpring}
/>
<motion.div
className="absolute h-40 w-40 rounded-full blur-[60px]"
animate={{
backgroundColor: checked ? "rgba(34, 197, 94, 0.2)" : "rgba(239, 68, 68, 0.1)",
opacity: isDisabled ? 0 : checked ? 0.8 : 0,
scale: checked ? 1.4 : 0.6,
}}
transition={sharedSpring}
/>
<motion.button
ref={ref}
type="button"
disabled={loading || props.disabled}
animate={{
scale: checked ? 1.1 : 0.9,
boxShadow: currentShadow,
color: currentColor,
}}
whileHover={{ scale: checked ? 1.15 : 0.95 }}
whileTap={{ scale: checked ? 1.05 : 0.85 }}
transition={sharedSpring}
className={cn(
"group",
"relative z-10 flex items-center justify-center h-36 w-36 rounded-full border-2",
"backdrop-blur-sm bg-white/10 border-white/20",
"focus:outline-none",
"disabled:cursor-not-allowed",
isDisabled && "grayscale opacity-50 bg-slate-100/70 border-slate-300/80",
className
)}
{...props}
>
<motion.span
className="flex items-center justify-center"
animate={{ scale: checked ? 1 / 1.1 : 1 }}
whileTap={{ scale: 0.95 }}
transition={sharedSpring}
>
<Power className="h-20 w-20" />
</motion.span>
</motion.button>
<AnimatePresence>
{loading && (
<motion.div
key="pb-loader"
className="absolute inset-0 z-20 flex items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
<div
className={cn(
"h-full w-full animate-spin rounded-full border-4",
"border-transparent",
checked ? "border-t-green-500" : "border-t-red-500",
"blur-xs"
)}
/>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
);

View File

@@ -41,14 +41,12 @@ interface IProxyGroup {
}
// --- Вспомогательная функция для цвета задержки ---
function getDelayColorClasses(delayValue: number): string {
if (delayValue < 0) {
return "text-muted-foreground border-border";
}
if (delayValue >= 150) {
return "text-destructive border-destructive/40";
}
return "text-green-600 border-green-500/40 dark:text-green-400 dark:border-green-400/30";
function getDelayBadgeVariant(
delayValue: number,
): "default" | "secondary" | "destructive" | "outline" {
if (delayValue < 0) return "secondary";
if (delayValue >= 150) return "destructive";
return "default";
}
// --- Дочерний компонент для элемента списка с "живым" обновлением пинга ---
@@ -82,21 +80,20 @@ const ProxySelectItem = ({
}, [proxyName, groupName]);
return (
<SelectItem key={proxyName} value={proxyName}>
<div className="flex items-center justify-between w-full">
<Badge
variant="outline"
className={cn(
"mr-2 flex-shrink-0 px-2 h-5 w-8 justify-center transition-colors duration-300",
getDelayColorClasses(delay),
isJustUpdated && "bg-primary/10 border-primary/50",
)}
>
{delay < 0 || delay > 10000 ? "---" : delay}
</Badge>
<span className="truncate">{proxyName}</span>
</div>
</SelectItem>
<SelectItem key={proxyName} value={proxyName}>
<div className="flex items-center justify-between w-full">
<span className="truncate">{proxyName}</span>
<Badge
variant={getDelayBadgeVariant(delay)}
className={cn(
"ml-4 flex-shrink-0 px-2 h-5 justify-center transition-colors duration-300",
isJustUpdated && "bg-primary/20 border-primary/50",
)}
>
{delay < 0 || delay > 10000 ? "---" : delay}
</Badge>
</div>
</SelectItem>
);
};
@@ -262,21 +259,11 @@ export const ProxySelectors: React.FC = () => {
?.all;
if (sourceList) {
const rawOptions = sourceList
.map((proxy: any) => ({
name: typeof proxy === "string" ? proxy : proxy.name,
}))
.filter((p: { name: string }) => p.name);
// Удаляем дубли по имени прокси
const uniqueNames = new Set<string>();
options = rawOptions.filter((proxy: any) => {
if (!uniqueNames.has(proxy.name)) {
uniqueNames.add(proxy.name);
return true;
}
return false;
});
options = sourceList
.map((proxy: any) => ({
name: typeof proxy === "string" ? proxy : proxy.name,
}))
.filter((p: { name: string }) => p.name);
}
if (sortType === "name")
@@ -358,37 +345,44 @@ export const ProxySelectors: React.FC = () => {
{sortType === "name" && <WholeWord className="h-4 w-4" />}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{sortType === "default" && <p>{t("Sort by default")}</p>}
{sortType === "delay" && <p>{t("Sort by delay")}</p>}
{sortType === "name" && <p>{t("Sort by name")}</p>}
</TooltipContent>
</Tooltip>
</div>
<Select
value={selectedProxy}
onValueChange={handleProxyChange}
disabled={isDirectMode}
onOpenChange={handleProxyListOpen}
>
<SelectTrigger className="w-100">
<Tooltip>
<TooltipTrigger asChild>
<span className="truncate">
<SelectValue placeholder={t("Select a proxy...")} />
</span>
</TooltipTrigger>
<TooltipContent>
{sortType === "default" && <p>{t("Sort by default")}</p>}
{sortType === "delay" && <p>{t("Sort by delay")}</p>}
{sortType === "name" && <p>{t("Sort by name")}</p>}
<p>{selectedProxy}</p>
</TooltipContent>
</Tooltip>
</div>
<Select
value={selectedProxy}
onValueChange={handleProxyChange}
disabled={isDirectMode}
onOpenChange={handleProxyListOpen}
>
<SelectTrigger className="w-100">
<span className="truncate">
<SelectValue placeholder={t("Select a proxy...")} />
</span>
</SelectTrigger>
<SelectContent>
{proxyOptions.map((proxy) => (
<ProxySelectItem
key={proxy.name}
proxyName={proxy.name}
groupName={selectedGroup}
/>
))}
</SelectContent>
</Select>
</div>
</SelectTrigger>
<SelectContent>
{proxyOptions.map((proxy) => (
<ProxySelectItem
key={proxy.name}
proxyName={proxy.name}
groupName={selectedGroup}
/>
))}
</SelectContent>
</Select>
</div>
</TooltipProvider>
</div>
</TooltipProvider>
);
};

View File

@@ -1,18 +1,16 @@
import { Link } from "react-router-dom";
import { Link } from 'react-router-dom';
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarContent, SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { t } from "i18next";
import { cn } from "@root/lib/utils";
SidebarMenuItem, useSidebar,
} from "@/components/ui/sidebar"
import { t } from 'i18next';
import { cn } from '@root/lib/utils';
import {
Home,
@@ -21,22 +19,20 @@ import {
Cable,
ListChecks,
FileText,
Settings,
EarthLock,
} from "lucide-react";
Settings, EarthLock,
} from 'lucide-react';
import { UpdateButton } from "@/components/layout/update-button";
import React from "react";
import { SheetClose } from "@/components/ui/sheet";
import logo from "@/assets/image/logo.png";
import { SheetClose } from '@/components/ui/sheet';
const menuItems = [
{ title: "Home", url: "/home", icon: Home },
{ title: "Profiles", url: "/profile", icon: Users },
{ title: "Proxies", url: "/proxies", icon: Server },
{ title: "Connections", url: "/connections", icon: Cable },
{ title: "Rules", url: "/rules", icon: ListChecks },
{ title: "Logs", url: "/logs", icon: FileText },
{ title: "Settings", url: "/settings", icon: Settings },
{ title: 'Home', url: '/home', icon: Home },
{ title: 'Profiles', url: '/profile', icon: Users },
{ title: 'Proxies', url: '/proxies', icon: Server },
{ title: 'Connections', url: '/connections', icon: Cable },
{ title: 'Rules', url: '/rules', icon: ListChecks },
{ title: 'Logs', url: '/logs', icon: FileText },
{ title: 'Settings', url: '/settings', icon: Settings },
];
export function AppSidebar() {
@@ -44,15 +40,18 @@ export function AppSidebar() {
return (
<Sidebar variant="floating" collapsible="icon">
<SidebarHeader>
<SidebarMenuButton
size="lg"
<SidebarMenuButton size="lg"
className={cn(
"flex h-12 items-center transition-all duration-200",
"group-data-[state=expanded]:w-full group-data-[state=expanded]:gap-2 group-data-[state=expanded]:px-3",
"group-data-[state=collapsed]:w-full group-data-[state=collapsed]:justify-center",
"group-data-[state=collapsed]:w-full group-data-[state=collapsed]:justify-center"
)}
>
<img src={logo} alt="logo" className="h-6 w-6 flex-shrink-0" />
<img
src="./assets/image/logo.png"
alt="logo"
className="h-6 w-6 flex-shrink-0"
/>
<span className="font-semibold whitespace-nowrap group-data-[state=collapsed]:hidden">
Koala Clash
</span>
@@ -69,29 +68,30 @@ export function AppSidebar() {
key={item.title}
to={item.url}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary",
"data-[active=true]:font-semibold data-[active=true]:border",
'flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary',
'data-[active=true]:font-semibold data-[active=true]:border'
)}
>
<item.icon className="h-4 w-4 drop-shadow-md" />
{t(item.title)}
</Link>
);
)
return (
<SidebarMenuItem key={item.title} className="my-1">
<SidebarMenuButton
asChild
isActive={isActive}
tooltip={t(item.title)}
>
{isMobile ? (
<SheetClose asChild>{linkElement}</SheetClose>
<SidebarMenuItem key={item.title} className="my-1">
<SidebarMenuButton
asChild
isActive={isActive}
tooltip={t(item.title)}>
{isMobile ? (
<SheetClose asChild>
{linkElement}
</SheetClose>
) : (
linkElement
)}
</SidebarMenuButton>
</SidebarMenuItem>
);
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarGroupContent>
@@ -103,5 +103,5 @@ export function AppSidebar() {
</div>
</SidebarFooter>
</Sidebar>
);
)
}

View File

@@ -6,7 +6,7 @@ import { DialogRef } from "../base";
import { useVerge } from "@/hooks/use-verge";
import { Button } from "@/components/ui/button";
import { t } from "i18next";
import { Download, RefreshCw } from "lucide-react";
import {Download, RefreshCw} from "lucide-react";
import { useSidebar } from "../ui/sidebar";
interface Props {
@@ -17,7 +17,7 @@ export const UpdateButton = (props: Props) => {
const { className } = props;
const { verge } = useVerge();
const { auto_check_update } = verge || {};
const { state: sidebarState } = useSidebar();
const { state: sidebarState } = useSidebar();
const viewerRef = useRef<DialogRef>(null);
@@ -36,7 +36,7 @@ export const UpdateButton = (props: Props) => {
return (
<>
<UpdateViewer ref={viewerRef} />
{sidebarState === "collapsed" ? (
{sidebarState === 'collapsed' ? (
<Button
variant="outline"
size="icon"

View File

@@ -1,56 +1,47 @@
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo } from "react";
import { useSetThemeMode, useThemeMode } from "@/services/states";
import { useVerge } from "@/hooks/use-verge";
import {
getCurrentWebviewWindow,
WebviewWindow,
} from "@tauri-apps/api/webviewWindow";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { Theme } from "@tauri-apps/api/window";
export const useCustomTheme = () => {
const appWindow: WebviewWindow = useMemo(() => getCurrentWebviewWindow(), []);
const appWindow = useMemo(() => getCurrentWebviewWindow(), []);
const { verge } = useVerge();
const { theme_mode } = verge ?? {};
const mode = useThemeMode();
const setMode = useSetThemeMode();
const [systemTheme, setSystemTheme] = useState(() =>
window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light",
);
useEffect(() => {
setMode(
theme_mode === "light" || theme_mode === "dark" ? theme_mode : "system",
);
}, [theme_mode, setMode]);
useEffect(() => {
if (mode !== "system") return;
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = (e: MediaQueryListEvent) => {
setSystemTheme(e.matches ? "dark" : "light");
};
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, [mode]);
useEffect(() => {
const root = document.documentElement;
const activeTheme = mode === "system" ? systemTheme : mode;
const activeTheme =
mode === "system"
? window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
: mode;
root.classList.remove("light", "dark");
root.classList.add(activeTheme);
appWindow.setTheme(activeTheme as Theme).catch(console.error);
}, [mode, appWindow]);
if (theme_mode === "system") {
appWindow.setTheme(null).catch(console.error);
} else {
appWindow.setTheme(activeTheme as Theme).catch(console.error);
}
}, [mode, systemTheme, appWindow, theme_mode]);
useEffect(() => {
if (theme_mode !== "system") return;
const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
setMode(payload);
});
return () => {
unlistenPromise.then((f) => f());
};
}, [theme_mode, appWindow, setMode]);
return {};
};

View File

@@ -1,5 +1,6 @@
import { useTranslation } from "react-i18next";
// Новые импорты из shadcn/ui
import {
AlertDialog,
AlertDialogAction,
@@ -17,7 +18,7 @@ interface Props {
open: boolean;
title: string;
description: string;
onOpenChange: (open: boolean) => void;
onOpenChange: (open: boolean) => void; // shadcn использует этот коллбэк
onConfirm: () => void;
}
@@ -29,7 +30,7 @@ export const ConfirmViewer = (props: Props) => {
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="truncate">{title}</AlertDialogTitle>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>

View File

@@ -1,12 +1,12 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogFooter
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { AlertTriangle } from "lucide-react";
@@ -21,10 +21,10 @@ export const HwidErrorDialog = () => {
setErrorMessage(customEvent.detail);
};
window.addEventListener("show-hwid-error", handleShowHwidError);
window.addEventListener('show-hwid-error', handleShowHwidError);
return () => {
window.removeEventListener("show-hwid-error", handleShowHwidError);
window.removeEventListener('show-hwid-error', handleShowHwidError);
};
}, []);

View File

@@ -23,6 +23,7 @@ import { open } from "@tauri-apps/plugin-shell";
import { ProxiesEditorViewer } from "./proxies-editor-viewer";
import { cn } from "@root/lib/utils";
// --- Компоненты shadcn/ui ---
import { Card } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Badge } from "@/components/ui/badge";
@@ -45,6 +46,7 @@ import {
ContextMenuTrigger,
} from "@/components/ui/context-menu";
// --- Иконки ---
import {
GripVertical,
File as FileIcon,
@@ -65,13 +67,14 @@ import {
ListTree,
CheckCircle,
Infinity,
RefreshCw,
Network,
RefreshCw, Network,
} from "lucide-react";
import { t } from "i18next";
// Активируем плагин для dayjs
dayjs.extend(relativeTime);
// --- Вспомогательные функции ---
const parseUrl = (url?: string): string | undefined => {
if (!url) return undefined;
try {
@@ -299,12 +302,6 @@ export const ProfileItem = (props: Props) => {
isDestructive: true,
};
const MAX_NAME_LENGTH = 25;
const truncatedName =
name.length > MAX_NAME_LENGTH
? `${name.slice(0, MAX_NAME_LENGTH)}...`
: name;
return (
<div ref={setNodeRef} style={style} {...attributes}>
<ContextMenu>
@@ -346,7 +343,10 @@ export const ProfileItem = (props: Props) => {
) : null}
</div>
<div className="flex items-center flex-shrink-0">
<Badge variant="outline" className="text-xs shadow-sm">
<Badge
variant="outline"
className="text-xs shadow-sm"
>
{type}
</Badge>
</div>
@@ -388,13 +388,14 @@ export const ProfileItem = (props: Props) => {
<div className="flex items-center justify-between">
<div className="flex items-center">
<Download className="h-3 w-3 inline mr-1.5" />
<span className="pr-5">{parseTraffic(download)}</span>
<span className="pr-5">
{parseTraffic(download)}
</span>
<Network className="h-3 w-3 inline mr-1.5" />
{total > 0 ? (
<span>{parseTraffic(total)}</span>
) : (
<Infinity className="h-3 w-3 inline mr-1.5" />
)}
<span>{parseTraffic(total)}</span>
) : <Infinity className="h-3 w-3 inline mr-1.5" />}
</div>
</div>
</div>
@@ -458,6 +459,7 @@ export const ProfileItem = (props: Props) => {
</ContextMenuContent>
</ContextMenu>
{/* Модальные окна для редактирования */}
{fileOpen && (
<EditorViewer
open={true}
@@ -477,10 +479,10 @@ export const ProfileItem = (props: Props) => {
<RulesEditorViewer
open={true}
onClose={() => setRulesOpen(false)}
profileUid={uid}
profileUid={uid} // <-- Был 'uid', стал 'profileUid'
property={option?.rules ?? ""}
groupsUid={option?.groups ?? ""}
mergeUid={option?.merge ?? ""}
groupsUid={option?.groups ?? ""} // <-- Добавлен недостающий пропс
mergeUid={option?.merge ?? ""} // <-- Добавлен недостающий пропс
onSave={onSave}
/>
)}
@@ -489,7 +491,7 @@ export const ProfileItem = (props: Props) => {
<ProxiesEditorViewer
open={true}
onClose={() => setProxiesOpen(false)}
profileUid={uid}
profileUid={uid} // <-- Был 'uid', стал 'profileUid'
property={option?.proxies ?? ""}
onSave={onSave}
/>
@@ -499,10 +501,10 @@ export const ProfileItem = (props: Props) => {
<GroupsEditorViewer
open={true}
onClose={() => setGroupsOpen(false)}
profileUid={uid}
profileUid={uid} // <-- Был 'uid', стал 'profileUid'
property={option?.groups ?? ""}
proxiesUid={option?.proxies ?? ""}
mergeUid={option?.merge ?? ""}
proxiesUid={option?.proxies ?? ""} // <-- Добавлен недостающий пропс
mergeUid={option?.merge ?? ""} // <-- Добавлен недостающий пропс
onSave={onSave}
/>
)}
@@ -511,7 +513,7 @@ export const ProfileItem = (props: Props) => {
open={confirmOpen}
onOpenChange={setConfirmOpen}
onConfirm={onDelete}
title={t("Delete Profile", { name: truncatedName })}
title={t("Delete Profile", { name })}
description={t("This action cannot be undone.")}
/>
</div>

View File

@@ -12,9 +12,7 @@ import {
createProfile,
patchProfile,
importProfile,
enhanceProfiles,
createProfileFromShareLink,
getProfiles,
enhanceProfiles, createProfileFromShareLink,
} from "@/services/cmds";
import { useProfiles } from "@/hooks/use-profiles";
import { showNotice } from "@/services/noticeService";
@@ -66,7 +64,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [openType, setOpenType] = useState<"new" | "edit">("new");
const { profiles, patchProfiles } = useProfiles();
const { profiles } = useProfiles();
const fileDataRef = useRef<string | null>(null);
const [showAdvanced, setShowAdvanced] = useState(false);
@@ -140,9 +138,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
setIsCheckingUrl(true);
const handler = setTimeout(() => {
const isValid = /^(https?|vmess|vless|ss|socks|trojan):\/\//.test(
importUrl,
);
const isValid = /^(https?|vmess|vless|ss|socks|trojan):\/\//.test(importUrl);
setIsUrlValid(isValid);
setIsCheckingUrl(false);
}, 500);
@@ -169,35 +165,23 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
await enhanceProfiles();
setOpen(false);
} catch (err: any) {
const errorMessage =
typeof err === "string" ? err : err.message || String(err);
const errorMessage = typeof err === 'string' ? err : (err.message || String(err));
const lowerErrorMessage = errorMessage.toLowerCase();
if (
lowerErrorMessage.includes("device") ||
lowerErrorMessage.includes("устройств")
) {
window.dispatchEvent(
new CustomEvent("show-hwid-error", { detail: errorMessage }),
);
if (lowerErrorMessage.includes('device') || lowerErrorMessage.includes('устройств')) {
window.dispatchEvent(new CustomEvent('show-hwid-error', { detail: errorMessage }));
} else if (!isShareLink && errorMessage.includes("failed to fetch")) {
showNotice("info", t("Import failed, retrying with Clash proxy..."));
try {
await importProfile(importUrl, {
with_proxy: false,
self_proxy: true,
});
showNotice("success", t("Profile Imported with Clash proxy"));
props.onChange();
await enhanceProfiles();
setOpen(false);
} catch (retryErr: any) {
showNotice(
"error",
`${t("Import failed even with Clash proxy")}: ${retryErr?.message || retryErr.toString()}`,
);
}
showNotice("info", t("Import failed, retrying with Clash proxy..."));
try {
await importProfile(importUrl, { with_proxy: false, self_proxy: true });
showNotice("success", t("Profile Imported with Clash proxy"));
props.onChange();
await enhanceProfiles();
setOpen(false);
} catch (retryErr: any) {
showNotice("error", `${t("Import failed even with Clash proxy")}: ${retryErr?.message || retryErr.toString()}`);
}
} else {
showNotice("error", errorMessage);
showNotice("error", errorMessage);
}
} finally {
setIsImporting(false);
@@ -211,81 +195,33 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
const handleSaveAdvanced = useLockFn(
handleSubmit(async (formData) => {
const form = { ...formData, url: formData.url || importUrl } as Partial<IProfileItem>;
const form = { ...formData, url: formData.url || importUrl };
setLoading(true);
try {
if (!form.type) throw new Error("`Type` should not be null");
if (form.type === "remote" && !form.url)
throw new Error("The URL should not be null");
if (form.option?.update_interval)
form.option.update_interval = +form.option.update_interval;
else delete form.option?.update_interval;
if (form.option?.user_agent === "") delete form.option.user_agent;
// Clean option fields: only send what user actually set
let option = form.option ? { ...form.option } : undefined;
if (option) {
if ((option as any).update_interval != null && (option as any).update_interval !== "") {
// ensure number
(option as any).update_interval = +((option as any).update_interval as any);
} else {
delete (option as any).update_interval;
}
if (typeof option.user_agent === "string" && option.user_agent.trim() === "") {
delete (option as any).user_agent;
}
}
const providedName = (form as any).name && String((form as any).name).trim();
const providedDesc = (form as any).desc && String((form as any).desc).trim();
const item: Partial<IProfileItem> = {
...form,
// Only include name/desc when user explicitly entered them
name: providedName ? (providedName as string) : undefined,
desc: providedDesc ? (providedDesc as string) : undefined,
option,
};
const name = form.name || `${form.type} file`;
const item = { ...form, name };
const isUpdate = openType === "edit";
const wasCurrent = isUpdate && form.uid === (profiles?.current ?? "");
const isActivating =
isUpdate && form.uid === (profiles?.current ?? "");
if (openType === "new") {
// Detect newly created profile and activate it explicitly
const before = await getProfiles().catch(() => null);
const beforeUids = new Set(
(before?.items || []).map((i: any) => i?.uid).filter(Boolean),
);
await createProfile(item, fileDataRef.current);
const after = await getProfiles().catch(() => null);
const newRemoteLocal = (after?.items || []).find(
(i: any) =>
i &&
(i.type === "remote" || i.type === "local") &&
i.uid &&
!beforeUids.has(i.uid),
);
const newUid = (newRemoteLocal && newRemoteLocal.uid) as
| string
| undefined;
if (newUid) {
try {
await patchProfiles({ current: newUid });
} catch {}
}
showNotice("success", t("Profile Created Successfully"));
setOpen(false);
props.onChange(true);
return;
} else {
if (!form.uid) throw new Error("UID not found");
await patchProfile(form.uid as string, item);
showNotice("success", t("Profile Updated Successfully"));
await patchProfile(form.uid, item);
}
setOpen(false);
props.onChange(wasCurrent);
props.onChange(isActivating);
} catch (err: any) {
showNotice("error", err.message || err.toString());
} finally {
@@ -360,32 +296,25 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
</Button>
{!isUrlValid && importUrl && (
<p className="text-sm text-destructive px-1">
{t("Invalid Profile URL")}
{t("Please enter a valid URL")}
</p>
)}
</div>
{/^(vmess|vless|ss|socks|trojan):\/\//.test(importUrl) && (
<div className="space-y-2">
<div className="space-y-2">
<Label>{t("Template")}</Label>
<Select
value={selectedTemplate}
onValueChange={setSelectedTemplate}
>
<SelectTrigger>
<SelectValue placeholder="Select a template..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">
{t("Default Template")}
</SelectItem>
<SelectItem value="without_ru">
{t("Template without RU Rules")}
</SelectItem>
</SelectContent>
<Select value={selectedTemplate} onValueChange={setSelectedTemplate}>
<SelectTrigger>
<SelectValue placeholder="Select a template..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">{t("Default Template")}</SelectItem>
<SelectItem value="without_ru">{t("Template without RU Rules")}</SelectItem>
</SelectContent>
</Select>
</div>
)}
</div>
)}
<Button
variant="outline"
@@ -544,15 +473,15 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
control={control}
name="option.update_always"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between">
<FormLabel>{t("Update on Startup")}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
<FormItem className="flex flex-row items-center justify-between">
<FormLabel>{t("Update on Startup")}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField

View File

@@ -24,6 +24,7 @@ import delayManager from "@/services/delay";
import { useTranslation } from "react-i18next";
import { ScrollTopButton } from "../layout/scroll-top-button";
function throttle<T extends (...args: any[]) => any>(
func: T,
wait: number,
@@ -52,6 +53,7 @@ function throttle<T extends (...args: any[]) => any>(
};
}
interface Props {
mode: string;
}
@@ -70,6 +72,7 @@ export const ProxyGroups = memo((props: Props) => {
const scrollerRef = useRef<Element | null>(null);
const [showScrollTop, setShowScrollTop] = useState(false);
// Обработчик скролла для показа/скрытия кнопки "Наверх"
const handleScroll = useCallback(
throttle((e: any) => {

View File

@@ -26,8 +26,8 @@ import { showNotice } from "@/services/noticeService";
// Константы и интерфейсы
const VALID_CORE = [
{ name: "Mihomo", core: "koala-mihomo", chip: "Release Version" },
{ name: "Mihomo Alpha", core: "koala-mihomo-alpha", chip: "Alpha Version" },
{ name: "Mihomo", core: "verge-mihomo", chip: "Release Version" },
{ name: "Mihomo Alpha", core: "verge-mihomo-alpha", chip: "Alpha Version" },
];
export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
@@ -44,7 +44,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
close: () => setOpen(false),
}));
const { clash_core = "koala-mihomo" } = verge ?? {};
const { clash_core = "verge-mihomo" } = verge ?? {};
const onCoreChange = useLockFn(async (core: string) => {
if (core === clash_core) return;

View File

@@ -58,23 +58,22 @@ const DEFAULT_DNS_CONFIG = {
],
"default-nameserver": [
"system",
"223.6.6.6",
"8.8.8.8",
"1.1.1.1",
"2400:3200::1",
"2001:4860:4860::8888",
],
nameserver: [
"8.8.8.8",
"https://doh.pub/dns-query",
"https://dns.google/dns-query",
"https://cloudflare-dns.com/dns-query",
"https://dns.alidns.com/dns-query",
],
fallback: [],
"nameserver-policy": {},
"proxy-server-nameserver": [
"https://doh.pub/dns-query",
"https://dns.google/dns-query",
"https://cloudflare-dns.com/dns-query",
"tls://1.1.1.1",
"https://dns.alidns.com/dns-query",
"tls://223.5.5.5",
],
"direct-nameserver": [],
"direct-nameserver-follow-policy": false,

Some files were not shown because too many files have changed in this diff Show More