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: github: clash-verge-rev
[
"https://t.me/tribute/app?startapp=dtfk",
"https://t.me/tribute/app?startapp=dtLE",
]

View File

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

View File

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

View File

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

View File

@@ -10,9 +10,6 @@ on:
jobs: jobs:
rustfmt: rustfmt:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
CARGO_NET_RETRY: "5"
CARGO_HTTP_CHECK_REVOKE: "false"
steps: steps:
- uses: actions/checkout@v4 - 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> <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** > :warning: **Warning**
If you get a notification that the application is corrupted when you run it on macOS, run this command:<br> 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 ### 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_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.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_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.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_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> <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) ### Windows (Win7 is no longer supported)
#### Normal version (recommended) #### 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> <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 i
pnpm run prebuild ${{ matrix.target }} 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 - name: Tauri build
id: build id: build
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
@@ -191,12 +186,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 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: with:
tauriScript: pnpm tauriScript: pnpm
args: --target ${{ matrix.target }} args: --target ${{ matrix.target }}
@@ -205,9 +194,10 @@ jobs:
if: runner.os == 'Windows' if: runner.os == 'Windows'
shell: pwsh shell: pwsh
run: | run: |
$version = ${{steps.build.outputs.appVersion}}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe" $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) { foreach ($file in $files) {
$newName = $file.Name -replace "_${{steps.build.outputs.appVersion}}_", "_" $newName = $file.Name -replace "_${version}_", "_"
Rename-Item $file.FullName $newName Rename-Item $file.FullName $newName
} }
@@ -216,11 +206,11 @@ jobs:
shell: bash shell: bash
run: | run: |
TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle" TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle"
if [ ! -d "$TARGET_DIR" ]; then if [ ! -d "$TARGET_DIR" ]; then
exit 1 exit 1
fi fi
find "$TARGET_DIR" -type f \( -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" \) -print0 | while IFS= read -r -d '' old_path; do 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") dir_path=$(dirname "$old_path")
old_filename=$(basename "$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/rpm/*.rpm
src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*setup* 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/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: release-for-linux-arm:
name: Release Build for Linux ARM name: Release Build for Linux ARM
@@ -364,11 +352,11 @@ jobs:
shell: bash shell: bash
run: | run: |
TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle" TARGET_DIR="src-tauri/target/${{ matrix.target }}/release/bundle"
if [ ! -d "$TARGET_DIR" ]; then if [ ! -d "$TARGET_DIR" ]; then
exit 1 exit 1
fi fi
find "$TARGET_DIR" -type f \( -name "*.dmg" -o -name "*.deb" -o -name "*.rpm" \) -print0 | while IFS= read -r -d '' old_path; do 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") dir_path=$(dirname "$old_path")
old_filename=$(basename "$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." UPDATE_LOGS="More new features are now supported. Check for detailed changelog soon."
else else
echo "Using found update logs" echo "Using found update logs"
UPDATE_LOGS=$(echo "$UPDATE_LOGS" | sed 's/^## \(v.*\)/\*\1\*/')
fi fi
cat > release.txt << EOF cat > release.txt << EOF
Вышло обновление! Вышло обновление!
$UPDATE_LOGS $UPDATE_LOGS
[Ссылка на релиз](https://github.com/coolcoala/clash-verge-rev-lite/releases/latest) [Ссылка на релиз](https://github.com/coolcoala/clash-verge-rev-lite/releases/latest)
EOF EOF
- name: notify to channel - name: notify to channel
@@ -593,7 +580,6 @@ jobs:
to: ${{ secrets.TELEGRAM_TO_CHANNEL }} to: ${{ secrets.TELEGRAM_TO_CHANNEL }}
token: ${{ secrets.TELEGRAM_TOKEN }} token: ${{ secrets.TELEGRAM_TOKEN }}
message_file: release.txt message_file: release.txt
format: markdown
- name: notify to group - name: notify to group
uses: appleboy/telegram-action@master uses: appleboy/telegram-action@master
@@ -602,3 +588,4 @@ jobs:
token: ${{ secrets.TELEGRAM_TOKEN }} token: ${{ secrets.TELEGRAM_TOKEN }}
message_file: release.txt message_file: release.txt
format: markdown format: markdown

1
.gitignore vendored
View File

@@ -10,4 +10,3 @@ scripts/_env.sh
.tool-versions .tool-versions
.idea .idea
.old .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 ## v0.2.4
- added auto-scaling and scaling via key combination - added auto-scaling and scaling via key combination
@@ -43,6 +18,7 @@
- corrected side menu in compressed window - corrected side menu in compressed window
- added check at the main toggle switch, now it cannot be enabled if there are no profiles. - added check at the main toggle switch, now it cannot be enabled if there are no profiles.
## v0.2.1 ## v0.2.1
- added headers "announce-url", "update-always" - 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() { export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>( const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
undefined,
);
React.useEffect(() => { React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => { const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}; }
mql.addEventListener("change", onChange); mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange); return () => mql.removeEventListener("change", onChange)
}, []); }, [])
return !!isMobile; return !!isMobile
} }

View File

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

49
pnpm-lock.yaml generated
View File

@@ -92,9 +92,6 @@ importers:
'@tauri-apps/plugin-clipboard-manager': '@tauri-apps/plugin-clipboard-manager':
specifier: ^2.2.2 specifier: ^2.2.2
version: 2.3.0 version: 2.3.0
'@tauri-apps/plugin-deep-link':
specifier: ~2
version: 2.4.1
'@tauri-apps/plugin-dialog': '@tauri-apps/plugin-dialog':
specifier: ^2.2.2 specifier: ^2.2.2
version: 2.3.0 version: 2.3.0
@@ -155,9 +152,6 @@ importers:
foxact: foxact:
specifier: ^0.2.45 specifier: ^0.2.45
version: 0.2.49(react@19.1.0) 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: glob:
specifier: ^11.0.2 specifier: ^11.0.2
version: 11.0.3 version: 11.0.3
@@ -2205,9 +2199,6 @@ packages:
'@tauri-apps/plugin-clipboard-manager@2.3.0': '@tauri-apps/plugin-clipboard-manager@2.3.0':
resolution: {integrity: sha512-81NOBA2P+OTY8RLkBwyl9ZR/0CeggLub4F6zxcxUIfFOAqtky7J61+K/MkH2SC1FMxNBxrX0swDuKvkjkHadlA==} 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': '@tauri-apps/plugin-dialog@2.3.0':
resolution: {integrity: sha512-ylSBvYYShpGlKKh732ZuaHyJ5Ie1JR71QCXewCtsRLqGdc8Is4xWdz6t43rzXyvkItM9syNPMvFVcvjgEy+/GA==} resolution: {integrity: sha512-ylSBvYYShpGlKKh732ZuaHyJ5Ie1JR71QCXewCtsRLqGdc8Is4xWdz6t43rzXyvkItM9syNPMvFVcvjgEy+/GA==}
@@ -2757,20 +2748,6 @@ packages:
fraction.js@4.3.7: fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} 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: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -3218,12 +3195,6 @@ packages:
peerDependencies: peerDependencies:
monaco-editor: '>=0.36' 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: mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -5808,10 +5779,6 @@ snapshots:
dependencies: dependencies:
'@tauri-apps/api': 2.6.0 '@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': '@tauri-apps/plugin-dialog@2.3.0':
dependencies: dependencies:
'@tauri-apps/api': 2.6.0 '@tauri-apps/api': 2.6.0
@@ -6394,16 +6361,6 @@ snapshots:
fraction.js@4.3.7: {} 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: fsevents@2.3.3:
optional: true optional: true
@@ -6957,12 +6914,6 @@ snapshots:
vscode-uri: 3.1.0 vscode-uri: 3.1.0
yaml: 2.7.1 yaml: 2.7.1
motion-dom@12.23.12:
dependencies:
motion-utils: 12.23.6
motion-utils@12.23.6: {}
mri@1.2.0: {} mri@1.2.0: {}
ms@2.1.3: {} ms@2.1.3: {}

View File

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

View File

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

View File

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

214
src-tauri/Cargo.lock generated
View File

@@ -256,22 +256,6 @@ dependencies = [
"pin-project-lite", "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]] [[package]]
name = "async-executor" name = "async-executor"
version = "1.13.2" version = "1.13.2"
@@ -1075,6 +1059,80 @@ dependencies = [
"inout", "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]] [[package]]
name = "clipboard-win" name = "clipboard-win"
version = "5.4.0" version = "5.4.0"
@@ -3564,81 +3622,6 @@ dependencies = [
"unicode-segmentation", "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]] [[package]]
name = "kuchikiki" name = "kuchikiki"
version = "0.8.8-speedreader" version = "0.8.8-speedreader"
@@ -3904,12 +3887,11 @@ dependencies = [
[[package]] [[package]]
name = "machine-uid" name = "machine-uid"
version = "0.5.3" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4506fa0abb0a2ea93f5862f55973da0a662d2ad0e98f337a1c5aac657f0892" checksum = "1f1595709b0a7386bcd56ba34d250d626e5503917d05d32cdccddcd68603e212"
dependencies = [ dependencies = [
"libc", "winreg 0.6.2",
"winreg 0.52.0",
] ]
[[package]] [[package]]
@@ -5869,7 +5851,6 @@ version = "0.12.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
dependencies = [ dependencies = [
"async-compression",
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
"cookie", "cookie",
@@ -6865,9 +6846,9 @@ dependencies = [
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.36.1" version = "0.35.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d" checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e"
dependencies = [ dependencies = [
"libc", "libc",
"memchr", "memchr",
@@ -7163,9 +7144,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-deep-link" name = "tauri-plugin-deep-link"
version = "2.4.1" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fec67f32d7a06d80bd3dc009fdb678c35a66116d9cb8cd2bb32e406c2b5bbd2" checksum = "ab261eb006db10ab478e3fbb5a4e2692df3f7eb3e28300ee2b64428979167ed0"
dependencies = [ dependencies = [
"dunce", "dunce",
"rust-ini", "rust-ini",
@@ -7313,22 +7294,6 @@ dependencies = [
"tokio", "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]] [[package]]
name = "tauri-plugin-updater" name = "tauri-plugin-updater"
version = "2.9.0" version = "2.9.0"
@@ -9358,6 +9323,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winreg"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.10.1" version = "0.10.1"
@@ -9594,9 +9568,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus" name = "zbus"
version = "5.9.0" version = "5.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad" checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68"
dependencies = [ dependencies = [
"async-broadcast", "async-broadcast",
"async-executor", "async-executor",
@@ -9628,9 +9602,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus_macros" name = "zbus_macros"
version = "5.9.0" version = "5.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659" checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a"
dependencies = [ dependencies = [
"proc-macro-crate 3.3.0", "proc-macro-crate 3.3.0",
"proc-macro2", "proc-macro2",

View File

@@ -1,16 +1,16 @@
[package] [package]
name = "koala-clash" name = "clash-verge"
version = "0.2.6" version = "0.2.4"
description = "koala clash" description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda", "coolcoala"] authors = ["zzzgydi", "wonfen", "MystiPanda", "coolcoala"]
license = "GPL-3.0-only" license = "GPL-3.0-only"
repository = "https://github.com/coolcoala/clash-verge-rev-lite.git" repository = "https://github.com/coolcoala/clash-verge-rev-lite.git"
default-run = "koala-clash" default-run = "clash-verge"
edition = "2021" edition = "2021"
build = "build.rs" build = "build.rs"
[package.metadata.bundle] [package.metadata.bundle]
identifier = "io.github.koala-clash" identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.3.0", features = [] } tauri-build = { version = "2.3.0", features = [] }
@@ -18,7 +18,7 @@ tauri-build = { version = "2.3.0", features = [] }
[dependencies] [dependencies]
url = "2.5.4" url = "2.5.4"
os_info = "3.0" os_info = "3.0"
machine-uid = "0.5.3" machine-uid = "0.2"
warp = "0.3.7" warp = "0.3.7"
anyhow = "1.0.98" anyhow = "1.0.98"
dirs = "6.0" dirs = "6.0"
@@ -28,7 +28,7 @@ dunce = "1.0.5"
log4rs = "1.3.0" log4rs = "1.3.0"
nanoid = "0.4" nanoid = "0.4"
chrono = "0.4.41" chrono = "0.4.41"
sysinfo = "0.36.1" sysinfo = "0.35.2"
boa_engine = "0.20.0" boa_engine = "0.20.0"
serde_json = "1.0.140" serde_json = "1.0.140"
serde_yaml = "0.9.34-deprecated" serde_yaml = "0.9.34-deprecated"
@@ -45,7 +45,7 @@ tokio = { version = "1.45.1", features = [
"sync", "sync",
] } ] }
serde = { version = "1.0.219", features = ["derive"] } 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" regex = "1.11.1"
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" } sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs" }
image = "0.25.6" image = "0.25.6"
@@ -63,6 +63,7 @@ tauri-plugin-dialog = "2.3.0"
tauri-plugin-fs = "2.4.0" tauri-plugin-fs = "2.4.0"
tauri-plugin-process = "2.3.0" tauri-plugin-process = "2.3.0"
tauri-plugin-clipboard-manager = "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-devtools = "2.0.0"
tauri-plugin-window-state = "2.3.0" tauri-plugin-window-state = "2.3.0"
zip = "4.2.0" zip = "4.2.0"
@@ -84,7 +85,6 @@ sha2 = "0.10.9"
hex = "0.4.3" hex = "0.4.3"
scopeguard = "1.2.0" scopeguard = "1.2.0"
tauri-plugin-notification = "2.3.0" tauri-plugin-notification = "2.3.0"
tauri-plugin-deep-link = "2"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
runas = "=1.2.0" runas = "=1.2.0"
@@ -110,7 +110,6 @@ users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.5.0" tauri-plugin-autostart = "2.5.0"
tauri-plugin-global-shortcut = "2.3.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" tauri-plugin-updater = "2.9.0"
[features] [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 #!/bin/bash
chmod +x /usr/bin/install-service chmod +x /usr/bin/install-service
chmod +x /usr/bin/uninstall-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/> <false/>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>io.github.koala-clash</string> <string>io.github.clash-verge-rev.clash-verge-rev</string>
</array> </array>
<key>com.apple.security.inherit</key> <key>com.apple.security.inherit</key>
<true/> <true/>

View File

@@ -427,52 +427,52 @@ Function .onInit
!endif !endif
FunctionEnd FunctionEnd
!macro CheckAllKoalaProcesses !macro CheckAllVergeProcesses
; Check if koala-clash-service.exe is running ; Check if clash-verge-service.exe is running
!if "${INSTALLMODE}" == "currentUser" !if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::FindProcessCurrentUser "koala-clash-service.exe" nsis_tauri_utils::FindProcessCurrentUser "clash-verge-service.exe"
!else !else
nsis_tauri_utils::FindProcess "koala-clash-service.exe" nsis_tauri_utils::FindProcess "clash-verge-service.exe"
!endif !endif
Pop $R0 Pop $R0
${If} $R0 = 0 ${If} $R0 = 0
DetailPrint "Kill koala-clash-service.exe..." DetailPrint "Kill clash-verge-service.exe..."
!if "${INSTALLMODE}" == "currentUser" !if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "koala-clash-service.exe" nsis_tauri_utils::KillProcessCurrentUser "clash-verge-service.exe"
!else !else
nsis_tauri_utils::KillProcess "koala-clash-service.exe" nsis_tauri_utils::KillProcess "clash-verge-service.exe"
!endif !endif
${EndIf} ${EndIf}
; Check if koala-mihomo-alpha.exe is running ; Check if verge-mihomo-alpha.exe is running
!if "${INSTALLMODE}" == "currentUser" !if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::FindProcessCurrentUser "koala-mihomo-alpha.exe" nsis_tauri_utils::FindProcessCurrentUser "verge-mihomo-alpha.exe"
!else !else
nsis_tauri_utils::FindProcess "koala-mihomo-alpha.exe" nsis_tauri_utils::FindProcess "verge-mihomo-alpha.exe"
!endif !endif
Pop $R0 Pop $R0
${If} $R0 = 0 ${If} $R0 = 0
DetailPrint "Kill koala-mihomo-alpha.exe..." DetailPrint "Kill verge-mihomo-alpha.exe..."
!if "${INSTALLMODE}" == "currentUser" !if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "koala-mihomo-alpha.exe" nsis_tauri_utils::KillProcessCurrentUser "verge-mihomo-alpha.exe"
!else !else
nsis_tauri_utils::KillProcess "koala-mihomo-alpha.exe" nsis_tauri_utils::KillProcess "verge-mihomo-alpha.exe"
!endif !endif
${EndIf} ${EndIf}
; Check if koala-mihomo.exe is running ; Check if verge-mihomo.exe is running
!if "${INSTALLMODE}" == "currentUser" !if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::FindProcessCurrentUser "koala-mihomo.exe" nsis_tauri_utils::FindProcessCurrentUser "verge-mihomo.exe"
!else !else
nsis_tauri_utils::FindProcess "koala-mihomo.exe" nsis_tauri_utils::FindProcess "verge-mihomo.exe"
!endif !endif
Pop $R0 Pop $R0
${If} $R0 = 0 ${If} $R0 = 0
DetailPrint "Kill koala-mihomo.exe..." DetailPrint "Kill verge-mihomo.exe..."
!if "${INSTALLMODE}" == "currentUser" !if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "koala-mihomo.exe" nsis_tauri_utils::KillProcessCurrentUser "verge-mihomo.exe"
!else !else
nsis_tauri_utils::KillProcess "koala-mihomo.exe" nsis_tauri_utils::KillProcess "verge-mihomo.exe"
!endif !endif
${EndIf} ${EndIf}
@@ -509,22 +509,22 @@ FunctionEnd
${EndIf} ${EndIf}
!macroend !macroend
!macro StartKoalaService !macro StartVergeService
; Check if the service exists ; Check if the service exists
SimpleSC::ExistsService "koala_clash_service" SimpleSC::ExistsService "clash_verge_service"
Pop $0 ; 0service existsother: service not exists Pop $0 ; 0service existsother: service not exists
; Service exists ; Service exists
${If} $0 == 0 ${If} $0 == 0
Push $0 Push $0
; Check if the service is running ; 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 $0 ; returns an errorcode (<>0) otherwise success (0)
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running) Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
${If} $0 == 0 ${If} $0 == 0
Push $0 Push $0
${If} $1 == 0 ${If} $1 == 0
DetailPrint "Restart Koala Clash Service..." DetailPrint "Restart Clash Verge Service..."
SimpleSC::StartService "koala_clash_service" "" 30 SimpleSC::StartService "clash_verge_service" "" 30
${EndIf} ${EndIf}
${ElseIf} $0 != 0 ${ElseIf} $0 != 0
Push $0 Push $0
@@ -535,35 +535,35 @@ FunctionEnd
${EndIf} ${EndIf}
!macroend !macroend
!macro RemoveKoalaService !macro RemoveVergeService
; Check if the service exists ; Check if the service exists
SimpleSC::ExistsService "koala_clash_service" SimpleSC::ExistsService "clash_verge_service"
Pop $0 ; 0service existsother: service not exists Pop $0 ; 0service existsother: service not exists
; Service exists ; Service exists
${If} $0 == 0 ${If} $0 == 0
Push $0 Push $0
; Check if the service is running ; 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 $0 ; returns an errorcode (<>0) otherwise success (0)
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running) Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
${If} $0 == 0 ${If} $0 == 0
Push $0 Push $0
${If} $1 == 1 ${If} $1 == 1
DetailPrint "Stop Koala Clash Service..." DetailPrint "Stop Clash Verge Service..."
SimpleSC::StopService "koala_clash_service" 1 30 SimpleSC::StopService "clash_verge_service" 1 30
Pop $0 ; returns an errorcode (<>0) otherwise success (0) Pop $0 ; returns an errorcode (<>0) otherwise success (0)
${If} $0 == 0 ${If} $0 == 0
DetailPrint "Removing Koala Clash Service..." DetailPrint "Removing Clash Verge Service..."
SimpleSC::RemoveService "koala_clash_service" SimpleSC::RemoveService "clash_verge_service"
${ElseIf} $0 != 0 ${ElseIf} $0 != 0
Push $0 Push $0
SimpleSC::GetErrorMessage SimpleSC::GetErrorMessage
Pop $0 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} ${EndIf}
${ElseIf} $1 == 0 ${ElseIf} $1 == 0
DetailPrint "Removing Koala Clash Service..." DetailPrint "Removing Clash Verge Service..."
SimpleSC::RemoveService "koala_clash_service" SimpleSC::RemoveService "clash_verge_service"
${EndIf} ${EndIf}
${ElseIf} $0 != 0 ${ElseIf} $0 != 0
Push $0 Push $0
@@ -764,7 +764,7 @@ Section Install
SetOutPath $INSTDIR SetOutPath $INSTDIR
nsExec::Exec 'netsh int tcp res' nsExec::Exec 'netsh int tcp res'
!insertmacro CheckIfAppIsRunning !insertmacro CheckIfAppIsRunning
!insertmacro CheckAllKoalaProcesses !insertmacro CheckAllVergeProcesses
; 清理自启动注册表项 ; 清理自启动注册表项
DetailPrint "Cleaning auto-launch registry entries..." DetailPrint "Cleaning auto-launch registry entries..."
@@ -772,32 +772,32 @@ Section Install
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run" StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
SetRegView 64 SetRegView 64
; 清理旧版本的注册表项 (Koala Clash) ; 清理旧版本的注册表项 (Clash Verge)
ReadRegStr $R2 HKCU "$R1" "Koala Clash" ReadRegStr $R2 HKCU "$R1" "Clash Verge"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKCU "$R1" "Koala Clash" DeleteRegValue HKCU "$R1" "Clash Verge"
${EndIf} ${EndIf}
ReadRegStr $R2 HKLM "$R1" "Koala Clash" ReadRegStr $R2 HKLM "$R1" "Clash Verge"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKLM "$R1" "Koala Clash" DeleteRegValue HKLM "$R1" "Clash Verge"
${EndIf} ${EndIf}
; 清理新版本的注册表项 (koala-clash) ; 清理新版本的注册表项 (clash-verge)
ReadRegStr $R2 HKCU "$R1" "koala-clash" ReadRegStr $R2 HKCU "$R1" "clash-verge"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKCU "$R1" "koala-clash" DeleteRegValue HKCU "$R1" "clash-verge"
${EndIf} ${EndIf}
ReadRegStr $R2 HKLM "$R1" "koala-clash" ReadRegStr $R2 HKLM "$R1" "clash-verge"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKLM "$R1" "koala-clash" DeleteRegValue HKLM "$R1" "clash-verge"
${EndIf} ${EndIf}
; Delete old files before installation ; Delete old files before installation
; Delete koala-clash.desktop ; Delete clash-verge.desktop
IfFileExists "$INSTDIR\Koala Clash.exe" 0 +2 IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
Delete "$INSTDIR\Koala Clash.exe" Delete "$INSTDIR\Clash Verge.exe"
; Copy main executable ; Copy main executable
File "${MAINBINARYSRCPATH}" File "${MAINBINARYSRCPATH}"
@@ -815,7 +815,7 @@ Section Install
File /a "/oname={{this}}" "{{@key}}" File /a "/oname={{this}}" "{{@key}}"
{{/each}} {{/each}}
!insertmacro StartKoalaService !insertmacro StartVergeService
; Create uninstaller ; Create uninstaller
WriteUninstaller "$INSTDIR\uninstall.exe" WriteUninstaller "$INSTDIR\uninstall.exe"
@@ -918,11 +918,11 @@ FunctionEnd
Section Uninstall Section Uninstall
;删除 window-state.json 文件 ;删除 window-state.json 文件
SetShellVarContext current 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 CheckIfAppIsRunning
!insertmacro CheckAllKoalaProcesses !insertmacro CheckAllVergeProcesses
!insertmacro RemoveKoalaService !insertmacro RemoveVergeService
; 清理自启动注册表项 ; 清理自启动注册表项
DetailPrint "Cleaning auto-launch registry entries..." DetailPrint "Cleaning auto-launch registry entries..."
@@ -930,26 +930,26 @@ Section Uninstall
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run" StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
SetRegView 64 SetRegView 64
; 清理旧版本的注册表项 (Koala Clash) ; 清理旧版本的注册表项 (Clash Verge)
ReadRegStr $R2 HKCU "$R1" "Koala Clash" ReadRegStr $R2 HKCU "$R1" "Clash Verge"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKCU "$R1" "Koala Clash" DeleteRegValue HKCU "$R1" "Clash Verge"
${EndIf} ${EndIf}
ReadRegStr $R2 HKLM "$R1" "Koala Clash" ReadRegStr $R2 HKLM "$R1" "Clash Verge"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKLM "$R1" "Koala Clash" DeleteRegValue HKLM "$R1" "Clash Verge"
${EndIf} ${EndIf}
; 清理新版本的注册表项 (koala-clash) ; 清理新版本的注册表项 (clash-verge)
ReadRegStr $R2 HKCU "$R1" "koala-clash" ReadRegStr $R2 HKCU "$R1" "clash-verge"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKCU "$R1" "koala-clash" DeleteRegValue HKCU "$R1" "clash-verge"
${EndIf} ${EndIf}
ReadRegStr $R2 HKLM "$R1" "koala-clash" ReadRegStr $R2 HKLM "$R1" "clash-verge"
${If} $R2 != "" ${If} $R2 != ""
DeleteRegValue HKLM "$R1" "koala-clash" DeleteRegValue HKLM "$R1" "clash-verge"
${EndIf} ${EndIf}
; Delete the app directory and its content from disk ; Delete the app directory and its content from disk
@@ -966,9 +966,9 @@ Section Uninstall
Delete "$INSTDIR\\{{this}}" Delete "$INSTDIR\\{{this}}"
{{/each}} {{/each}}
; Delete koala-clash.desktop ; Delete clash-verge.desktop
IfFileExists "$INSTDIR\Koala Clash.exe" 0 +2 IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
Delete "$INSTDIR\Koala Clash.exe" Delete "$INSTDIR\Clash Verge.exe"
; Delete uninstaller ; Delete uninstaller
Delete "$INSTDIR\uninstall.exe" Delete "$INSTDIR\uninstall.exe"
@@ -982,20 +982,20 @@ Section Uninstall
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" !insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
!insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk" !insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk"
; 兼容旧名称快捷方式 ; 兼容旧名称快捷方式
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\koala-clash.lnk" !insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
!insertmacro UnpinShortcut "$DESKTOP\koala-clash.lnk" !insertmacro UnpinShortcut "$DESKTOP\clash-verge.lnk"
; Remove start menu shortcut ; Remove start menu shortcut
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder !insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
; 兼容旧名称快捷方式 ; 兼容旧名称快捷方式
Delete "$SMPROGRAMS\$AppStartMenuFolder\koala-clash.lnk" Delete "$SMPROGRAMS\$AppStartMenuFolder\clash-verge.lnk"
RMDir "$SMPROGRAMS\$AppStartMenuFolder" RMDir "$SMPROGRAMS\$AppStartMenuFolder"
; Remove desktop shortcuts ; Remove desktop shortcuts
Delete "$DESKTOP\${PRODUCTNAME}.lnk" Delete "$DESKTOP\${PRODUCTNAME}.lnk"
; 兼容旧名称快捷方式 ; 兼容旧名称快捷方式
Delete "$DESKTOP\koala-clash.lnk" Delete "$DESKTOP\clash-verge.lnk"
; Remove registry information for add/remove programs ; Remove registry information for add/remove programs
!if "${INSTALLMODE}" == "both" !if "${INSTALLMODE}" == "both"
@@ -1017,7 +1017,7 @@ Section Uninstall
;删除 window-state.json 文件 ;删除 window-state.json 文件
SetShellVarContext current 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 ${GetOptions} $CMDLINE "/P" $R0
IfErrors +2 0 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()) Ok(icon_path.to_string_lossy().to_string())
} else { } else {
let _ = std::fs::remove_file(&temp_path); 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已准备就绪 /// 通知UI已准备就绪
#[tauri::command] #[tauri::command]
pub fn notify_ui_ready() -> CmdResult<()> { 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(); 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(()) Ok(())
} }
/// UI加载阶段 /// UI加载阶段
#[tauri::command] #[tauri::command]
pub fn update_ui_stage(stage: String) -> CmdResult<()> { 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; use crate::utils::resolve::UiReadyStage;
@@ -230,8 +228,8 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
"ResourcesLoaded" => UiReadyStage::ResourcesLoaded, "ResourcesLoaded" => UiReadyStage::ResourcesLoaded,
"Ready" => UiReadyStage::Ready, "Ready" => UiReadyStage::Ready,
_ => { _ => {
log::warn!(target: "app", "Unknown UI loading stage: {stage}"); log::warn!(target: "app", "未知的UI加载阶段: {stage}");
return Err(format!("Unknown UI loading stage: {stage}")); return Err(format!("未知的UI加载阶段: {stage}"));
} }
}; };
@@ -242,7 +240,7 @@ pub fn update_ui_stage(stage: String) -> CmdResult<()> {
/// 重置UI就绪状态 /// 重置UI就绪状态
#[tauri::command] #[tauri::command]
pub fn reset_ui_ready_state() -> CmdResult<()> { 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(); crate::utils::resolve::reset_ui_ready();
Ok(()) Ok(())
} }

View File

@@ -7,7 +7,7 @@ use serde_yaml::Mapping;
/// get the system proxy /// get the system proxy
#[tauri::command] #[tauri::command]
pub async fn get_sys_proxy() -> CmdResult<Mapping> { 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; 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()); 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) Ok(map)
} }
/// 获取自动代理配置 /// 获取自动代理配置
#[tauri::command] #[tauri::command]
pub async fn get_auto_proxy() -> CmdResult<Mapping> { 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(); 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("enable".into(), current.enable.into());
map.insert("url".into(), current.url.clone().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) Ok(map)
} }

View File

@@ -6,14 +6,14 @@ use crate::{
utils::{dirs, help, logging::Type}, utils::{dirs, help, logging::Type},
wrap_err, 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::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration; use std::time::Duration;
use tokio::sync::{Mutex, RwLock}; use tokio::sync::{Mutex, RwLock};
use std::collections::BTreeMap;
use url::Url; 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(()); static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
@@ -30,7 +30,7 @@ async fn cleanup_processing_state(sequence: u64, reason: &str) {
info, info,
Type::Cmd, Type::Cmd,
true, true,
"{}Cleanup status, serial number: {}", "{}清理状态,序列号: {}",
reason, reason,
sequence sequence
); );
@@ -55,14 +55,14 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
match latest_result { match latest_result {
Ok(Ok(profiles)) => { Ok(Ok(profiles)) => {
logging!(info, Type::Cmd, false, "Quickly fetched profiles list successfully"); logging!(info, Type::Cmd, false, "快速获取配置列表成功");
return Ok(profiles); return Ok(profiles);
} }
Ok(Err(join_err)) => { 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(_) => { 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 { match data_result {
Ok(Ok(profiles)) => { Ok(Ok(profiles)) => {
logging!(info, Type::Cmd, false, "Fetched draft profile list successfully"); logging!(info, Type::Cmd, false, "获取draft配置列表成功");
return Ok(profiles); return Ok(profiles);
} }
Ok(Err(join_err)) => { Ok(Err(join_err)) => {
@@ -90,12 +90,12 @@ pub async fn get_profiles() -> CmdResult<IProfiles> {
error, error,
Type::Cmd, Type::Cmd,
true, true,
"Failed to obtain draft configuration task: {}", "获取draft配置任务失败: {}",
join_err join_err
); );
} }
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, warn,
Type::Cmd, Type::Cmd,
true, true,
"All attempts to obtain configuration policies failed. Trying fallback" "所有获取配置策略都失败,尝试fallback"
); );
match tokio::task::spawn_blocking(IProfiles::new).await { match tokio::task::spawn_blocking(IProfiles::new).await {
Ok(profiles) => { Ok(profiles) => {
logging!(info, Type::Cmd, true, "Fallback profiles created successfully"); logging!(info, Type::Cmd, true, "使用fallback配置成功");
Ok(profiles) Ok(profiles)
} }
Err(err) => { Err(err) => {
logging!(error, Type::Cmd, true, "Fallback profiles creation failed: {}", err); logging!(error, Type::Cmd, true, "fallback配置也失败: {}", err);
// 返回空配置避免崩溃 // 返回空配置避免崩溃
Ok(IProfiles { Ok(IProfiles {
current: None, current: None,
@@ -138,43 +138,20 @@ pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult
let profiles = Config::profiles(); let profiles = Config::profiles();
let profiles = profiles.latest(); let profiles = profiles.latest();
profiles profiles.items.as_ref()
.items
.as_ref()
.and_then(|items| items.iter().find(|item| item.url.as_deref() == Some(&url))) .and_then(|items| items.iter().find(|item| item.url.as_deref() == Some(&url)))
.and_then(|item| item.uid.clone()) .and_then(|item| item.uid.clone())
}; };
if let Some(uid) = existing_uid { if let Some(uid) = existing_uid {
logging!( logging!(info, Type::Cmd, true, "The profile with URL {} already exists (UID: {}). Running the update...", url, uid);
info,
Type::Cmd,
true,
"The profile with URL {} already exists (UID: {}). Running the update...",
url,
uid
);
update_profile(uid, option).await update_profile(uid, option).await
} else { } else {
logging!( logging!(info, Type::Cmd, true, "Profile with URL {} not found. Create a new one...", url);
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 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))
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(())
} }
} }
/// 重新排序配置文件 /// 重新排序配置文件
@@ -187,17 +164,7 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
#[tauri::command] #[tauri::command]
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult { pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
let item = wrap_err!(PrfItem::from(item, file_data).await)?; 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))
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(())
} }
/// 更新配置文件 /// 更新配置文件
@@ -209,43 +176,7 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
/// 删除配置文件 /// 删除配置文件
#[tauri::command] #[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult { pub async fn delete_profile(index: String) -> CmdResult {
let should_update; let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
{
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 _ = Config::profiles().latest().auto_cleanup(); let _ = Config::profiles().latest().auto_cleanup();
@@ -268,7 +199,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info, info,
Type::Cmd, Type::Cmd,
true, true,
"Starting to modify profiles, sequence: {}, target profile: {:?}", "开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
current_sequence, current_sequence,
target_profile target_profile
); );
@@ -285,7 +216,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info, info,
Type::Cmd, Type::Cmd,
true, true,
"Newer request detected (seq: {} < {}), abandoning current", "检测到更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence, current_sequence,
latest_sequence latest_sequence
); );
@@ -295,7 +226,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info, info,
Type::Cmd, Type::Cmd,
true, true,
"Force acquiring lock to process latest request: {}", "强制获取锁以处理最新请求: {}",
current_sequence current_sequence
); );
PROFILE_UPDATE_MUTEX.lock().await PROFILE_UPDATE_MUTEX.lock().await
@@ -308,7 +239,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info, info,
Type::Cmd, Type::Cmd,
true, true,
"After acquiring lock, found newer request (seq: {} < {}), abandoning current", "获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence, current_sequence,
latest_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(); 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 let Some(new_profile) = profiles.current.as_ref() {
if current_profile.as_ref() != Some(new_profile) { 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 = { let config_file_result = {
@@ -338,7 +269,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
} }
} }
Err(e) => { Err(e) => {
logging!(error, Type::Cmd, true, "Failed to get target profile info: {}", e); logging!(error, Type::Cmd, true, "获取目标配置信息失败: {}", e);
None None
} }
} }
@@ -351,7 +282,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
error, error,
Type::Cmd, Type::Cmd,
true, true,
"Target profile does not exist: {}", "目标配置文件不存在: {}",
file_path.display() file_path.display()
); );
handle::Handle::notice_message( handle::Handle::notice_message(
@@ -377,7 +308,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
match yaml_parse_result { match yaml_parse_result {
Ok(Ok(_)) => { Ok(Ok(_)) => {
logging!(info, Type::Cmd, true, "Target profile file syntax is correct"); logging!(info, Type::Cmd, true, "目标配置文件语法正确");
} }
Ok(Err(err)) => { Ok(Err(err)) => {
let error_msg = format!(" {err}"); let error_msg = format!(" {err}");
@@ -385,7 +316,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
error, error,
Type::Cmd, Type::Cmd,
true, true,
"YAML syntax error in target profile file: {}", "目标配置文件存在YAML语法错误:{}",
error_msg error_msg
); );
handle::Handle::notice_message( handle::Handle::notice_message(
@@ -395,7 +326,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
return Ok(false); return Ok(false);
} }
Err(join_err) => { 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); logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message( handle::Handle::notice_message(
"config_validate::yaml_parse_error", "config_validate::yaml_parse_error",
@@ -406,7 +337,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
} }
} }
Ok(Err(err)) => { 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); logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message( handle::Handle::notice_message(
"config_validate::file_read_error", "config_validate::file_read_error",
@@ -415,7 +346,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
return Ok(false); return Ok(false);
} }
Err(_) => { 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); logging!(error, Type::Cmd, true, "{}", error_msg);
handle::Handle::notice_message( handle::Handle::notice_message(
"config_validate::file_read_timeout", "config_validate::file_read_timeout",
@@ -435,7 +366,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info, info,
Type::Cmd, Type::Cmd,
true, true,
"Found newer request before core operation (seq: {} < {}), abandoning current", "在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence, current_sequence,
latest_sequence latest_sequence
); );
@@ -448,7 +379,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info, info,
Type::Cmd, Type::Cmd,
true, true,
"Set current processing profile: {}, serial number: {}", "设置当前处理profile: {}, 序列号: {}",
profile, profile,
current_sequence current_sequence
); );
@@ -459,7 +390,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info, info,
Type::Cmd, Type::Cmd,
true, true,
"Updating draft profiles, sequence: {}", "正在更新配置草稿,序列号: {}",
current_sequence current_sequence
); );
@@ -474,7 +405,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info, info,
Type::Cmd, Type::Cmd,
true, true,
"Detect updated requests before kernel interaction (sequence number: {} < {}) and abandon the current request.", "在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求",
current_sequence, current_sequence,
latest_sequence latest_sequence
); );
@@ -487,7 +418,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info, info,
Type::Cmd, Type::Cmd,
true, true,
"Starting kernel config update, sequence: {}", "开始内核配置更新,序列号: {}",
current_sequence current_sequence
); );
let update_result = tokio::time::timeout( let update_result = tokio::time::timeout(
@@ -506,7 +437,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info, info,
Type::Cmd, Type::Cmd,
true, true,
"After kernel operation, an updated request was found (sequence number: {} < {}), ignore the current result.", "内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
current_sequence, current_sequence,
latest_sequence latest_sequence
); );
@@ -518,7 +449,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info, info,
Type::Cmd, Type::Cmd,
true, true,
"Configuration update successful, serial number: {}", "配置更新成功,序列号: {}",
current_sequence current_sequence
); );
Config::profiles().apply(); Config::profiles().apply();
@@ -527,22 +458,22 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
// 强制刷新代理缓存确保profile切换后立即获取最新节点数据 // 强制刷新代理缓存确保profile切换后立即获取最新节点数据
crate::process::AsyncHandler::spawn(|| async move { crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = super::proxy::force_refresh_proxies().await { 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 { crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = Tray::global().update_tooltip() { 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() { 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() { 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, info,
Type::Cmd, Type::Cmd,
true, true,
"Sending profile change event to frontend: {}, sequence: {}", "向前端发送配置变更事件: {}, 序列号: {}",
current, current,
current_sequence current_sequence
); );
handle::Handle::notify_profile_changed(current.clone()); 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(true)
} }
Ok(Ok((false, error_msg))) => { 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(); Config::profiles().discard();
// 如果验证失败,恢复到之前的配置 // 如果验证失败,恢复到之前的配置
if let Some(prev_profile) = current_profile { if let Some(prev_profile) = current_profile {
@@ -572,7 +503,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info, info,
Type::Cmd, Type::Cmd,
true, true,
"Attempting to restore previous profile: {}", "尝试恢复到之前的配置: {}",
prev_profile prev_profile
); );
let restore_profiles = IProfiles { let restore_profiles = IProfiles {
@@ -585,17 +516,17 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
crate::process::AsyncHandler::spawn(|| async move { crate::process::AsyncHandler::spawn(|| async move {
if let Err(e) = Config::profiles().data().save_file() { 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); 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) Ok(false)
} }
@@ -604,25 +535,25 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
warn, warn,
Type::Cmd, Type::Cmd,
true, true,
"Error occurred during update: {}, sequence: {}", "更新过程发生错误: {}, 序列号: {}",
e, e,
current_sequence current_sequence
); );
Config::profiles().discard(); Config::profiles().discard();
handle::Handle::notice_message("config_validate::boot_error", e.to_string()); 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) Ok(false)
} }
Err(_) => { Err(_) => {
// 超时处理 // 超时处理
let timeout_msg = "Profile update timed out (30s), possibly due to validation or kernel communication"; let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
logging!( logging!(
error, error,
Type::Cmd, Type::Cmd,
true, true,
"{}, sequence: {}", "{}, 序列号: {}",
timeout_msg, timeout_msg,
current_sequence current_sequence
); );
@@ -633,7 +564,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
info, info,
Type::Cmd, Type::Cmd,
true, true,
"After timeout, attempting to restore previous profile: {}, sequence: {}", "超时后尝试恢复到之前的配置: {}, 序列号: {}",
prev_profile, prev_profile,
current_sequence 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); 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) Ok(false)
} }
@@ -660,7 +591,7 @@ pub async fn patch_profiles_config_by_profile_index(
_app_handle: tauri::AppHandle, _app_handle: tauri::AppHandle,
profile_index: String, profile_index: String,
) -> CmdResult<bool> { ) -> CmdResult<bool> {
logging!(info, Type::Cmd, true, "Switching profile to: {}", profile_index); logging!(info, Type::Cmd, true, "切换配置到: {}", profile_index);
let profiles = IProfiles { let profiles = IProfiles {
current: Some(profile_index), current: Some(profile_index),
@@ -689,9 +620,9 @@ pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
if update_interval_changed { if update_interval_changed {
let index_clone = index.clone(); let index_clone = index.clone();
crate::process::AsyncHandler::spawn(move || async move { 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() { if let Err(e) = crate::core::Timer::global().refresh() {
logging!(error, Type::Timer, "Failed to refresh timers: {}", e); logging!(error, Type::Timer, "刷新定时器失败: {}", e);
} else { } else {
// 刷新成功后发送自定义事件,不触发配置重载 // 刷新成功后发送自定义事件,不触发配置重载
crate::core::handle::Handle::notify_timer_updated(index_clone); 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) Ok(next_time)
} }
#[tauri::command] #[tauri::command]
pub async fn update_profiles_on_startup() -> CmdResult { pub async fn update_profiles_on_startup() -> CmdResult {
logging!( logging!(info, Type::Cmd, true, "Checking profiles for updates at startup...");
info,
Type::Cmd,
true,
"Checking profiles for updates at startup..."
);
let profiles_to_update = { let profiles_to_update = {
let profiles = Config::profiles(); let profiles = Config::profiles();
let profiles = profiles.latest(); let profiles = profiles.latest();
profiles.items.as_ref().map_or_else(Vec::new, |items| { profiles.items.as_ref()
items .map_or_else(
.iter() Vec::new,
.filter(|item| { |items| items.iter()
item.option .filter(|item| item.option.as_ref().is_some_and(|opt| opt.update_always == Some(true)))
.as_ref() .filter_map(|item| item.uid.clone())
.is_some_and(|opt| opt.update_always == Some(true)) .collect()
}) )
.filter_map(|item| item.uid.clone())
.collect()
})
}; };
if profiles_to_update.is_empty() { if profiles_to_update.is_empty() {
@@ -769,13 +693,7 @@ pub async fn update_profiles_on_startup() -> CmdResult {
return Ok(()); return Ok(());
} }
logging!( logging!(info, Type::Cmd, true, "Found profiles to update: {:?}", profiles_to_update);
info,
Type::Cmd,
true,
"Found profiles to update: {:?}",
profiles_to_update
);
let mut update_futures = Vec::new(); let mut update_futures = Vec::new();
for uid in profiles_to_update { 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; let results = futures::future::join_all(update_futures).await;
if results.iter().any(|res| res.is_ok()) { if results.iter().any(|res| res.is_ok()) {
logging!( logging!(info, Type::Cmd, true, "The startup update is complete, restart the kernel...");
info, CoreManager::global().update_config().await.map_err(|e| e.to_string())?;
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(); handle::Handle::refresh_clash();
} else { } else {
logging!( logging!(warn, Type::Cmd, true, "All updates completed with errors on startup.");
warn,
Type::Cmd,
true,
"All updates completed with errors on startup."
);
} }
Ok(()) Ok(())
@@ -810,6 +716,7 @@ pub async fn update_profiles_on_startup() -> CmdResult {
#[tauri::command] #[tauri::command]
pub async fn create_profile_from_share_link(link: String, template_name: String) -> CmdResult { pub async fn create_profile_from_share_link(link: String, template_name: String) -> CmdResult {
const DEFAULT_TEMPLATE: &str = r#" const DEFAULT_TEMPLATE: &str = r#"
mixed-port: 2080 mixed-port: 2080
allow-lan: true 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 parsed_url = Url::parse(&link).map_err(|e| e.to_string())?;
let scheme = parsed_url.scheme(); let scheme = parsed_url.scheme();
let proxy_name = parsed_url let proxy_name = parsed_url.fragment()
.fragment()
.map(|f| percent_decode_str(f).decode_utf8_lossy().to_string()) .map(|f| percent_decode_str(f).decode_utf8_lossy().to_string())
.unwrap_or_else(|| "Proxy from Link".to_string()); .unwrap_or_else(|| "Proxy from Link".to_string());
let mut proxy_map: BTreeMap<String, Value> = BTreeMap::new(); let mut proxy_map: BTreeMap<String, Value> = BTreeMap::new();
proxy_map.insert("name".into(), proxy_name.clone().into()); proxy_map.insert("name".into(), proxy_name.clone().into());
proxy_map.insert("type".into(), scheme.into()); proxy_map.insert("type".into(), scheme.into());
proxy_map.insert( proxy_map.insert("server".into(), parsed_url.host_str().unwrap_or_default().into());
"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("port".into(), parsed_url.port().unwrap_or(443).into());
proxy_map.insert("udp".into(), true.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" => { "security" if value == "tls" => {
proxy_map.insert("tls".into(), true.into()); proxy_map.insert("tls".into(), true.into());
} }
"flow" => { "flow" => { proxy_map.insert("flow".into(), value.to_string().into()); }
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()); }
"sni" => { "pbk" => { reality_opts.insert("public-key".into(), value.to_string().into()); }
proxy_map.insert("servername".into(), value.to_string().into()); "sid" => { reality_opts.insert("short-id".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() { if !reality_opts.is_empty() {
proxy_map.insert( proxy_map.insert("reality-opts".into(), serde_yaml::to_value(reality_opts).map_err(|e| e.to_string())?);
"reality-opts".into(),
serde_yaml::to_value(reality_opts).map_err(|e| e.to_string())?,
);
} }
} }
"ss" => { "ss" => {
@@ -1238,32 +1128,19 @@ pub async fn create_profile_from_share_link(link: String, template_name: String)
"vmess" => { "vmess" => {
if let Ok(decoded_bytes) = STANDARD.decode(parsed_url.host_str().unwrap_or_default()) { 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(json_str) = String::from_utf8(decoded_bytes) {
if let Ok(vmess_params) = if let Ok(vmess_params) = serde_json::from_str::<BTreeMap<String, Value>>(&json_str) {
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(add) = vmess_params.get("add") { if let Some(id) = vmess_params.get("id") { proxy_map.insert("uuid".into(), id.clone()); }
proxy_map.insert("server".into(), add.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(port) = vmess_params.get("port") { if let Some(ps) = vmess_params.get("ps") { proxy_map.insert("name".into(), ps.clone()); }
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())?; 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())?); proxies.push(serde_yaml::to_value(proxy_map).map_err(|e| e.to_string())?);
} }
if let Some(groups) = config if let Some(groups) = config.get_mut("proxy-groups").and_then(|v| v.as_sequence_mut()) {
.get_mut("proxy-groups")
.and_then(|v| v.as_sequence_mut())
{
for group in groups.iter_mut() { for group in groups.iter_mut() {
if let Some(mapping) = group.as_mapping_mut() { if let Some(mapping) = group.as_mapping_mut() {
if let Some(proxies_list) = if let Some(proxies_list) = mapping.get_mut("proxies").and_then(|p| p.as_sequence_mut()) {
mapping.get_mut("proxies").and_then(|p| p.as_sequence_mut())
{
let new_proxies_list: Vec<Value> = proxies_list let new_proxies_list: Vec<Value> = proxies_list
.iter() .iter()
.map(|p| { .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 new_yaml_content = serde_yaml::to_string(&config).map_err(|e| e.to_string())?;
let item = PrfItem::from_local( let item = PrfItem::from_local(proxy_name, "Created from share link".into(), Some(new_yaml_content), None)
proxy_name, .map_err(|e| e.to_string())?;
"Created from share link".into(),
Some(new_yaml_content),
None,
)
.map_err(|e| e.to_string())?;
wrap_err!(Config::profiles().data().append_item(item)) 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.proxies = Box::new(proxies);
state.need_refresh = false; state.need_refresh = false;
} }
log::debug!(target: "app", "Proxies refreshed successfully"); log::debug!(target: "app", "proxies刷新成功");
} }
let 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 app_handle = handle::Handle::global().app_handle().unwrap();
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>(); 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?; 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(); state.last_refresh_time = Instant::now();
} }
log::debug!(target: "app", "Force refresh proxy cache completed"); log::debug!(target: "app", "强制刷新代理缓存完成");
Ok(proxies) Ok(proxies)
} }
@@ -88,7 +88,7 @@ pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
state.providers_proxies = Box::new(providers); state.providers_proxies = Box::new(providers);
state.need_refresh = false; state.need_refresh = false;
} }
log::debug!(target: "app", "providers_proxies refreshed successfully"); log::debug!(target: "app", "providers_proxies刷新成功");
} }
let 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))?; wrap_err!(fs::write(&file_path, original_content))?;
// 发送合并文件专用错误通知 // 发送合并文件专用错误通知
let result = (false, error_msg.clone()); 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(()); return Ok(());
} }
Err(e) => { 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) || (!file_path_str.ends_with(".js") && !is_script_error)
{ {
// 普通YAML错误使用YAML通知处理 // 普通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()); 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 { } 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()); let result = (false, error_msg.clone());
crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件"); crate::cmd::validate::handle_script_validation_notice(&result, "脚本文件");
} else { } 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); 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, error,
Type::Config, Type::Config,
true, true,
"[cmd config save] Error occurred during validation: {}", "[cmd配置save] 验证过程发生错误: {}",
e e
); );
// 恢复原始配置文件 // 恢复原始配置文件

View File

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

View File

@@ -42,7 +42,7 @@ impl IClashTemp {
tun.insert("enable".into(), false.into()); tun.insert("enable".into(), false.into());
tun.insert("stack".into(), "gvisor".into()); tun.insert("stack".into(), "gvisor".into());
tun.insert("auto-route".into(), true.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("auto-detect-interface".into(), true.into());
tun.insert("dns-hijack".into(), vec!["any:53"].into()); tun.insert("dns-hijack".into(), vec!["any:53"].into());
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
@@ -129,7 +129,7 @@ impl IClashTemp {
help::save_yaml( help::save_yaml(
&dirs::clash_path()?, &dirs::clash_path()?,
&self.0, &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 std::path::PathBuf;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
pub const RUNTIME_CONFIG: &str = "koala-clash.yaml"; pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "koala-clash-check.yaml"; pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
pub struct Config { pub struct Config {
clash_config: Draft<Box<IClashTemp>>, clash_config: Draft<Box<IClashTemp>>,
@@ -69,9 +69,9 @@ impl Config {
} }
// 生成运行时配置 // 生成运行时配置
if let Err(err) = Self::generate().await { 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 { } 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() { 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 { match CoreManager::global().validate_config().await {
Ok((is_valid, error_msg)) => { Ok((is_valid, error_msg)) => {
@@ -88,7 +88,7 @@ impl Config {
warn, warn,
Type::Config, Type::Config,
true, true,
"[First launch] Config validation failed, starting with minimal default config: {}", "[首次启动] 配置验证失败,使用默认最小配置启动: {}",
error_msg error_msg
); );
CoreManager::global() CoreManager::global()
@@ -96,12 +96,12 @@ impl Config {
.await?; .await?;
Some(("config_validate::boot_error", error_msg)) Some(("config_validate::boot_error", error_msg))
} else { } else {
logging!(info, Type::Config, true, "Config validation succeeded"); logging!(info, Type::Config, true, "配置验证成功");
Some(("config_validate::success", String::new())) Some(("config_validate::success", String::new()))
} }
} }
Err(err) => { Err(err) => {
logging!(warn, Type::Config, true, "Validation process execution failed: {}", err); logging!(warn, Type::Config, true, "验证进程执行失败: {}", err);
CoreManager::global() CoreManager::global()
.use_default_config("config_validate::process_terminated", "") .use_default_config("config_validate::process_terminated", "")
.await?; .await?;
@@ -109,7 +109,7 @@ impl Config {
} }
} }
} else { } else {
logging!(warn, Type::Config, true, "Failed to generate config file; using default config"); logging!(warn, Type::Config, true, "生成配置文件失败,使用默认配置");
CoreManager::global() CoreManager::global()
.use_default_config("config_validate::error", "") .use_default_config("config_validate::error", "")
.await?; .await?;
@@ -141,7 +141,7 @@ impl Config {
.as_ref() .as_ref()
.ok_or(anyhow!("failed to get runtime config"))?; .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) Ok(path)
} }

View File

@@ -19,11 +19,11 @@ macro_rules! draft_define {
impl Draft<Box<$id>> { impl Draft<Box<$id>> {
#[allow(unused)] #[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) 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| { MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() { if inner.1.is_none() {
&mut inner.0 &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| { MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() { if inner.1.is_none() {
inner.1 = Some(inner.0.clone()); inner.1 = Some(inner.0.clone());

View File

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

View File

@@ -66,7 +66,7 @@ impl IProfiles {
help::save_yaml( help::save_yaml(
&dirs::profiles_path()?, &dirs::profiles_path()?,
self, 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}\""))?; .with_context(|| format!("failed to write to file \"{file}\""))?;
} }
if item.itype == Some("remote".to_string()) || item.itype == Some("local".to_string()) { if self.current.is_none()
// Always switch current to the newly created remote/local profile && (item.itype == Some("remote".to_string()) || item.itype == Some("local".to_string()))
self.current = uid.clone(); {
self.current = uid;
} }
if self.items.is_none() { if self.items.is_none() {
@@ -535,7 +536,7 @@ impl IProfiles {
if Self::is_profile_file(file_name) { if Self::is_profile_file(file_name) {
// 检查是否为全局扩展文件 // 检查是否为全局扩展文件
if protected_files.contains(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; continue;
} }
@@ -544,11 +545,11 @@ impl IProfiles {
match std::fs::remove_file(&path) { match std::fs::remove_file(&path) {
Ok(_) => { Ok(_) => {
deleted_files.push(file_name.to_string()); 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) => { Err(e) => {
failed_deletions.push(format!("{file_name}: {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() { if !result.deleted_files.is_empty() {
log::info!( log::info!(
target: "app", target: "app",
"Auto cleanup completed, deleted {} redundant files", "自动清理完成,删除了 {} 个冗余文件",
result.deleted_files.len() result.deleted_files.len()
); );
} }
Ok(()) Ok(())
} }
Err(e) => { Err(e) => {
log::warn!(target: "app", "Auto cleanup failed: {e}"); log::warn!(target: "app", "自动清理失败: {e}");
Ok(()) Ok(())
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,7 +78,7 @@ struct QueryRequest {
response_tx: oneshot::Sender<Autoproxy>, response_tx: oneshot::Sender<Autoproxy>,
} }
// Configuration structure moved to external // 配置结构体移到外部
struct ProxyConfig { struct ProxyConfig {
sys_enabled: bool, sys_enabled: bool,
pac_enabled: bool, pac_enabled: bool,
@@ -106,59 +106,59 @@ impl EventDrivenProxyManager {
} }
} }
/// Get automatic proxy configuration (cached) /// 获取自动代理配置(缓存)
pub fn get_auto_proxy_cached(&self) -> Autoproxy { pub fn get_auto_proxy_cached(&self) -> Autoproxy {
self.state.read().auto_proxy.clone() self.state.read().auto_proxy.clone()
} }
/// Asynchronously get the latest automatic proxy configuration /// 异步获取最新的自动代理配置
pub async fn get_auto_proxy_async(&self) -> Autoproxy { pub async fn get_auto_proxy_async(&self) -> Autoproxy {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
let query = QueryRequest { response_tx: tx }; let query = QueryRequest { response_tx: tx };
if self.query_sender.send(query).is_err() { 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(); return self.get_auto_proxy_cached();
} }
match timeout(Duration::from_secs(5), rx).await { match timeout(Duration::from_secs(5), rx).await {
Ok(Ok(result)) => result, Ok(Ok(result)) => result,
_ => { _ => {
log::warn!(target: "app", "Query timed out, returning cached data"); log::warn!(target: "app", "查询超时,返回缓存数据");
self.get_auto_proxy_cached() self.get_auto_proxy_cached()
} }
} }
} }
/// Notify configuration changed /// 通知配置变更
pub fn notify_config_changed(&self) { pub fn notify_config_changed(&self) {
self.send_event(ProxyEvent::ConfigChanged); self.send_event(ProxyEvent::ConfigChanged);
} }
/// Notify application started /// 通知应用启动
pub fn notify_app_started(&self) { pub fn notify_app_started(&self) {
self.send_event(ProxyEvent::AppStarted); self.send_event(ProxyEvent::AppStarted);
} }
/// Notify application stopping /// 通知应用即将关闭
#[allow(dead_code)] #[allow(dead_code)]
pub fn notify_app_stopping(&self) { pub fn notify_app_stopping(&self) {
self.send_event(ProxyEvent::AppStopping); self.send_event(ProxyEvent::AppStopping);
} }
/// Enable system proxy /// 启用系统代理
#[allow(dead_code)] #[allow(dead_code)]
pub fn enable_proxy(&self) { pub fn enable_proxy(&self) {
self.send_event(ProxyEvent::EnableProxy); self.send_event(ProxyEvent::EnableProxy);
} }
/// Disable system proxy /// 禁用系统代理
#[allow(dead_code)] #[allow(dead_code)]
pub fn disable_proxy(&self) { pub fn disable_proxy(&self) {
self.send_event(ProxyEvent::DisableProxy); self.send_event(ProxyEvent::DisableProxy);
} }
/// Force check proxy status /// 强制检查代理状态
#[allow(dead_code)] #[allow(dead_code)]
pub fn force_check(&self) { pub fn force_check(&self) {
self.send_event(ProxyEvent::ForceCheck); self.send_event(ProxyEvent::ForceCheck);
@@ -166,7 +166,7 @@ impl EventDrivenProxyManager {
fn send_event(&self, event: ProxyEvent) { fn send_event(&self, event: ProxyEvent) {
if let Err(e) = self.event_sender.send(event) { 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>, mut query_rx: mpsc::UnboundedReceiver<QueryRequest>,
) { ) {
tokio::spawn(async move { tokio::spawn(async move {
log::info!(target: "app", "Event-driven proxy manager started"); log::info!(target: "app", "事件驱动代理管理器启动");
loop { loop {
tokio::select! { tokio::select! {
event = event_rx.recv() => { event = event_rx.recv() => {
match event { match event {
Some(event) => { Some(event) => {
log::debug!(target: "app", "Handling proxy event: {event:?}"); log::debug!(target: "app", "处理代理事件: {event:?}");
Self::handle_event(&state, event).await; Self::handle_event(&state, event).await;
} }
None => { None => {
log::info!(target: "app", "Event channel closed, proxy manager stopped"); log::info!(target: "app", "事件通道关闭,代理管理器停止");
break; break;
} }
} }
@@ -199,7 +199,7 @@ impl EventDrivenProxyManager {
let _ = query.response_tx.send(result); let _ = query.response_tx.send(result);
} }
None => { None => {
log::info!(target: "app", "Query channel closed"); log::info!(target: "app", "查询通道关闭");
break; break;
} }
} }
@@ -230,7 +230,7 @@ impl EventDrivenProxyManager {
Self::initialize_proxy_state(state).await; Self::initialize_proxy_state(state).await;
} }
ProxyEvent::AppStopping => { 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>>) { 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 config = Self::get_proxy_config();
let auto_proxy = Self::get_auto_proxy_with_timeout().await; let auto_proxy = Self::get_auto_proxy_with_timeout().await;
@@ -260,11 +260,11 @@ impl EventDrivenProxyManager {
s.is_healthy = true; 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>>) { 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(); let config = Self::get_proxy_config();
@@ -288,7 +288,7 @@ impl EventDrivenProxyManager {
return; return;
} }
log::debug!(target: "app", "Checking proxy status"); log::debug!(target: "app", "检查代理状态");
if pac_enabled { if pac_enabled {
Self::check_and_restore_pac_proxy(state).await; Self::check_and_restore_pac_proxy(state).await;
@@ -306,7 +306,7 @@ impl EventDrivenProxyManager {
}); });
if !current.enable || current.url != expected.url { 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; Self::restore_pac_proxy(&expected.url).await;
sleep(Duration::from_millis(500)).await; sleep(Duration::from_millis(500)).await;
@@ -328,7 +328,7 @@ impl EventDrivenProxyManager {
}); });
if !current.enable || current.host != expected.host || current.port != expected.port { 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; Self::restore_sys_proxy(&expected).await;
sleep(Duration::from_millis(500)).await; sleep(Duration::from_millis(500)).await;
@@ -344,7 +344,7 @@ impl EventDrivenProxyManager {
} }
async fn enable_system_proxy(state: &Arc<RwLock<ProxyState>>) { 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; let pac_enabled = state.read().pac_enabled;
@@ -360,7 +360,7 @@ impl EventDrivenProxyManager {
} }
async fn disable_system_proxy(_state: &Arc<RwLock<ProxyState>>) { 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"))] #[cfg(not(target_os = "windows"))]
{ {
@@ -373,7 +373,7 @@ impl EventDrivenProxyManager {
} }
async fn switch_proxy_mode(state: &Arc<RwLock<ProxyState>>, to_pac: bool) { 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 { if to_pac {
let disabled_sys = Sysproxy::default(); let disabled_sys = Sysproxy::default();
@@ -396,7 +396,7 @@ impl EventDrivenProxyManager {
async fn get_auto_proxy_with_timeout() -> Autoproxy { async fn get_auto_proxy_with_timeout() -> Autoproxy {
let async_proxy = AsyncProxyQuery::get_auto_proxy().await; let async_proxy = AsyncProxyQuery::get_auto_proxy().await;
// Convert to compatible structure // 转换为兼容的结构
Autoproxy { Autoproxy {
enable: async_proxy.enable, enable: async_proxy.enable,
url: async_proxy.url, url: async_proxy.url,
@@ -406,7 +406,7 @@ impl EventDrivenProxyManager {
async fn get_sys_proxy_with_timeout() -> Sysproxy { async fn get_sys_proxy_with_timeout() -> Sysproxy {
let async_proxy = AsyncProxyQuery::get_system_proxy().await; let async_proxy = AsyncProxyQuery::get_system_proxy().await;
// Convert to compatible structure // 转换为兼容的结构
Sysproxy { Sysproxy {
enable: async_proxy.enable, enable: async_proxy.enable,
host: async_proxy.host, 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) fn update_state_timestamp<F>(state: &Arc<RwLock<ProxyState>>, update_fn: F)
where where
F: FnOnce(&mut ProxyState), F: FnOnce(&mut ProxyState),
@@ -534,14 +534,14 @@ impl EventDrivenProxyManager {
let binary_path = match dirs::service_path() { let binary_path = match dirs::service_path() {
Ok(path) => path, Ok(path) => path,
Err(e) => { Err(e) => {
log::error!(target: "app", "Failed to get service path: {}", e); log::error!(target: "app", "获取服务路径失败: {}", e);
return; return;
} }
}; };
let sysproxy_exe = binary_path.with_file_name("sysproxy.exe"); let sysproxy_exe = binary_path.with_file_name("sysproxy.exe");
if !sysproxy_exe.exists() { if !sysproxy_exe.exists() {
log::error!(target: "app", "sysproxy.exe does not exist"); log::error!(target: "app", "sysproxy.exe 不存在");
return; return;
} }
@@ -554,17 +554,17 @@ impl EventDrivenProxyManager {
match output { match output {
Ok(output) => { Ok(output) => {
if !output.status.success() { 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); let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() { if !stderr.is_empty() {
log::error!(target: "app", "sysproxy stderr: {}", stderr); log::error!(target: "app", "sysproxy错误输出: {}", stderr);
} }
} else { } else {
log::debug!(target: "app", "Successfully executed sysproxy command: {:?}", args); log::debug!(target: "app", "成功执行sysproxy命令: {:?}", args);
} }
} }
Err(e) => { 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_errors: Arc<RwLock<Vec<ErrorMessage>>>,
startup_completed: Arc<RwLock<bool>>, startup_completed: Arc<RwLock<bool>>,
notification_system: Arc<RwLock<Option<NotificationSystem>>>, 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 { impl Default for Handle {
@@ -270,7 +268,6 @@ impl Default for Handle {
startup_errors: Arc::new(RwLock::new(Vec::new())), startup_errors: Arc::new(RwLock::new(Vec::new())),
startup_completed: Arc::new(RwLock::new(false)), startup_completed: Arc::new(RwLock::new(false)),
notification_system: Arc::new(RwLock::new(Some(NotificationSystem::new()))), 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> { 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 app_handle = self.app_handle()?;
let window: Option<WebviewWindow> = app_handle.get_webview_window("main"); let window: Option<WebviewWindow> = app_handle.get_webview_window("main");
if window.is_none() { if window.is_none() {
@@ -418,13 +411,12 @@ impl Handle {
let status_str = status.into(); let status_str = status.into();
let msg_str = msg.into(); let msg_str = msg.into();
// If startup not completed, buffer messages (existing behavior)
if !*handle.startup_completed.read() { if !*handle.startup_completed.read() {
logging!( logging!(
info, info,
Type::Frontend, Type::Frontend,
true, true,
"Error found during startup; queued: {} - {}", "启动过程中发现错误,加入消息队列: {} - {}",
status_str, status_str,
msg_str msg_str
); );
@@ -437,23 +429,6 @@ impl Handle {
return; 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() { if handle.is_exiting() {
return; 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) { pub fn mark_startup_completed(&self) {
{ {
let mut completed = self.startup_completed.write(); let mut completed = self.startup_completed.write();
@@ -519,7 +466,7 @@ impl Handle {
info, info,
Type::Frontend, Type::Frontend,
true, true,
"Sending {} accumulated startup error messages", "发送{}条启动时累积的错误消息",
errors.len() errors.len()
); );

View File

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

View File

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

View File

@@ -63,7 +63,7 @@ impl Sysopt {
let proxy_manager = EventDrivenProxyManager::global(); let proxy_manager = EventDrivenProxyManager::global();
proxy_manager.notify_app_started(); proxy_manager.notify_app_started();
log::info!(target: "app", "Event-driven proxy guard enabled"); log::info!(target: "app", "已启用事件驱动代理守卫");
Ok(()) Ok(())
} }
@@ -193,7 +193,7 @@ impl Sysopt {
let mut autoproxy = match Autoproxy::get_auto_proxy() { let mut autoproxy = match Autoproxy::get_auto_proxy() {
Ok(ap) => ap, Ok(ap) => ap,
Err(e) => { Err(e) => {
log::warn!(target: "app", "Failed to get auto proxy config while resetting: {e}, using default config"); log::warn!(target: "app", "重置代理时获取自动代理配置失败: {e}, 使用默认配置");
Autoproxy { Autoproxy {
enable: false, enable: false,
url: "".to_string(), url: "".to_string(),
@@ -248,14 +248,14 @@ impl Sysopt {
{ {
if is_enable { if is_enable {
if let Err(e) = startup_shortcut::create_shortcut() { 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); self.try_original_autostart_method(is_enable);
} else { } else {
return Ok(()); return Ok(());
} }
} else if let Err(e) = startup_shortcut::remove_shortcut() { } 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); self.try_original_autostart_method(is_enable);
} else { } else {
return Ok(()); return Ok(());
@@ -290,11 +290,11 @@ impl Sysopt {
{ {
match startup_shortcut::is_shortcut_enabled() { match startup_shortcut::is_shortcut_enabled() {
Ok(enabled) => { Ok(enabled) => {
log::info!(target: "app", "Shortcut auto-launch state: {}", enabled); log::info!(target: "app", "快捷方式自启动状态: {}", enabled);
return Ok(enabled); return Ok(enabled);
} }
Err(e) => { 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!( logging!(
info, info,
Type::Timer, Type::Timer,
"Registered timer task count: {}", "已注册的定时任务数量: {}",
timer_map.len() timer_map.len()
); );
@@ -81,7 +81,7 @@ impl Timer {
logging!( logging!(
info, info,
Type::Timer, Type::Timer,
"Registered timer task - uid={}, interval={}min, task_id={}", "注册了定时任务 - uid={}, interval={}min, task_id={}",
uid, uid,
task.interval_minutes, task.interval_minutes,
task.task_id task.task_id
@@ -100,12 +100,7 @@ impl Timer {
let uid = item.uid.as_ref()?; let uid = item.uid.as_ref()?;
if interval > 0 && cur_timestamp - updated >= interval * 60 { if interval > 0 && cur_timestamp - updated >= interval * 60 {
logging!( logging!(info, Type::Timer, "需要立即更新的配置: uid={}", uid);
info,
Type::Timer,
"Profile requires immediate update: uid={}",
uid
);
Some(uid.clone()) Some(uid.clone())
} else { } else {
None None
@@ -121,7 +116,7 @@ impl Timer {
logging!( logging!(
info, info,
Type::Timer, Type::Timer,
"Number of profiles requiring immediate update: {}", "需要立即更新的配置数量: {}",
profiles_to_update.len() profiles_to_update.len()
); );
let timer_map = self.timer_map.read(); let timer_map = self.timer_map.read();
@@ -129,7 +124,7 @@ impl Timer {
for uid in profiles_to_update { for uid in profiles_to_update {
if let Some(task) = timer_map.get(&uid) { 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) { if let Err(e) = delay_timer.advance_task(task.task_id) {
logging!(warn, Type::Timer, "Failed to advance task {}: {}", uid, e); logging!(warn, Type::Timer, "Failed to advance task {}: {}", uid, e);
} }
@@ -242,7 +237,7 @@ impl Timer {
logging!( logging!(
debug, debug,
Type::Timer, Type::Timer,
"Found scheduled update config: uid={}, interval={}min", "找到定时更新配置: uid={}, interval={}min",
uid, uid,
interval interval
); );
@@ -256,7 +251,7 @@ impl Timer {
logging!( logging!(
debug, debug,
Type::Timer, Type::Timer,
"Generated scheduled update config count: {}", "生成的定时更新配置数量: {}",
new_map.len() new_map.len()
); );
new_map new_map
@@ -272,7 +267,7 @@ impl Timer {
logging!( logging!(
debug, debug,
Type::Timer, Type::Timer,
"Current timer_map size: {}", "当前 timer_map 大小: {}",
timer_map.len() timer_map.len()
); );
@@ -284,7 +279,7 @@ impl Timer {
logging!( logging!(
debug, debug,
Type::Timer, Type::Timer,
"Timer task interval changed: uid={}, old={}, new={}", "定时任务间隔变更: uid={}, ={}, ={}",
uid, uid,
task.interval_minutes, task.interval_minutes,
interval interval
@@ -293,12 +288,12 @@ impl Timer {
} }
None => { None => {
// Task no longer needed // 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)); diff_map.insert(uid.clone(), DiffFlag::Del(task.task_id));
} }
_ => { _ => {
// Task exists with same interval, no change needed // 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!( logging!(
debug, debug,
Type::Timer, Type::Timer,
"Added timer task: uid={}, interval={}min", "新增定时任务: uid={}, interval={}min",
uid, uid,
interval interval
); );
@@ -325,13 +320,7 @@ impl Timer {
*self.timer_count.lock() = next_id; *self.timer_count.lock() = next_id;
} }
logging!(debug, Type::Timer, "Number of scheduled task changes: {}", diff_map.len()); logging!(debug, Type::Timer, "定时任务变更数量: {}", diff_map.len());
logging!(
debug,
Type::Timer,
"Number of timer task changes: {}",
diff_map.len()
);
diff_map diff_map
} }
@@ -374,18 +363,13 @@ impl Timer {
/// Get next update time for a profile /// Get next update time for a profile
pub fn get_next_update_time(&self, uid: &str) -> Option<i64> { 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 timer_map = self.timer_map.read();
let task = match timer_map.get(uid) { let task = match timer_map.get(uid) {
Some(t) => t, Some(t) => t,
None => { None => {
logging!( logging!(warn, Type::Timer, "找不到对应的定时任务uid={}", uid);
warn,
Type::Timer,
"Corresponding timer task not found, uid={}",
uid
);
return None; return None;
} }
}; };
@@ -396,7 +380,7 @@ impl Timer {
let items = match profiles.get_items() { let items = match profiles.get_items() {
Some(i) => i, Some(i) => i,
None => { None => {
logging!(warn, Type::Timer, "Failed to get profile list"); logging!(warn, Type::Timer, "获取配置列表失败");
return None; return None;
} }
}; };
@@ -404,12 +388,7 @@ impl Timer {
let profile = match items.iter().find(|item| item.uid.as_deref() == Some(uid)) { let profile = match items.iter().find(|item| item.uid.as_deref() == Some(uid)) {
Some(p) => p, Some(p) => p,
None => { None => {
logging!( logging!(warn, Type::Timer, "找不到对应的配置uid={}", uid);
warn,
Type::Timer,
"Corresponding profile not found, uid={}",
uid
);
return None; return None;
} }
}; };
@@ -422,7 +401,7 @@ impl Timer {
logging!( logging!(
info, info,
Type::Timer, Type::Timer,
"Calculated next update time: {}, uid={}", "计算得到下次更新时间: {}, uid={}",
next_time, next_time,
uid uid
); );
@@ -431,7 +410,7 @@ impl Timer {
logging!( logging!(
warn, warn,
Type::Timer, Type::Timer,
"Invalid update time or interval, updated={}, interval={}", "更新时间或间隔无效,updated={}, interval={}",
updated, updated,
task.interval_minutes task.interval_minutes
); );
@@ -463,7 +442,7 @@ impl Timer {
logging!( logging!(
info, info,
Type::Timer, Type::Timer,
"Is profile {} currently active: {}", "配置 {} 是否为当前激活配置: {}",
uid, uid,
is_current is_current
); );

View File

@@ -3,6 +3,7 @@ use tauri::tray::TrayIconBuilder;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub mod speed_rate; pub mod speed_rate;
use crate::{ use crate::{
cmd,
config::Config, config::Config,
feat, logging, feat, logging,
module::{lightweight::is_in_lightweight_mode, mihomo::Rate}, module::{lightweight::is_in_lightweight_mode, mihomo::Rate},
@@ -45,7 +46,7 @@ fn should_handle_tray_click() -> bool {
*last_click = now; *last_click = now;
true true
} else { } 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()); now.duration_since(*last_click).as_millis());
false false
} }
@@ -230,7 +231,7 @@ impl Tray {
let app_handle = match handle::Handle::global().app_handle() { let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle, Some(handle) => handle,
None => { None => {
log::warn!(target: "app", "Failed to update tray menu: app_handle not found"); log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在");
return Ok(()); return Ok(());
} }
}; };
@@ -278,11 +279,11 @@ impl Tray {
profile_uid_and_name, profile_uid_and_name,
is_lightweight_mode, is_lightweight_mode,
)?)); )?));
log::debug!(target: "app", "Tray menu updated successfully"); log::debug!(target: "app", "托盘菜单更新成功");
Ok(()) Ok(())
} }
None => { None => {
log::warn!(target: "app", "Failed to update tray menu: tray not found"); log::warn!(target: "app", "更新托盘菜单失败: 托盘不存在");
Ok(()) Ok(())
} }
} }
@@ -294,7 +295,7 @@ impl Tray {
let app_handle = match handle::Handle::global().app_handle() { let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle, Some(handle) => handle,
None => { None => {
log::warn!(target: "app", "Failed to update tray icon: app_handle not found"); log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
return Ok(()); return Ok(());
} }
}; };
@@ -302,7 +303,7 @@ impl Tray {
let tray = match app_handle.tray_by_id("main") { let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray, Some(tray) => tray,
None => { None => {
log::warn!(target: "app", "Failed to update tray icon: tray not found"); log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
return Ok(()); return Ok(());
} }
}; };
@@ -331,7 +332,7 @@ impl Tray {
let app_handle = match handle::Handle::global().app_handle() { let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle, Some(handle) => handle,
None => { None => {
log::warn!(target: "app", "Failed to update tray icon: app_handle not found"); log::warn!(target: "app", "更新托盘图标失败: app_handle不存在");
return Ok(()); return Ok(());
} }
}; };
@@ -339,7 +340,7 @@ impl Tray {
let tray = match app_handle.tray_by_id("main") { let tray = match app_handle.tray_by_id("main") {
Some(tray) => tray, Some(tray) => tray,
None => { None => {
log::warn!(target: "app", "Failed to update tray icon: tray not found"); log::warn!(target: "app", "更新托盘图标失败: 托盘不存在");
return Ok(()); return Ok(());
} }
}; };
@@ -375,7 +376,7 @@ impl Tray {
let app_handle = match handle::Handle::global().app_handle() { let app_handle = match handle::Handle::global().app_handle() {
Some(handle) => handle, Some(handle) => handle,
None => { None => {
log::warn!(target: "app", "Failed to update tray tooltip: app_handle not found"); log::warn!(target: "app", "更新托盘提示失败: app_handle不存在");
return Ok(()); return Ok(());
} }
}; };
@@ -383,7 +384,7 @@ impl Tray {
let version = match VERSION.get() { let version = match VERSION.get() {
Some(v) => v, Some(v) => v,
None => { None => {
log::warn!(target: "app", "Failed to update tray tooltip: version info not found"); log::warn!(target: "app", "更新托盘提示失败: 版本信息不存在");
return Ok(()); return Ok(());
} }
}; };
@@ -413,7 +414,7 @@ impl Tray {
if let Some(tray) = app_handle.tray_by_id("main") { if let Some(tray) = app_handle.tray_by_id("main") {
let _ = tray.set_tooltip(Some(&format!( let _ = tray.set_tooltip(Some(&format!(
"Koala Clash {version}\n{}: {}\n{}: {}\n{}: {}", "Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
t("SysProxy"), t("SysProxy"),
switch_map[system_proxy], switch_map[system_proxy],
t("TUN"), t("TUN"),
@@ -422,7 +423,7 @@ impl Tray {
current_profile_name current_profile_name
))); )));
} else { } else {
log::warn!(target: "app", "Failed to update tray tooltip: tray not found"); log::warn!(target: "app", "更新托盘提示失败: 托盘不存在");
} }
Ok(()) Ok(())
@@ -442,7 +443,7 @@ impl Tray {
pub fn unsubscribe_traffic(&self) {} pub fn unsubscribe_traffic(&self) {}
pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> { 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; let icon_bytes = TrayState::get_common_tray_icon().1;
@@ -490,20 +491,20 @@ impl Tray {
"tun_mode" => feat::toggle_tun_mode(None), "tun_mode" => feat::toggle_tun_mode(None),
"main_window" => { "main_window" => {
use crate::utils::window_manager::WindowManager; 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() { 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(); crate::module::lightweight::exit_lightweight_mode();
} }
let result = WindowManager::show_main_window(); 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); tray.on_menu_event(on_menu_event);
log::info!(target: "app", "System tray created successfully"); log::info!(target: "app", "系统托盘创建成功");
Ok(()) Ok(())
} }
@@ -600,6 +601,16 @@ fn create_tray_menu(
) )
.unwrap(); .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( let profiles = &Submenu::with_id_and_items(
app_handle, app_handle,
"profiles", "profiles",
@@ -639,6 +650,45 @@ fn create_tray_menu(
) )
.unwrap(); .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( let restart_clash = &MenuItem::with_id(
app_handle, app_handle,
"restart_clash", "restart_clash",
@@ -686,6 +736,7 @@ fn create_tray_menu(
separator, separator,
rule_mode, rule_mode,
global_mode, global_mode,
direct_mode,
separator, separator,
profiles, profiles,
separator, separator,
@@ -693,6 +744,8 @@ fn create_tray_menu(
tun_mode, tun_mode,
separator, separator,
lighteweight_mode, lighteweight_mode,
copy_env,
open_dir,
more, more,
separator, separator,
quit, quit,
@@ -717,18 +770,18 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
} }
"open_window" => { "open_window" => {
use crate::utils::window_manager::WindowManager; use crate::utils::window_manager::WindowManager;
log::info!(target: "app", "Tray menu click: open window"); log::info!(target: "app", "托盘菜单点击: 打开窗口");
if !should_handle_tray_click() { if !should_handle_tray_click() {
return; return;
} }
if crate::module::lightweight::is_in_lightweight_mode() { 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(); crate::module::lightweight::exit_lightweight_mode();
} }
let result = WindowManager::show_main_window(); let result = WindowManager::show_main_window();
log::info!(target: "app", "Window show result: {result:?}"); log::info!(target: "app", "窗口显示结果: {result:?}");
} }
"system_proxy" => { "system_proxy" => {
feat::toggle_system_proxy(); feat::toggle_system_proxy();
@@ -736,6 +789,16 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
"tun_mode" => { "tun_mode" => {
feat::toggle_tun_mode(None); 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_clash" => feat::restart_clash_core(),
"restart_app" => feat::restart_app(), "restart_app" => feat::restart_app(),
"entry_lightweight_mode" => { "entry_lightweight_mode" => {
@@ -753,7 +816,7 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
if was_lightweight { if was_lightweight {
use crate::utils::window_manager::WindowManager; use crate::utils::window_manager::WindowManager;
let result = WindowManager::show_main_window(); let result = WindowManager::show_main_window();
log::info!(target: "app", "Show main window after exiting lightweight mode: {result:?}"); log::info!(target: "app", "退出轻量模式后显示主窗口: {result:?}");
} }
} }
"quit" => { "quit" => {
@@ -767,6 +830,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) {
} }
if let Err(e) = Tray::global().update_all_states() { 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()), (self, core.as_str()),
(ChainSupport::All, _) (ChainSupport::All, _)
| (ChainSupport::Clash, "clash") | (ChainSupport::Clash, "clash")
| (ChainSupport::ClashMeta, "koala-mihomo") | (ChainSupport::ClashMeta, "verge-mihomo")
| (ChainSupport::ClashMetaAlpha, "koala-mihomo-alpha") | (ChainSupport::ClashMetaAlpha, "verge-mihomo-alpha")
), ),
None => true, 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()); let patch_tun = value.as_mapping().cloned().unwrap_or(Mapping::new());
for (key, value) in patch_tun.into_iter() { 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()); config.insert("tun".into(), tun.into());
} else { } else {
@@ -241,7 +239,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
.filter(|(s, _)| s.is_support(clash_core.as_ref())) .filter(|(s, _)| s.is_support(clash_core.as_ref()))
.map(|(_, c)| c) .map(|(_, c)| c)
.for_each(|item| { .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 { if let ChainType::Script(script) = item.data {
match use_script(script, config.to_owned(), "".to_string()) { match use_script(script, config.to_owned(), "".to_string()) {
Ok((res_config, _)) => { Ok((res_config, _)) => {

View File

@@ -141,8 +141,8 @@ fn test_script() {
fn test_escape_unescape() { fn test_escape_unescape() {
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#; let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
let escaped = escape_js_string_for_single_quote(test_string); let escaped = escape_js_string_for_single_quote(test_string);
println!("Original: {test_string}"); println!("Original: {}", test_string);
println!("Escaped: {escaped}"); println!("Escaped: {}", escaped);
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#; let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
let parsed = parse_json_safely(json_str).unwrap(); 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")] #[cfg(target_os = "macos")]
{ {
crate::utils::resolve::restore_public_dns().await; 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>, option: Option<PrfOption>,
auto_refresh: Option<bool>, auto_refresh: Option<bool>,
) -> Result<()> { ) -> Result<()> {
logging!( logging!(info, Type::Config, true, "[订阅更新] 开始更新订阅 {}", uid);
info,
Type::Config,
true,
"[Subscription Update] Start updating subscription {}",
uid
);
let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true保持兼容性 let auto_refresh = auto_refresh.unwrap_or(true); // 默认为true保持兼容性
let url_opt = { 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"); let is_remote = item.itype.as_ref().is_some_and(|s| s == "remote");
if !is_remote { if !is_remote {
log::info!(target: "app", "[Subscription Update] {uid} is not a remote subscription, skipping update"); log::info!(target: "app", "[订阅更新] {uid} 不是远程订阅,跳过更新");
None // 非远程订阅直接更新 None // 非远程订阅直接更新
} else if item.url.is_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"); bail!("failed to get the profile item url");
} else { } else {
log::info!(target: "app", log::info!(target: "app",
"[Subscription Update] {} is a remote subscription, URL: {}", "[订阅更新] {} 是远程订阅,URL: {}",
uid, uid,
item.url.clone().unwrap() item.url.clone().unwrap()
); );
@@ -64,24 +58,24 @@ pub async fn update_profile(
let should_update = match url_opt { let should_update = match url_opt {
Some((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()); let merged_opt = PrfOption::merge(opt.clone(), option.clone());
// 尝试使用正常设置更新 // 尝试使用正常设置更新
match PrfItem::from_url(&url, None, None, merged_opt.clone()).await { match PrfItem::from_url(&url, None, None, merged_opt.clone()).await {
Ok(item) => { Ok(item) => {
log::info!(target: "app", "[Subscription Update] Subscription config updated successfully"); log::info!(target: "app", "[订阅更新] 更新订阅配置成功");
let profiles = Config::profiles(); let profiles = Config::profiles();
let mut profiles = profiles.latest(); let mut profiles = profiles.latest();
profiles.update_item(uid.clone(), item)?; profiles.update_item(uid.clone(), item)?;
let is_current = Some(uid.clone()) == profiles.get_current(); 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 is_current && auto_refresh
} }
Err(err) => { Err(err) => {
// 首次更新失败尝试使用Clash代理 // 首次更新失败尝试使用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()); handle::Handle::notice_message("update_retry_with_clash", uid.clone());
@@ -98,7 +92,7 @@ pub async fn update_profile(
// 使用Clash代理重试 // 使用Clash代理重试
match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await { match PrfItem::from_url(&url, None, None, Some(fallback_opt)).await {
Ok(mut item) => { Ok(mut item) => {
log::info!(target: "app", "[Subscription Update] Update via Clash proxy succeeded"); log::info!(target: "app", "[订阅更新] 使用Clash代理更新成功");
// 恢复原始代理设置到item // 恢复原始代理设置到item
if let Some(option) = item.option.as_mut() { 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); handle::Handle::notice_message("update_with_clash_proxy", profile_name);
let is_current = Some(uid.clone()) == profiles.get_current(); 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 is_current && auto_refresh
} }
Err(retry_err) => { 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( handle::Handle::notice_message(
"update_failed_even_with_clash", "update_failed_even_with_clash",
format!("{retry_err}"), format!("{retry_err}"),
@@ -137,30 +131,14 @@ pub async fn update_profile(
}; };
if should_update { if should_update {
logging!( logging!(info, Type::Config, true, "[订阅更新] 更新内核配置");
info,
Type::Config,
true,
"[Subscription Update] Update core configuration"
);
match CoreManager::global().update_config().await { match CoreManager::global().update_config().await {
Ok(_) => { Ok(_) => {
logging!( logging!(info, Type::Config, true, "[订阅更新] 更新成功");
info,
Type::Config,
true,
"[Subscription Update] Update succeeded"
);
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
} }
Err(err) => { Err(err) => {
logging!( logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err);
error,
Type::Config,
true,
"[Subscription Update] Update failed: {}",
err
);
handle::Handle::notice_message("update_failed", format!("{err}")); handle::Handle::notice_message("update_failed", format!("{err}"));
log::error!(target: "app", "{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::process::AsyncHandler;
use crate::utils::window_manager::WindowManager; 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 创建死锁 // 热键调用调度到主线程执行,避免 WebView 创建死锁
if bypass_debounce { if bypass_debounce {
log::info!(target: "app", "Hotkey invoked, dispatching window operation to main thread"); log::info!(target: "app", "热键调用,调度到主线程执行窗口操作");
AsyncHandler::spawn(move || async move { 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() { if crate::module::lightweight::is_in_lightweight_mode() {
log::info!(target: "app", "Currently in lightweight mode, exiting 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() { pub fn quit() {
use crate::process::AsyncHandler; 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(); 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() { if let Some(window) = handle::Handle::global().get_window() {
let _ = window.hide(); let _ = window.hide();
log::info!(target: "app", "Window hidden"); log::info!(target: "app", "窗口已隐藏");
} }
// 使用异步任务处理资源清理,避免阻塞 // 使用异步任务处理资源清理,避免阻塞
AsyncHandler::spawn(move || async move { AsyncHandler::spawn(move || async move {
logging!( logging!(info, Type::System, true, "开始异步清理资源");
info,
Type::System,
true,
"Start asynchronous resource cleanup"
);
let cleanup_result = clean_async().await; let cleanup_result = clean_async().await;
logging!( logging!(
info, info,
Type::System, Type::System,
true, true,
"Resource cleanup completed, exit code: {}", "资源清理完成,退出代码: {}",
if cleanup_result { 0 } else { 1 } if cleanup_result { 0 } else { 1 }
); );
app_handle.exit(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 { async fn clean_async() -> bool {
use tokio::time::{timeout, Duration}; use tokio::time::{timeout, Duration};
logging!( logging!(info, Type::System, true, "开始执行异步清理操作...");
info,
Type::System,
true,
"Start executing asynchronous cleanup..."
);
// 1. 处理TUN模式 // 1. 处理TUN模式
let tun_task = async { let tun_task = async {
@@ -122,11 +112,11 @@ async fn clean_async() -> bool {
.await .await
{ {
Ok(_) => { Ok(_) => {
log::info!(target: "app", "TUN mode disabled"); log::info!(target: "app", "TUN模式已禁用");
true true
} }
Err(_) => { Err(_) => {
log::warn!(target: "app", "Timeout disabling TUN mode"); log::warn!(target: "app", "禁用TUN模式超时");
false false
} }
} }
@@ -144,11 +134,11 @@ async fn clean_async() -> bool {
.await .await
{ {
Ok(_) => { Ok(_) => {
log::info!(target: "app", "System proxy reset"); log::info!(target: "app", "系统代理已重置");
true true
} }
Err(_) => { Err(_) => {
log::warn!(target: "app", "Timeout resetting system proxy"); log::warn!(target: "app", "重置系统代理超时");
false false
} }
} }
@@ -158,11 +148,11 @@ async fn clean_async() -> bool {
let core_task = async { let core_task = async {
match timeout(Duration::from_secs(3), CoreManager::global().stop_core()).await { match timeout(Duration::from_secs(3), CoreManager::global().stop_core()).await {
Ok(_) => { Ok(_) => {
log::info!(target: "app", "Core service stopped"); log::info!(target: "app", "核心服务已停止");
true true
} }
Err(_) => { Err(_) => {
log::warn!(target: "app", "Timeout stopping core service"); log::warn!(target: "app", "停止核心服务超时");
false false
} }
} }
@@ -178,11 +168,11 @@ async fn clean_async() -> bool {
.await .await
{ {
Ok(_) => { Ok(_) => {
log::info!(target: "app", "DNS settings restored"); log::info!(target: "app", "DNS设置已恢复");
true true
} }
Err(_) => { Err(_) => {
log::warn!(target: "app", "Timeout restoring DNS settings"); log::warn!(target: "app", "恢复DNS设置超时");
false false
} }
} }
@@ -202,7 +192,7 @@ async fn clean_async() -> bool {
info, info,
Type::System, Type::System,
true, true,
"Asynchronous cleanup completed - TUN: {}, Proxy: {}, Core: {}, DNS: {}, Overall: {}", "异步清理操作完成 - TUN: {}, 代理: {}, 核心: {}, DNS: {}, 总体: {}",
tun_success, tun_success,
proxy_success, proxy_success,
core_success, core_success,
@@ -219,7 +209,7 @@ pub fn clean() -> bool {
let (tx, rx) = std::sync::mpsc::channel(); let (tx, rx) = std::sync::mpsc::channel();
AsyncHandler::spawn(move || async move { AsyncHandler::spawn(move || async move {
logging!(info, Type::System, true, "Start executing cleanup..."); logging!(info, Type::System, true, "开始执行清理操作...");
// 使用已有的异步清理函数 // 使用已有的异步清理函数
let cleanup_result = clean_async().await; let cleanup_result = clean_async().await;
@@ -230,13 +220,7 @@ pub fn clean() -> bool {
match rx.recv_timeout(std::time::Duration::from_secs(8)) { match rx.recv_timeout(std::time::Duration::from_secs(8)) {
Ok(result) => { Ok(result) => {
logging!( logging!(info, Type::System, true, "清理操作完成,结果: {}", result);
info,
Type::System,
true,
"Cleanup completed, result: {}",
result
);
result result
} }
Err(_) => { Err(_) => {
@@ -244,7 +228,7 @@ pub fn clean() -> bool {
warn, warn,
Type::System, Type::System,
true, true,
"Cleanup timed out, returning success to avoid blocking" "清理操作超时,返回成功状态避免阻塞"
); );
true true
} }

View File

@@ -7,7 +7,11 @@ mod module;
mod process; mod process;
mod state; mod state;
mod utils; 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 config::Config;
use std::sync::{Mutex, Once}; use std::sync::{Mutex, Once};
use tauri::AppHandle; use tauri::AppHandle;
@@ -82,75 +86,85 @@ impl AppHandleManager {
#[allow(clippy::panic)] #[allow(clippy::panic)]
pub fn run() { pub fn run() {
// Capture early deep link before any async setup (cold start on macOS) utils::network::NetworkManager::global().init();
utils::resolve::capture_early_deep_link_from_args();
utils::network::NetworkManager::global().init();
let _ = utils::dirs::init_portable_flag(); 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")] #[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
let devtools = tauri_plugin_devtools::init(); let devtools = tauri_plugin_devtools::init();
#[allow(unused_mut)] #[allow(unused_mut)]
let mut builder = tauri::Builder::default() let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|_app, argv, _cwd| { .plugin(tauri_plugin_notification::init())
// Handle deep link when a second instance is invoked: forward URL to the running instance .plugin(tauri_plugin_updater::Builder::new().build())
if let Some(url) = argv .plugin(tauri_plugin_clipboard_manager::init())
.iter() .plugin(tauri_plugin_process::init())
.find(|a| a.starts_with("clash://") || a.starts_with("koala-clash://")) .plugin(tauri_plugin_global_shortcut::Builder::new().build())
.cloned() .plugin(tauri_plugin_fs::init())
{ .plugin(tauri_plugin_dialog::init())
// Robust scheduling avoids races with lightweight/window .plugin(tauri_plugin_shell::init())
resolve::schedule_handle_deep_link(url); .plugin(tauri_plugin_deep_link::init())
} .setup(|app| {
})) logging!(info, Type::Setup, true, "开始应用初始化...");
.plugin(tauri_plugin_notification::init()) let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new();
.plugin(tauri_plugin_updater::Builder::new().build()) #[cfg(target_os = "macos")]
.plugin(tauri_plugin_clipboard_manager::init()) {
.plugin(tauri_plugin_process::init()) auto_start_plugin_builder = auto_start_plugin_builder
.plugin(tauri_plugin_global_shortcut::Builder::new().build()) .macos_launcher(MacosLauncher::LaunchAgent)
.plugin(tauri_plugin_fs::init()) .app_name(app.config().identifier.clone());
.plugin(tauri_plugin_dialog::init()) }
.plugin(tauri_plugin_shell::init()) let _ = app.handle().plugin(auto_start_plugin_builder.build());
.plugin(tauri_plugin_deep_link::init())
.setup(|app| {
logging!(info, Type::Setup, true, "Starting app initialization...");
// Register deep link handler as early as possible to not miss cold-start events (macOS) #[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
app.deep_link().on_open_url(|event| { {
let urls: Vec<String> = event.urls().iter().map(|u| u.to_string()).collect(); use tauri_plugin_deep_link::DeepLinkExt;
logging!(info, Type::Setup, true, "on_open_url received: {:?}", urls); logging!(info, Type::Setup, true, "注册深层链接...");
if let Some(url) = urls.first().cloned() { logging_error!(Type::System, true, app.deep_link().register_all());
resolve::schedule_handle_deep_link(url); }
}
});
let mut auto_start_plugin_builder = tauri_plugin_autostart::Builder::new(); app.deep_link().on_open_url(|event| {
#[cfg(target_os = "macos")] AsyncHandler::spawn(move || {
{ let url = event.urls().first().map(|u| u.to_string());
auto_start_plugin_builder = auto_start_plugin_builder async move {
.macos_launcher(MacosLauncher::LaunchAgent) if let Some(url) = url {
.app_name(app.config().identifier.clone()); logging_error!(Type::Setup, true, resolve_scheme(url).await);
} }
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
// 窗口管理 // 窗口管理
logging!( logging!(info, Type::Setup, true, "初始化窗口状态管理...");
info,
Type::Setup,
true,
"Initializing window state management..."
);
let window_state_plugin = tauri_plugin_window_state::Builder::new() let window_state_plugin = tauri_plugin_window_state::Builder::new()
.with_filename("window_state.json") .with_filename("window_state.json")
.with_state_flags(tauri_plugin_window_state::StateFlags::default()) .with_state_flags(tauri_plugin_window_state::StateFlags::default())
@@ -160,12 +174,7 @@ pub fn run() {
// 异步处理 // 异步处理
let app_handle = app.handle().clone(); let app_handle = app.handle().clone();
AsyncHandler::spawn(move || async move { AsyncHandler::spawn(move || async move {
logging!( logging!(info, Type::Setup, true, "异步执行应用设置...");
info,
Type::Setup,
true,
"Executing app setup asynchronously..."
);
match timeout( match timeout(
Duration::from_secs(30), Duration::from_secs(30),
resolve::resolve_setup_async(&app_handle), resolve::resolve_setup_async(&app_handle),
@@ -173,81 +182,49 @@ pub fn run() {
.await .await
{ {
Ok(_) => { Ok(_) => {
logging!(info, Type::Setup, true, "App setup completed successfully"); logging!(info, Type::Setup, true, "应用设置成功完成");
} }
Err(_) => { Err(_) => {
logging!( logging!(
error, error,
Type::Setup, Type::Setup,
true, true,
"App setup timed out (30s), continuing with subsequent steps" "应用设置超时(30秒),继续执行后续流程"
); );
} }
} }
}); });
logging!( logging!(info, Type::Setup, true, "执行主要设置操作...");
info,
Type::Setup,
true,
"Executing main setup operations..."
);
logging!(info, Type::Setup, true, "Initializing AppHandleManager..."); logging!(info, Type::Setup, true, "初始化AppHandleManager...");
AppHandleManager::global().init(app.handle().clone()); 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()); 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() { if let Err(e) = utils::init::init_config() {
logging!( logging!(error, Type::Setup, true, "初始化配置失败: {}", e);
error,
Type::Setup,
true,
"Failed to initialize config: {}",
e
);
} }
logging!(info, Type::Setup, true, "Initializing resources..."); logging!(info, Type::Setup, true, "初始化资源...");
if let Err(e) = utils::init::init_resources() { if let Err(e) = utils::init::init_resources() {
logging!( logging!(error, Type::Setup, true, "初始化资源失败: {}", e);
error,
Type::Setup,
true,
"Failed to initialize resources: {}",
e
);
} }
app.manage(Mutex::new(state::proxy::CmdProxyState::default())); app.manage(Mutex::new(state::proxy::CmdProxyState::default()));
app.manage(Mutex::new(state::lightweight::LightWeightState::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) tauri::async_runtime::spawn(async {
utils::resolve::replay_early_deep_link(); 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) logging!(info, Type::Setup, true, "初始化完成,继续执行");
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"
);
Ok(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
@@ -367,7 +344,7 @@ pub fn run() {
app.run(|app_handle, e| match e { app.run(|app_handle, e| match e {
tauri::RunEvent::Ready | tauri::RunEvent::Resumed => { 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()); AppHandleManager::global().init(app_handle.clone());
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
@@ -375,7 +352,7 @@ pub fn run() {
.get_handle() .get_handle()
.get_webview_window("main") .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"); let _ = window.set_title("Koala Clash");
} }
} }
@@ -387,10 +364,6 @@ pub fn run() {
} => { } => {
if !has_visible_windows { if !has_visible_windows {
AppHandleManager::global().set_activation_policy_regular(); 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()); AppHandleManager::global().init(app_handle.clone());
} }
@@ -411,6 +384,7 @@ pub fn run() {
match event { match event {
tauri::WindowEvent::CloseRequested { api, .. } => { tauri::WindowEvent::CloseRequested { api, .. } => {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
AppHandleManager::global().set_activation_policy_accessory();
if core::handle::Handle::global().is_exiting() { if core::handle::Handle::global().is_exiting() {
return; return;
} }
@@ -419,12 +393,7 @@ pub fn run() {
if let Some(window) = core::handle::Handle::global().get_window() { if let Some(window) = core::handle::Handle::global().get_window() {
let _ = window.hide(); let _ = window.hide();
} else { } else {
logging!( logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在");
warn,
Type::Window,
true,
"Tried to hide window but it does not exist"
);
} }
} }
tauri::WindowEvent::Focused(true) => { tauri::WindowEvent::Focused(true) => {

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ use std::{fs, os::windows::process::CommandExt, path::Path, path::PathBuf};
/// Windows 下的开机启动文件夹路径 /// Windows 下的开机启动文件夹路径
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn get_startup_dir() -> Result<PathBuf> { 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) let startup_dir = Path::new(&appdata)
.join("Microsoft") .join("Microsoft")
@@ -19,7 +19,7 @@ pub fn get_startup_dir() -> Result<PathBuf> {
.join("Startup"); .join("Startup");
if !startup_dir.exists() { if !startup_dir.exists() {
return Err(anyhow!("Startup directory does not exist: {:?}", startup_dir)); return Err(anyhow!("Startup 目录不存在: {:?}", startup_dir));
} }
Ok(startup_dir) Ok(startup_dir)
@@ -29,7 +29,7 @@ pub fn get_startup_dir() -> Result<PathBuf> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn get_exe_path() -> Result<PathBuf> { pub fn get_exe_path() -> Result<PathBuf> {
let exe_path = 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) Ok(exe_path)
} }
@@ -39,11 +39,11 @@ pub fn get_exe_path() -> Result<PathBuf> {
pub fn create_shortcut() -> Result<()> { pub fn create_shortcut() -> Result<()> {
let exe_path = get_exe_path()?; let exe_path = get_exe_path()?;
let startup_dir = get_startup_dir()?; 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() { if shortcut_path.exists() {
info!(target: "app", "Startup shortcut already exists"); info!(target: "app", "启动快捷方式已存在");
return Ok(()); return Ok(());
} }
@@ -59,36 +59,36 @@ pub fn create_shortcut() -> Result<()> {
let output = std::process::Command::new("powershell") let output = std::process::Command::new("powershell")
.args(["-Command", &powershell_command]) .args(["-Command", &powershell_command])
// Hide the PowerShell window // 隐藏 PowerShell 窗口
.creation_flags(0x08000000) // CREATE_NO_WINDOW .creation_flags(0x08000000) // CREATE_NO_WINDOW
.output() .output()
.map_err(|e| anyhow!("Failed to execute PowerShell command: {}", e))?; .map_err(|e| anyhow!("执行 PowerShell 命令失败: {}", e))?;
if !output.status.success() { if !output.status.success() {
let error_msg = String::from_utf8_lossy(&output.stderr); 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(()) Ok(())
} }
/// Remove the shortcut /// 删除快捷方式
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn remove_shortcut() -> Result<()> { pub fn remove_shortcut() -> Result<()> {
let startup_dir = get_startup_dir()?; 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() { if !shortcut_path.exists() {
info!(target: "app", "Startup shortcut does not exist, nothing to remove"); info!(target: "app", "启动快捷方式不存在,无需删除");
return Ok(()); 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(()) Ok(())
} }
@@ -96,7 +96,7 @@ pub fn remove_shortcut() -> Result<()> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn is_shortcut_enabled() -> Result<bool> { pub fn is_shortcut_enabled() -> Result<bool> {
let startup_dir = get_startup_dir()?; 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()) Ok(shortcut_path.exists())
} }

View File

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

View File

@@ -178,8 +178,9 @@ fn init_dns_config() -> Result<()> {
"default-nameserver".into(), "default-nameserver".into(),
Value::Sequence(vec![ Value::Sequence(vec![
Value::String("system".into()), Value::String("system".into()),
Value::String("223.6.6.6".into()),
Value::String("8.8.8.8".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()), Value::String("2001:4860:4860::8888".into()),
]), ]),
), ),
@@ -188,8 +189,7 @@ fn init_dns_config() -> Result<()> {
Value::Sequence(vec![ Value::Sequence(vec![
Value::String("8.8.8.8".into()), Value::String("8.8.8.8".into()),
Value::String("https://doh.pub/dns-query".into()), Value::String("https://doh.pub/dns-query".into()),
Value::String("https://dns.google/dns-query".into()), Value::String("https://dns.alidns.com/dns-query".into()),
Value::String("https://cloudflare-dns.com/dns-query".into()),
]), ]),
), ),
("fallback".into(), Value::Sequence(vec![])), ("fallback".into(), Value::Sequence(vec![])),
@@ -201,9 +201,8 @@ fn init_dns_config() -> Result<()> {
"proxy-server-nameserver".into(), "proxy-server-nameserver".into(),
Value::Sequence(vec![ Value::Sequence(vec![
Value::String("https://doh.pub/dns-query".into()), Value::String("https://doh.pub/dns-query".into()),
Value::String("https://dns.google/dns-query".into()), Value::String("https://dns.alidns.com/dns-query".into()),
Value::String("https://cloudflare-dns.com/dns-query".into()), Value::String("tls://223.5.5.5".into()),
Value::String("tls://1.1.1.1".into()),
]), ]),
), ),
("direct-nameserver".into(), Value::Sequence(vec![])), ("direct-nameserver".into(), Value::Sequence(vec![])),
@@ -247,7 +246,7 @@ fn init_dns_config() -> Result<()> {
help::save_yaml( help::save_yaml(
&dns_path, &dns_path,
&default_dns_config, &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| { crate::log_err!(dirs::clash_path().map(|path| {
if !path.exists() { 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(()) <Result<()>>::Ok(())
})); }));
crate::log_err!(dirs::verge_path().map(|path| { crate::log_err!(dirs::verge_path().map(|path| {
if !path.exists() { if !path.exists() {
help::save_yaml(&path, &IVerge::template(), Some("# Koala Clash"))?; help::save_yaml(&path, &IVerge::template(), Some("# Clash Verge"))?;
} }
<Result<()>>::Ok(()) <Result<()>>::Ok(())
})); }));
@@ -292,7 +291,7 @@ pub fn init_config() -> Result<()> {
crate::log_err!(dirs::profiles_path().map(|path| { crate::log_err!(dirs::profiles_path().map(|path| {
if !path.exists() { if !path.exists() {
help::save_yaml(&path, &IProfiles::template(), Some("# Koala Clash"))?; help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?;
} }
<Result<()>>::Ok(()) <Result<()>>::Ok(())
})); }));
@@ -372,8 +371,8 @@ pub fn init_scheme() -> Result<()> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER); let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let (clash, _) = hkcu.create_subkey("Software\\Classes\\Clash")?; let (clash, _) = hkcu.create_subkey("Software\\Classes\\Clash")?;
clash.set_value("", &"Koala Clash")?; clash.set_value("", &"Clash Verge")?;
clash.set_value("URL Protocol", &"Koala Clash URL Scheme Protocol")?; clash.set_value("URL Protocol", &"Clash Verge URL Scheme Protocol")?;
let (default_icon, _) = hkcu.create_subkey("Software\\Classes\\Clash\\DefaultIcon")?; let (default_icon, _) = hkcu.create_subkey("Software\\Classes\\Clash\\DefaultIcon")?;
default_icon.set_value("", &app_exe)?; default_icon.set_value("", &app_exe)?;
let (command, _) = hkcu.create_subkey("Software\\Classes\\Clash\\Shell\\Open\\Command")?; 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<()> { pub fn init_scheme() -> Result<()> {
let output = std::process::Command::new("xdg-mime") let output = std::process::Command::new("xdg-mime")
.arg("default") .arg("default")
.arg("koala-clash.desktop") .arg("clash-verge.desktop")
.arg("x-scheme-handler/clash") .arg("x-scheme-handler/clash")
.output()?; .output()?;
if !output.status.success() { if !output.status.success() {
@@ -398,33 +397,6 @@ pub fn init_scheme() -> Result<()> {
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub fn init_scheme() -> Result<()> { 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(()) Ok(())
} }

View File

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

View File

@@ -40,7 +40,7 @@ impl NetworkManager {
// 创建专用的异步运行时线程数限制为4个 // 创建专用的异步运行时线程数限制为4个
let runtime = Builder::new_multi_thread() let runtime = Builder::new_multi_thread()
.worker_threads(4) .worker_threads(4)
.thread_name("koala-clash-network") .thread_name("clash-verge-network")
.enable_io() .enable_io()
.enable_time() .enable_time()
.build() .build()
@@ -65,7 +65,7 @@ impl NetworkManager {
pub fn init(&self) { pub fn init(&self) {
self.init.call_once(|| { self.init.call_once(|| {
self.runtime.spawn(async { self.runtime.spawn(async {
logging!(info, Type::Network, true, "Initializing network manager"); logging!(info, Type::Network, true, "初始化网络管理器");
// 创建无代理客户端 // 创建无代理客户端
let no_proxy_client = ClientBuilder::new() let no_proxy_client = ClientBuilder::new()
@@ -81,7 +81,7 @@ impl NetworkManager {
let mut no_proxy_guard = NETWORK_MANAGER.no_proxy_client.lock().unwrap(); let mut no_proxy_guard = NETWORK_MANAGER.no_proxy_client.lock().unwrap();
*no_proxy_guard = Some(no_proxy_client); *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) { 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(); let mut client = self.self_proxy_client.lock().unwrap();
*client = None; *client = None;
@@ -323,8 +323,8 @@ impl NetworkManager {
use crate::utils::resolve::VERSION; use crate::utils::resolve::VERSION;
let version = match VERSION.get() { let version = match VERSION.get() {
Some(v) => format!("koala-clash/v{v}"), Some(v) => format!("clash-verge/v{v}"),
None => "koala-clash/unknown".to_string(), None => "clash-verge/unknown".to_string(),
}; };
builder = builder.user_agent(version); builder = builder.user_agent(version);
@@ -409,7 +409,7 @@ impl NetworkManager {
let watchdog = tokio::spawn(async move { let watchdog = tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(timeout_duration)).await; tokio::time::sleep(Duration::from_secs(timeout_duration)).await;
let _ = cancel_tx.send(()); 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! { let result = tokio::select! {

View File

@@ -3,11 +3,10 @@ use crate::AppHandleManager;
use crate::{ use crate::{
config::{Config, IVerge, PrfItem}, config::{Config, IVerge, PrfItem},
core::*, core::*,
core::handle::Handle,
logging, logging_error, logging, logging_error,
module::lightweight::{self, auto_lightweight_mode_init}, module::lightweight::{self, auto_lightweight_mode_init},
process::AsyncHandler, process::AsyncHandler,
utils::{init, logging::Type, server, window_manager::WindowManager}, utils::{init, logging::Type, server},
wrap_err, wrap_err,
}; };
use anyhow::{bail, Result}; use anyhow::{bail, Result};
@@ -24,6 +23,7 @@ use tauri::{AppHandle, Manager};
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tauri::Url; use tauri::Url;
use crate::config::PrfOption;
//#[cfg(not(target_os = "linux"))] //#[cfg(not(target_os = "linux"))]
// use window_shadows::set_shadow; // use window_shadows::set_shadow;
@@ -66,35 +66,6 @@ impl Default for UiReadyState {
// 获取UI就绪状态细节 // 获取UI就绪状态细节
static UI_READY_STATE: OnceCell<Arc<UiReadyState>> = OnceCell::new(); 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)> { fn get_window_creating_lock() -> &'static Mutex<(bool, Instant)> {
WINDOW_CREATING.get_or_init(|| Mutex::new((false, Instant::now()))) 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))) 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> { fn get_ui_ready_state() -> &'static Arc<UiReadyState> {
UI_READY_STATE.get_or_init(|| Arc::new(UiReadyState::default())) 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() { pub fn mark_ui_ready() {
let mut ready = get_ui_ready().write(); let mut ready = get_ui_ready().write();
*ready = true; *ready = true;
logging!(info, Type::Window, true, "UI marked as fully ready"); logging!(info, Type::Window, true, "UI已标记为完全就绪");
// 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
} }
// 重置UI就绪状态 // 重置UI就绪状态
@@ -145,83 +108,7 @@ pub fn reset_ui_ready() {
let mut stage = state.stage.write(); let mut stage = state.stage.write();
*stage = UiReadyStage::NotStarted; *stage = UiReadyStage::NotStarted;
} }
logging!(info, Type::Window, true, "UI readiness state has been reset"); logging!(info, Type::Window, true, "UI就绪状态已重置");
}
/// 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;
}
});
} }
pub async fn find_unused_port() -> Result<u16> { 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) { pub async fn resolve_setup_async(app_handle: &AppHandle) {
let start_time = std::time::Instant::now(); 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() { if VERSION.get().is_none() {
let version = app_handle.package_info().version.to_string(); let version = app_handle.package_info().version.to_string();
VERSION.get_or_init(|| { VERSION.get_or_init(|| {
logging!(info, Type::Setup, true, "Initializing version information: {}", version); logging!(info, Type::Setup, true, "初始化版本信息: {}", version);
version.clone() 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); logging_error!(Type::Config, true, Config::init_config().await);
// 启动时清理冗余的 Profile 文件 // 启动时清理冗余的 Profile 文件
logging!(info, Type::Setup, true, "Cleaning redundant profile files..."); logging!(info, Type::Setup, true, "清理冗余的Profile文件...");
let profiles = Config::profiles(); let profiles = Config::profiles();
if let Err(e) = profiles.latest().auto_cleanup() { 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 { } 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); logging_error!(Type::Core, true, CoreManager::global().init().await);
log::trace!(target: "app", "Starting embedded server..."); log::trace!(target: "app", "启动内嵌服务器...");
server::embed_server(); server::embed_server();
logging_error!(Type::Tray, true, tray::Tray::global().init()); logging_error!(Type::Tray, true, tray::Tray::global().init());
if let Some(app_handle) = handle::Handle::global().app_handle() { 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); let result = tray::Tray::global().create_tray_from_handle(&app_handle);
if result.is_ok() { if result.is_ok() {
logging!(info, Type::Tray, true, "System tray created successfully"); logging!(info, Type::Tray, true, "系统托盘创建成功");
} else if let Err(e) = result { } else if let Err(e) = result {
logging!(error, Type::Tray, true, "Failed to create system tray: {}", e); logging!(error, Type::Tray, true, "系统托盘创建失败: {}", e);
} }
} else { } else {
logging!( logging!(
error, error,
Type::Tray, Type::Tray,
true, 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_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()); logging_error!(Type::System, true, hotkey::Hotkey::global().init());
let elapsed = start_time.elapsed(); let elapsed = start_time.elapsed();
@@ -345,7 +232,7 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
info, info,
Type::Setup, Type::Setup,
true, true,
"Asynchronous task completed, time taken: {:?}", "异步设置任务完成,耗时: {:?}",
elapsed elapsed
); );
@@ -355,7 +242,7 @@ pub async fn resolve_setup_async(app_handle: &AppHandle) {
warn, warn,
Type::Setup, Type::Setup,
true, true,
"Asynchronous task setup takes a long time ({:?})", "异步设置任务耗时较长({:?})",
elapsed elapsed
); );
} }
@@ -387,12 +274,12 @@ pub fn create_window(is_show: bool) -> bool {
info, info,
Type::Window, Type::Window,
true, true,
"Creating/showing main window, is_show={}", "开始创建/显示主窗口, is_show={}",
is_show is_show
); );
if !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); lightweight::set_lightweight_mode(true);
handle::Handle::notify_startup_completed(); handle::Handle::notify_startup_completed();
return false; 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(app_handle) = handle::Handle::global().app_handle() {
if let Some(window) = app_handle.get_webview_window("main") { 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 is_show {
if window.is_minimized().unwrap_or(false) { 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 _ = window.unminimize();
} }
let show_result = window.show(); let _ = window.show();
let focus_result = window.set_focus(); let _ = window.set_focus();
// If showing or focusing fails (possibly destroyed webview after lightweight), fallback to recreate #[cfg(target_os = "macos")]
if show_result.is_err() || focus_result.is_err() { {
logging!( AppHandleManager::global().set_activation_policy_regular();
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;
} }
} else {
return true;
} }
return true;
} }
} }
@@ -442,7 +316,7 @@ pub fn create_window(is_show: bool) -> bool {
info, info,
Type::Window, Type::Window,
true, true,
"Window creation request ignored because recently created ({:?}ms)", "窗口创建请求被忽略,因为最近创建过 ({:?}ms)",
elapsed.as_millis() elapsed.as_millis()
); );
return false; return false;
@@ -453,7 +327,7 @@ pub fn create_window(is_show: bool) -> bool {
// ScopeGuard 确保创建状态重置,防止 webview 卡死 // ScopeGuard 确保创建状态重置,防止 webview 卡死
let _guard = scopeguard::guard(creating, |mut creating_guard| { let _guard = scopeguard::guard(creating, |mut creating_guard| {
*creating_guard = (false, Instant::now()); *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( match tauri::WebviewWindowBuilder::new(
@@ -470,16 +344,16 @@ pub fn create_window(is_show: bool) -> bool {
.visible(true) // 立即显示窗口,避免用户等待 .visible(true) // 立即显示窗口,避免用户等待
.initialization_script( .initialization_script(
r#" r#"
console.log('[Tauri] Window init script started'); console.log('[Tauri] 窗口初始化脚本开始执行');
function createLoadingOverlay() { function createLoadingOverlay() {
if (document.getElementById('initial-loading-overlay')) { if (document.getElementById('initial-loading-overlay')) {
console.log('[Tauri] Loading indicator already exists'); console.log('[Tauri] 加载指示器已存在');
return; return;
} }
console.log('[Tauri] Creating loading indicator'); console.log('[Tauri] 创建加载指示器');
const loadingDiv = document.createElement('div'); const loadingDiv = document.createElement('div');
loadingDiv.id = 'initial-loading-overlay'; loadingDiv.id = 'initial-loading-overlay';
loadingDiv.innerHTML = ` loadingDiv.innerHTML = `
@@ -498,7 +372,7 @@ pub fn create_window(is_show: bool) -> bool {
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
"></div> "></div>
</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> </div>
<style> <style>
@keyframes spin { @keyframes spin {
@@ -530,13 +404,13 @@ pub fn create_window(is_show: bool) -> bool {
createLoadingOverlay(); createLoadingOverlay();
} }
console.log('[Tauri] Window init script finished'); console.log('[Tauri] 窗口初始化脚本执行完成');
"#, "#,
) )
.build() .build()
{ {
Ok(newly_created_window) => { 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); update_ui_ready_stage(UiReadyStage::NotStarted);
@@ -546,7 +420,7 @@ pub fn create_window(is_show: bool) -> bool {
debug, debug,
Type::Window, Type::Window,
true, true,
"Async window task started (startup marked completed)" "异步窗口任务开始 (启动已标记完成)"
); );
// 先运行轻量模式检测 // 先运行轻量模式检测
@@ -557,7 +431,7 @@ pub fn create_window(is_show: bool) -> bool {
debug, debug,
Type::Window, Type::Window,
true, true,
"Sending verge://startup-completed event" "发送 verge://startup-completed 事件"
); );
handle::Handle::notify_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.show();
let _ = window_clone.set_focus(); let _ = window_clone.set_focus();
logging!(info, Type::Window, true, "Window shown immediately"); logging!(info, Type::Window, true, "窗口已立即显示");
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
AppHandleManager::global().set_activation_policy_regular(); AppHandleManager::global().set_activation_policy_regular();
@@ -583,7 +457,7 @@ pub fn create_window(is_show: bool) -> bool {
info, info,
Type::Window, Type::Window,
true, true,
"Start monitoring UI load status (up to {} seconds)...", "开始监控UI加载状态 (最多{}秒)...",
timeout_seconds timeout_seconds
); );
@@ -602,7 +476,7 @@ pub fn create_window(is_show: bool) -> bool {
debug, debug,
Type::Window, Type::Window,
true, true,
"UI loading status check... ({}s)", "UI加载状态检查... ({})",
check_count / 10 check_count / 10
); );
} }
@@ -612,7 +486,7 @@ pub fn create_window(is_show: bool) -> bool {
match wait_result { match wait_result {
Ok(_) => { 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() { if let Some(window) = handle::Handle::global().get_window() {
let _ = window.eval(r#" let _ = window.eval(r#"
@@ -629,7 +503,7 @@ pub fn create_window(is_show: bool) -> bool {
warn, warn,
Type::Window, Type::Window,
true, true,
"UI load monitoring timed out ({}s), but window is already visible", "UI加载监控超时({}秒),但窗口已正常显示",
timeout_seconds timeout_seconds
); );
*get_ui_ready().write() = true; *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 { } else {
logging!( logging!(
debug, debug,
Type::Window, Type::Window,
true, true,
"is_show is false; keeping window hidden" "is_showfalse,窗口保持隐藏状态"
); );
} }
}); });
true true
} }
Err(e) => { Err(e) => {
logging!(error, Type::Window, true, "Failed to build main window: {}", e); logging!(error, Type::Window, true, "主窗口构建失败: {}", e);
false 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 name: Option<String> = None;
let mut url_param: Option<String> = None; let mut url_param: Option<String> = None;
for (key, value) in link_parsed.query_pairs() { for (key, value) in link_parsed.query_pairs() {
match key.as_ref() { match key.as_ref() {
"name" => name = Some(value.into_owned()), "name" => name = Some(value.into_owned()),
"url" => { "url" => url_param = Some(percent_decode_str(&value).decode_utf8_lossy().to_string()),
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) => { Some(url) => {
log::info!(target:"app", "decoded subscription url: {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 { match PrfItem::from_url(url.as_ref(), name, None, None).await {
Ok(item) => { Ok(item) => {
let uid = item.uid.clone().unwrap(); let uid = item.uid.clone().unwrap();
let _ = wrap_err!(Config::profiles().data().append_item(item)); 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); handle::Handle::notice_message("import_sub_url::ok", uid);
} }
Err(e) => { Err(e) => {

View File

@@ -7,7 +7,8 @@ use crate::{
process::AsyncHandler, process::AsyncHandler,
utils::logging::Type, utils::logging::Type,
}; };
use anyhow::Result; use anyhow::{bail, Result};
use port_scanner::local_port_available;
use std::convert::Infallible; use std::convert::Infallible;
use warp::Filter; use warp::Filter;
@@ -16,6 +17,32 @@ struct QueryParam {
param: String, 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 /// The embed server only be used to implement singleton process
/// maybe it can be used as pac server later /// maybe it can be used as pac server later
pub fn embed_server() { pub fn embed_server() {

View File

@@ -1,7 +1,7 @@
//! Some config file template //! Some config file template
/// template for new a profile item /// 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: [] proxies: []
@@ -11,13 +11,13 @@ rules: []
"; ";
/// enhanced profile /// 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: profile:
store-selected: true 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 /// 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: [] prepend: []
@@ -40,7 +40,7 @@ delete: []
"; ";
/// enhanced profile /// 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: [] prepend: []
@@ -50,7 +50,7 @@ delete: []
"; ";
/// enhanced profile /// 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: [] prepend: []

View File

@@ -53,7 +53,7 @@ fn get_window_operation_debounce() -> &'static Mutex<Instant> {
fn should_handle_window_operation() -> bool { fn should_handle_window_operation() -> bool {
if WINDOW_OPERATION_IN_PROGRESS.load(Ordering::Acquire) { 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; return false;
} }
@@ -62,16 +62,16 @@ fn should_handle_window_operation() -> bool {
let now = Instant::now(); let now = Instant::now();
let elapsed = now.duration_since(*last_operation); 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); elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) { if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) {
*last_operation = now; *last_operation = now;
WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release); WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release);
log::info!(target: "app", "[debounce] Window operation allowed to execute"); log::info!(target: "app", "[防抖] 窗口操作被允许执行");
true true
} else { } 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); elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS);
false false
} }
@@ -127,7 +127,7 @@ impl WindowManager {
finish_window_operation(); finish_window_operation();
}); });
logging!(info, Type::Window, true, "Starting smart show for main window"); logging!(info, Type::Window, true, "开始智能显示主窗口");
logging!( logging!(
debug, debug,
Type::Window, Type::Window,
@@ -140,18 +140,18 @@ impl WindowManager {
match current_state { match current_state {
WindowState::NotExist => { WindowState::NotExist => {
logging!(info, Type::Window, true, "Main window not found; creating new window"); logging!(info, Type::Window, true, "窗口不存在,创建新窗口");
if Self::create_new_window() { 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)); std::thread::sleep(std::time::Duration::from_millis(100));
WindowOperationResult::Created WindowOperationResult::Created
} else { } else {
logging!(warn, Type::Window, true, "Window creation failed"); logging!(warn, Type::Window, true, "窗口创建失败");
WindowOperationResult::Failed WindowOperationResult::Failed
} }
} }
WindowState::VisibleFocused => { WindowState::VisibleFocused => {
logging!(info, Type::Window, true, "Window already visible and focused; no action needed"); logging!(info, Type::Window, true, "窗口已经可见且有焦点,无需操作");
WindowOperationResult::NoAction WindowOperationResult::NoAction
} }
WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => { WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => {
@@ -184,14 +184,14 @@ impl WindowManager {
finish_window_operation(); 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(); let current_state = Self::get_main_window_state();
logging!( logging!(
info, info,
Type::Window, Type::Window,
true, true,
"Current window state: {:?} | Details: {}", "当前窗口状态: {:?} | 详细状态: {}",
current_state, current_state,
Self::get_window_status_info() Self::get_window_status_info()
); );
@@ -199,7 +199,7 @@ impl WindowManager {
match current_state { match current_state {
WindowState::NotExist => { 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() { if Self::create_new_window() {
WindowOperationResult::Created WindowOperationResult::Created
@@ -212,26 +212,26 @@ impl WindowManager {
info, info,
Type::Window, Type::Window,
true, true,
"Window visible (focused: {}), hiding window", "窗口可见(焦点状态: {}),将隐藏窗口",
if current_state == WindowState::VisibleFocused { if current_state == WindowState::VisibleFocused {
"focused" "有焦点"
} else { } else {
"unfocused" "无焦点"
} }
); );
if let Some(window) = Self::get_main_window() { if let Some(window) = Self::get_main_window() {
match window.hide() { match window.hide() {
Ok(_) => { Ok(_) => {
logging!(info, Type::Window, true, "Window hidden successfully"); logging!(info, Type::Window, true, "窗口已成功隐藏");
WindowOperationResult::Hidden WindowOperationResult::Hidden
} }
Err(e) => { Err(e) => {
logging!(warn, Type::Window, true, "Failed to hide window: {}", e); logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e);
WindowOperationResult::Failed WindowOperationResult::Failed
} }
} }
} else { } else {
logging!(warn, Type::Window, true, "Unable to get window instance"); logging!(warn, Type::Window, true, "无法获取窗口实例");
WindowOperationResult::Failed WindowOperationResult::Failed
} }
} }
@@ -240,12 +240,12 @@ impl WindowManager {
info, info,
Type::Window, Type::Window,
true, true,
"Window exists but is hidden or minimized; activating" "窗口存在但被隐藏或最小化,将激活窗口"
); );
if let Some(window) = Self::get_main_window() { if let Some(window) = Self::get_main_window() {
Self::activate_window(&window) Self::activate_window(&window)
} else { } else {
logging!(warn, Type::Window, true, "Unable to get window instance"); logging!(warn, Type::Window, true, "无法获取窗口实例");
WindowOperationResult::Failed WindowOperationResult::Failed
} }
} }
@@ -254,35 +254,35 @@ impl WindowManager {
/// 激活窗口(取消最小化、显示、设置焦点) /// 激活窗口(取消最小化、显示、设置焦点)
fn activate_window(window: &WebviewWindow<Wry>) -> WindowOperationResult { 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; let mut operations_successful = true;
// 1. 如果窗口最小化,先取消最小化 // 1. 如果窗口最小化,先取消最小化
if window.is_minimized().unwrap_or(false) { 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() { 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; operations_successful = false;
} }
} }
// 2. 显示窗口 // 2. 显示窗口
if let Err(e) = window.show() { 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; operations_successful = false;
} }
// 3. 设置焦点 // 3. 设置焦点
if let Err(e) = window.set_focus() { 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; operations_successful = false;
} }
// 4. 平台特定的激活策略 // 4. 平台特定的激活策略
#[cfg(target_os = "macos")] #[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(); AppHandleManager::global().set_activation_policy_regular();
} }
@@ -294,7 +294,7 @@ impl WindowManager {
debug, debug,
Type::Window, Type::Window,
true, true,
"Failed to set always-on-top (non-critical): {}", "设置置顶失败(非关键错误): {}",
e e
); );
} }
@@ -304,38 +304,38 @@ impl WindowManager {
debug, debug,
Type::Window, Type::Window,
true, true,
"Failed to unset always-on-top (non-critical): {}", "取消置顶失败(非关键错误): {}",
e e
); );
} }
} }
if operations_successful { if operations_successful {
logging!(info, Type::Window, true, "Window activation successful"); logging!(info, Type::Window, true, "窗口激活成功");
WindowOperationResult::Shown WindowOperationResult::Shown
} else { } else {
logging!(warn, Type::Window, true, "Window activation partially failed"); logging!(warn, Type::Window, true, "窗口激活部分失败");
WindowOperationResult::Failed WindowOperationResult::Failed
} }
} }
/// 隐藏主窗口 /// 隐藏主窗口
pub fn hide_main_window() -> WindowOperationResult { 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() { match Self::get_main_window() {
Some(window) => match window.hide() { Some(window) => match window.hide() {
Ok(_) => { Ok(_) => {
logging!(info, Type::Window, true, "Window hidden"); logging!(info, Type::Window, true, "窗口已隐藏");
WindowOperationResult::Hidden WindowOperationResult::Hidden
} }
Err(e) => { Err(e) => {
logging!(warn, Type::Window, true, "Failed to hide window: {}", e); logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e);
WindowOperationResult::Failed WindowOperationResult::Failed
} }
}, },
None => { None => {
logging!(info, Type::Window, true, "Window does not exist; nothing to hide"); logging!(info, Type::Window, true, "窗口不存在,无需隐藏");
WindowOperationResult::NoAction WindowOperationResult::NoAction
} }
} }
@@ -376,7 +376,7 @@ impl WindowManager {
let is_minimized = Self::is_main_window_minimized(); let is_minimized = Self::is_main_window_minimized();
format!( 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", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": { "bundle": {
"active": true, "active": true,
@@ -13,7 +13,7 @@
], ],
"resources": ["resources", "resources/locales/*"], "resources": ["resources", "resources/locales/*"],
"publisher": "Koala Clash", "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", "copyright": "GNU General Public License v3.0",
"category": "DeveloperTool", "category": "DeveloperTool",
"shortDescription": "Koala Clash", "shortDescription": "Koala Clash",
@@ -26,7 +26,7 @@
"devUrl": "http://localhost:3000/" "devUrl": "http://localhost:3000/"
}, },
"productName": "Koala Clash", "productName": "Koala Clash",
"identifier": "io.github.koala-clash", "identifier": "io.github.clash-verge-rev.clash-verge-rev",
"plugins": { "plugins": {
"updater": { "updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERCQjQ1QjQ0QUJDQTU1RTkKUldUcFZjcXJSRnUwMjdXSERoZVQ1R0hHRDMrT3VkSmpvbDJmb01sN3ZpYWhVYnEwaWpYUWU4YU0K", "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERCQjQ1QjQ0QUJDQTU1RTkKUldUcFZjcXJSRnUwMjdXSERoZVQ1R0hHRDMrT3VkSmpvbDJmb01sN3ZpYWhVYnEwaWpYUWU4YU0K",
@@ -40,7 +40,7 @@
}, },
"deep-link": { "deep-link": {
"desktop": { "desktop": {
"schemes": ["clash", "koala-clash"] "schemes": ["clash", "clash-verge"]
} }
} }
}, },

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$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", "productName": "Koala Clash",
"bundle": { "bundle": {
"targets": ["app", "dmg"], "targets": ["app", "dmg"],

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,14 @@
import { AppDataProvider } from "./providers/app-data-provider"; import { AppDataProvider } from "./providers/app-data-provider";
import { ThemeProvider } from "@/components/layout/theme-provider";
import Layout from "./pages/_layout"; import Layout from "./pages/_layout";
function App() { function App() {
return ( return (
<AppDataProvider> <ThemeProvider>
<Layout /> <AppDataProvider>
</AppDataProvider> <Layout />
</AppDataProvider>
</ThemeProvider>
); );
} }
export default App; 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"> <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"
<g filter="url(#filter0_f_40_29)"> viewBox="0 0 117 27" style="enable-background:new 0 0 117 27;" xml:space="preserve">
<ellipse cx="512" cy="516" rx="254" ry="216" fill="url(#paint0_radial_40_29)" fill-opacity="0.3"/> <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>
<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> </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 { BaseErrorBoundary } from "./base-error-boundary";
export { Switch } from "./base-switch"; export { Switch } from "./base-switch";
export { BaseLoadingOverlay } from "./base-loading-overlay"; export { BaseLoadingOverlay } from "./base-loading-overlay";
export { NoticeManager } from "./NoticeManager";

View File

@@ -1,14 +1,15 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import React, { useMemo, useState, useEffect, useRef } from "react"; import React, { useMemo, useState, useEffect, RefObject } from "react";
import { import {
ColumnDef, ColumnDef,
flexRender, flexRender,
getCoreRowModel, getCoreRowModel,
useReactTable, useReactTable,
Header, Row,
ColumnSizingState, ColumnSizingState,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { TableVirtuoso, TableComponents } from "react-virtuoso";
import { import {
Table, Table,
@@ -26,30 +27,7 @@ import { cn } from "@root/lib/utils";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
interface IConnectionsItem { // Интерфейс для строки данных, которую использует react-table
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;
}
interface ConnectionRow { interface ConnectionRow {
id: string; id: string;
host: string; host: string;
@@ -67,78 +45,22 @@ interface ConnectionRow {
connectionData: IConnectionsItem; connectionData: IConnectionsItem;
} }
// Интерфейс для пропсов, которые компонент получает от родителя
interface Props { interface Props {
connections: IConnectionsItem[]; connections: IConnectionsItem[];
onShowDetail: (data: IConnectionsItem) => void; onShowDetail: (data: IConnectionsItem) => void;
scrollerRef: (element: HTMLElement | Window | null) => 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) => { export const ConnectionTable = (props: Props) => {
const { connections, onShowDetail, scrollerRef } = 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>(() => { const [columnSizing, setColumnSizing] = useState<ColumnSizingState>(() => {
try { try {
const saved = localStorage.getItem("connection-table-widths"); const saved = localStorage.getItem("connection-table-widths");
return saved return saved ? JSON.parse(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,
};
} catch { } catch {
return { 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,
};
} }
}); });
@@ -185,16 +107,13 @@ export const ConnectionTable = (props: Props) => {
header: () => t("Host"), header: () => t("Host"),
size: columnSizing?.host || 220, size: columnSizing?.host || 220,
minSize: 180, minSize: 180,
maxSize: 400,
}, },
{ {
accessorKey: "download", accessorKey: "download",
header: () => t("Downloaded"), header: () => t("Downloaded"),
size: columnSizing?.download || 88, size: columnSizing?.download || 88,
minSize: 80,
maxSize: 150,
cell: ({ getValue }) => ( cell: ({ getValue }) => (
<div className="text-right font-mono text-sm"> <div className="text-right">
{parseTraffic(getValue<number>()).join(" ")} {parseTraffic(getValue<number>()).join(" ")}
</div> </div>
), ),
@@ -203,10 +122,8 @@ export const ConnectionTable = (props: Props) => {
accessorKey: "upload", accessorKey: "upload",
header: () => t("Uploaded"), header: () => t("Uploaded"),
size: columnSizing?.upload || 88, size: columnSizing?.upload || 88,
minSize: 80,
maxSize: 150,
cell: ({ getValue }) => ( cell: ({ getValue }) => (
<div className="text-right font-mono text-sm"> <div className="text-right">
{parseTraffic(getValue<number>()).join(" ")} {parseTraffic(getValue<number>()).join(" ")}
</div> </div>
), ),
@@ -215,10 +132,8 @@ export const ConnectionTable = (props: Props) => {
accessorKey: "dlSpeed", accessorKey: "dlSpeed",
header: () => t("DL Speed"), header: () => t("DL Speed"),
size: columnSizing?.dlSpeed || 88, size: columnSizing?.dlSpeed || 88,
minSize: 80,
maxSize: 150,
cell: ({ getValue }) => ( cell: ({ getValue }) => (
<div className="text-right font-mono text-sm"> <div className="text-right">
{parseTraffic(getValue<number>()).join(" ")}/s {parseTraffic(getValue<number>()).join(" ")}/s
</div> </div>
), ),
@@ -227,10 +142,8 @@ export const ConnectionTable = (props: Props) => {
accessorKey: "ulSpeed", accessorKey: "ulSpeed",
header: () => t("UL Speed"), header: () => t("UL Speed"),
size: columnSizing?.ulSpeed || 88, size: columnSizing?.ulSpeed || 88,
minSize: 80,
maxSize: 150,
cell: ({ getValue }) => ( cell: ({ getValue }) => (
<div className="text-right font-mono text-sm"> <div className="text-right">
{parseTraffic(getValue<number>()).join(" ")}/s {parseTraffic(getValue<number>()).join(" ")}/s
</div> </div>
), ),
@@ -240,30 +153,26 @@ export const ConnectionTable = (props: Props) => {
header: () => t("Chains"), header: () => t("Chains"),
size: columnSizing?.chains || 340, size: columnSizing?.chains || 340,
minSize: 180, minSize: 180,
maxSize: 500,
}, },
{ {
accessorKey: "rule", accessorKey: "rule",
header: () => t("Rule"), header: () => t("Rule"),
size: columnSizing?.rule || 280, size: columnSizing?.rule || 280,
minSize: 180, minSize: 180,
maxSize: 400,
}, },
{ {
accessorKey: "process", accessorKey: "process",
header: () => t("Process"), header: () => t("Process"),
size: columnSizing?.process || 220, size: columnSizing?.process || 220,
minSize: 180, minSize: 180,
maxSize: 350,
}, },
{ {
accessorKey: "time", accessorKey: "time",
header: () => t("Time"), header: () => t("Time"),
size: columnSizing?.time || 120, size: columnSizing?.time || 120,
minSize: 100, minSize: 100,
maxSize: 180,
cell: ({ getValue }) => ( cell: ({ getValue }) => (
<div className="text-right font-mono text-sm"> <div className="text-right">
{dayjs(getValue<string>()).fromNow()} {dayjs(getValue<string>()).fromNow()}
</div> </div>
), ),
@@ -273,21 +182,18 @@ export const ConnectionTable = (props: Props) => {
header: () => t("Source"), header: () => t("Source"),
size: columnSizing?.source || 200, size: columnSizing?.source || 200,
minSize: 130, minSize: 130,
maxSize: 300,
}, },
{ {
accessorKey: "remoteDestination", accessorKey: "remoteDestination",
header: () => t("Destination"), header: () => t("Destination"),
size: columnSizing?.remoteDestination || 200, size: columnSizing?.remoteDestination || 200,
minSize: 130, minSize: 130,
maxSize: 300,
}, },
{ {
accessorKey: "type", accessorKey: "type",
header: () => t("Type"), header: () => t("Type"),
size: columnSizing?.type || 160, size: columnSizing?.type || 160,
minSize: 100, minSize: 100,
maxSize: 220,
}, },
], ],
[columnSizing], [columnSizing],
@@ -300,91 +206,92 @@ export const ConnectionTable = (props: Props) => {
onColumnSizingChange: setColumnSizing, onColumnSizingChange: setColumnSizing,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
columnResizeMode: "onChange", columnResizeMode: "onChange",
enableColumnResizing: true,
}); });
const totalTableWidth = useMemo(() => { const VirtuosoTableComponents = useMemo<TableComponents<Row<ConnectionRow>>>(
return table.getCenterTotalSize(); () => ({
}, [table.getState().columnSizing]); // Явно типизируем `ref` для каждого компонента
Scroller: React.forwardRef<HTMLDivElement>((props, ref) => (
if (connRows.length === 0) { <div className="h-full" {...props} ref={ref} />
return ( )),
<div className="flex h-full items-center justify-center"> Table: (props) => <Table {...props} className="w-full border-collapse" />,
<p className="text-muted-foreground">{t("No connections")}</p> TableHead: React.forwardRef<HTMLTableSectionElement>((props, ref) => (
</div> <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 ( return (
<div className="rounded-md border relative bg-background"> <div className="h-full rounded-md border overflow-hidden">
<Table {connRows.length > 0 ? (
className="w-full border-collapse table-fixed" <TableVirtuoso
style={{ scrollerRef={scrollerRef}
width: totalTableWidth, data={table.getRowModel().rows}
minWidth: "100%", components={VirtuosoTableComponents}
}} fixedHeaderContent={() =>
> table.getHeaderGroups().map((headerGroup) => (
<TableHeader> <TableRow
{table.getHeaderGroups().map((headerGroup) => ( key={headerGroup.id}
<TableRow className="hover:bg-transparent bg-background/95 backdrop-blur"
key={headerGroup.id} >
className="hover:bg-transparent border-b-0 h-10" {headerGroup.headers.map((header) => (
> <TableHead
{headerGroup.headers.map((header) => ( key={header.id}
<TableHead style={{ width: header.getSize() }}
key={header.id} className="p-2"
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">
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(
header.column.columnDef.header, header.column.columnDef.header,
header.getContext(), header.getContext(),
)} )}
</div> </TableHead>
{header.column.getCanResize() && ( ))}
<ColumnResizer header={header} /> </TableRow>
)} ))
</TableHead> }
))} itemContent={(index, row) => (
</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)}
>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell <TableCell
key={cell.id} 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() }}
style={{ className="p-2 whitespace-nowrap"
width: cell.column.getSize(), onClick={() => onShowDetail(row.original.connectionData)}
minWidth: cell.column.columnDef.minSize,
maxWidth: cell.column.columnDef.maxSize,
}}
> >
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell> </TableCell>
))} ))}
</TableRow> </>
))} )}
</TableBody> />
</Table> ) : (
<div className="flex h-full items-center justify-center">
<p>No results.</p>
</div>
)}
</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 { function getDelayBadgeVariant(
if (delayValue < 0) { delayValue: number,
return "text-muted-foreground border-border"; ): "default" | "secondary" | "destructive" | "outline" {
} if (delayValue < 0) return "secondary";
if (delayValue >= 150) { if (delayValue >= 150) return "destructive";
return "text-destructive border-destructive/40"; return "default";
}
return "text-green-600 border-green-500/40 dark:text-green-400 dark:border-green-400/30";
} }
// --- Дочерний компонент для элемента списка с "живым" обновлением пинга --- // --- Дочерний компонент для элемента списка с "живым" обновлением пинга ---
@@ -82,21 +80,20 @@ const ProxySelectItem = ({
}, [proxyName, groupName]); }, [proxyName, groupName]);
return ( return (
<SelectItem key={proxyName} value={proxyName}> <SelectItem key={proxyName} value={proxyName}>
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<Badge <span className="truncate">{proxyName}</span>
variant="outline" <Badge
className={cn( variant={getDelayBadgeVariant(delay)}
"mr-2 flex-shrink-0 px-2 h-5 w-8 justify-center transition-colors duration-300", className={cn(
getDelayColorClasses(delay), "ml-4 flex-shrink-0 px-2 h-5 justify-center transition-colors duration-300",
isJustUpdated && "bg-primary/10 border-primary/50", isJustUpdated && "bg-primary/20 border-primary/50",
)} )}
> >
{delay < 0 || delay > 10000 ? "---" : delay} {delay < 0 || delay > 10000 ? "---" : delay}
</Badge> </Badge>
<span className="truncate">{proxyName}</span> </div>
</div> </SelectItem>
</SelectItem>
); );
}; };
@@ -262,21 +259,11 @@ export const ProxySelectors: React.FC = () => {
?.all; ?.all;
if (sourceList) { if (sourceList) {
const rawOptions = sourceList options = sourceList
.map((proxy: any) => ({ .map((proxy: any) => ({
name: typeof proxy === "string" ? proxy : proxy.name, name: typeof proxy === "string" ? proxy : proxy.name,
})) }))
.filter((p: { name: string }) => p.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;
});
} }
if (sortType === "name") if (sortType === "name")
@@ -358,37 +345,44 @@ export const ProxySelectors: React.FC = () => {
{sortType === "name" && <WholeWord className="h-4 w-4" />} {sortType === "name" && <WholeWord className="h-4 w-4" />}
</Button> </Button>
</span> </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> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{sortType === "default" && <p>{t("Sort by default")}</p>} <p>{selectedProxy}</p>
{sortType === "delay" && <p>{t("Sort by delay")}</p>}
{sortType === "name" && <p>{t("Sort by name")}</p>}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </SelectTrigger>
<Select <SelectContent>
value={selectedProxy} {proxyOptions.map((proxy) => (
onValueChange={handleProxyChange} <ProxySelectItem
disabled={isDirectMode} key={proxy.name}
onOpenChange={handleProxyListOpen} proxyName={proxy.name}
> groupName={selectedGroup}
<SelectTrigger className="w-100"> />
<span className="truncate"> ))}
<SelectValue placeholder={t("Select a proxy...")} /> </SelectContent>
</span> </Select>
</SelectTrigger>
<SelectContent>
{proxyOptions.map((proxy) => (
<ProxySelectItem
key={proxy.name}
proxyName={proxy.name}
groupName={selectedGroup}
/>
))}
</SelectContent>
</Select>
</div>
</div> </div>
</TooltipProvider> </div>
</TooltipProvider>
); );
}; };

View File

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

View File

@@ -6,7 +6,7 @@ import { DialogRef } from "../base";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { t } from "i18next"; import { t } from "i18next";
import { Download, RefreshCw } from "lucide-react"; import {Download, RefreshCw} from "lucide-react";
import { useSidebar } from "../ui/sidebar"; import { useSidebar } from "../ui/sidebar";
interface Props { interface Props {
@@ -17,7 +17,7 @@ export const UpdateButton = (props: Props) => {
const { className } = props; const { className } = props;
const { verge } = useVerge(); const { verge } = useVerge();
const { auto_check_update } = verge || {}; const { auto_check_update } = verge || {};
const { state: sidebarState } = useSidebar(); const { state: sidebarState } = useSidebar();
const viewerRef = useRef<DialogRef>(null); const viewerRef = useRef<DialogRef>(null);
@@ -36,7 +36,7 @@ export const UpdateButton = (props: Props) => {
return ( return (
<> <>
<UpdateViewer ref={viewerRef} /> <UpdateViewer ref={viewerRef} />
{sidebarState === "collapsed" ? ( {sidebarState === 'collapsed' ? (
<Button <Button
variant="outline" variant="outline"
size="icon" 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 { useSetThemeMode, useThemeMode } from "@/services/states";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
getCurrentWebviewWindow,
WebviewWindow,
} from "@tauri-apps/api/webviewWindow";
import { Theme } from "@tauri-apps/api/window"; import { Theme } from "@tauri-apps/api/window";
export const useCustomTheme = () => { export const useCustomTheme = () => {
const appWindow: WebviewWindow = useMemo(() => getCurrentWebviewWindow(), []); const appWindow = useMemo(() => getCurrentWebviewWindow(), []);
const { verge } = useVerge(); const { verge } = useVerge();
const { theme_mode } = verge ?? {}; const { theme_mode } = verge ?? {};
const mode = useThemeMode(); const mode = useThemeMode();
const setMode = useSetThemeMode(); const setMode = useSetThemeMode();
const [systemTheme, setSystemTheme] = useState(() =>
window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light",
);
useEffect(() => { useEffect(() => {
setMode( setMode(
theme_mode === "light" || theme_mode === "dark" ? theme_mode : "system", theme_mode === "light" || theme_mode === "dark" ? theme_mode : "system",
); );
}, [theme_mode, setMode]); }, [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(() => { useEffect(() => {
const root = document.documentElement; 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.remove("light", "dark");
root.classList.add(activeTheme); root.classList.add(activeTheme);
appWindow.setTheme(activeTheme as Theme).catch(console.error);
}, [mode, appWindow]);
if (theme_mode === "system") { useEffect(() => {
appWindow.setTheme(null).catch(console.error); if (theme_mode !== "system") return;
} else { const unlistenPromise = appWindow.onThemeChanged(({ payload }) => {
appWindow.setTheme(activeTheme as Theme).catch(console.error); setMode(payload);
} });
}, [mode, systemTheme, appWindow, theme_mode]); return () => {
unlistenPromise.then((f) => f());
};
}, [theme_mode, appWindow, setMode]);
return {}; return {};
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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